fix(cluster): heartbeat-driven registration refresh for host_lan_ip/url/hardware#2
Closed
hognek wants to merge 2010 commits into
Closed
fix(cluster): heartbeat-driven registration refresh for host_lan_ip/url/hardware#2hognek wants to merge 2010 commits into
hognek wants to merge 2010 commits into
Conversation
…aylfc#1640) Design Studio's react-konva editor (add/transform/rotate elements, layers, undo/redo, PNG export, Magic image gen) was fully real but ephemeral -- closing the window wiped the design. Add a backend store + routes and a Library view so designs survive across sessions, mirroring the existing office/web/game studio persistence pattern (opaque JSON content blob, CRUD store + routes, a Library view for browsing saved work). The konva editing, undo/redo, MagicView and export code are untouched; this only reads the current {artboard, elements} state to save and writes it back on open. Docs-Reviewed: new tinyagentos/routes/designs.py and desktop/src/apps/designstudio/{designs-api.ts,LibraryView.tsx} follow the established office/web/game persistence pattern already documented for those studios; no new architectural surface to document.
…ent (jaylfc#1639) * feat(office): wire Write Assist + Calc "Ask your data" to the taOS agent Write's four Assist buttons (Rewrite, Shorten, Continue writing, Change tone) were drawn but had no onClick. They now take the current TipTap selection (or the whole doc if nothing is selected), stream a taOS agent reply via the same streamTaosAgentChat client Web Studio/Game Studio use, and apply the result as a single insertContentAt transaction so Ctrl+Z undoes the whole AI edit in one step (closeHistory() keeps it from merging with whatever transaction came before it). Calc's "Ask your data" card is now a real question/answer panel: it serializes the active sheet to CSV with the existing sheetToCsv helper, sends {question, csv} to the agent, and streams the answer into the panel. Read-only this pass -- it never writes cells back. The rail's dead "Assist" button in OfficeStudioApp now toggles a small hint panel pointing at where AI actually lives, instead of doing nothing. Both surfaces get loading state, Cancel (AbortController), and a non-blocking error message that leaves the document/panel untouched. * docs: doc-gate trailer for AI-wiring-only Office Studio change The apps rule in docs/doc-gate.toml fires on any added file under desktop/src/apps/*/** (including new test files), which is what tripped it here -- this PR wires existing Write/Calc UI to the agent and adds tests, it does not add or remove a desktop app, so there is no README change to make. Docs-Reviewed: no app was added/removed -- new files are officestudio tests (WriteView.test.tsx, CalcView.test.tsx) exercising the AI wiring; README's app list is unaffected. * fix(office): fold Kilo review findings into Write Assist and Calc Ask your data Addresses six review findings on the taOS agent wiring: - Guard the stream cleanup path with a monotonic run id in both WriteView's Assist flow and CalcView's askData, so a superseded run's abort/finally cleanup can never clobber a newer run's busy/preview state. - Keep the partial answer visible in CalcView when the stream errors mid-way, surfacing the error alongside it instead of wiping it. - Preserve single newlines in Assist output as hard breaks instead of collapsing them into spaces, while blank lines still start new paragraphs. - Delimit and frame both the Calc CSV data and the Write selected text as untrusted content in the prompts, with an explicit instruction not to follow anything inside the delimiters, closing the prompt injection gaps in both views. - Surface empty-result feedback in Calc when a stream completes with no answer, matching the existing WriteView behavior. Adds tests for the stale-abort race, error-keeps-partial-answer, newline preservation, empty-result feedback, and the delimited-data framing (including an injection-style cell/selection payload) for both views.
…#1603, jaylfc#1601) (jaylfc#1641) Manual verification confirmed the persistence layer already avoids the regression reported after beta.25: dock (position/icon size) and settings (wallpaper) are stored under separate keys and each save is a read-merge-write, so a Dock-only save can never touch the wallpaper field and vice versa. That invariant had no dedicated regression coverage, so it could silently break again on a future change to either save path. - tests/test_desktop_settings.py: two store-level tests asserting a wallpaper save survives a Dock update and a Dock save survives a wallpaper update. - desktop/src/hooks/__tests__/use-session-persistence.test.ts: two tests asserting the debounced auto-save effects PUT to their own endpoint only, with a payload that never carries the other setting's field. - tinyagentos/desktop_settings.py: comments on update_settings/update_dock documenting the merge-not-replace invariant for future maintainers.
…Design persistence, Images honesty, wallpaper fix) (jaylfc#1643)
release: promote dev to master (v1.0.0-beta.27)
…box-render pipeline (mock to functional) (jaylfc#1642) Turns App Studio's Build/Publish views from a hardcoded mockup into a working pipeline, reusing the Game Studio / userspace-apps machinery end to end. 1. Real generation. generate-app.ts streams the taos-agent with a file-block system prompt (app-authoring-prompt.ts) and parses the response with Game Studio's existing parseFileBlocks() convention. An empty parse, or a response missing an index.html entry point, falls back to a minimal honest starter page with a visible notice instead of faking success. 2. Generic packaging. New POST /api/userspace-apps/package builds a .taosapp (manifest.yaml + files) from an in-memory file map, mirroring routes/games.py's package_game but for callers with no server-side project store of their own. 3. BuildView now runs the whole pipeline on a single Build click: generate, analyze via the existing static security gate (a critical finding blocks the rest), package, install tagged "ai-generated", then renders the REAL installed app through SandboxedAppWindow -- install-then-render, so the actual bundle/CSP/broker and the server-side install gate all apply, not a client-side preview. The hardcoded "Chore Quest" mock and cosmetic capability chips are gone. 4. PublishView shows the app actually built in the Build view (handed off via a small module-level build session, since AppStudioApp only ever mounts one view at a time) instead of a hardcoded demo source. "Publish to my Store" and "Share with family" both install the real package (there's no separate public-store or family-sharing backend yet, same posture as Game/Web Studio's Share flow); "Export package" downloads it. The old cosmetic permission toggles are removed since the pipeline always packages permissions: [] in v1 and toggles that controlled nothing would now be actively misleading next to a real install action. Deferred: permission inference from the generated app (manifest always requests no permissions in v1) and an editable project store for in-progress builds. Docs-Reviewed: new files are App Studio feature source (real generation, packaging client, build-session hand-off) for the existing app; the package route is a generic addition to the existing userspace_apps.py route file, not a new route module. No new app or route surface, so no README addition is warranted.
… before install (jaylfc#169) (jaylfc#1645) * feat(licensing): weight-license metadata + non-commercial accept gate before install (jaylfc#169) App-catalog service manifests labeled model-backed services with only their CODE license (MIT), which quietly implied the model WEIGHTS were permissive too. For musicgen, musicgpt, and flux-fill the weights are non-commercial (Meta's CC-BY-NC 4.0 MusicGen weights; the FLUX.1-dev non-commercial license), so a user could install and use them commercially without ever seeing the restriction. Slice 1 (honesty): - Add weights_license and license_class fields to AppManifest (dataclass + from_file mapping) alongside the existing code license. - Correct the 3 manifests: keep license: MIT, add weights_license + license_class: non-commercial. Other services were audited and left as code-license-only (generic runtimes that run user-chosen weights, or custom community licenses that permit commercial use below a threshold, which is a different category from non-commercial). - Surface license/weights_license/license_class in the /api/store/catalog payload and render a "Non-commercial weights" badge on Store cards. - Add tests/test_services_manifests.py and extend tests/test_registry.py. Slice 2 (arms-length accept-gate): - LicenseAcceptancesStore keyed (user_id, app_id, license_id); wired onto app.state like the other stores. - install-v2 returns 412 needs_license_acceptance for a non-commercial-weights service the user has not accepted; a follow-up install with accepted: true records the acceptance then proceeds. Applies to admins too. - Frontend LicenseAcceptDialog: on 412, show the license dialog and re-POST with accepted: true on agree; block install until accepted. Also unblocks jaylfc#115 (RK image-gen flagged install) via the same gate. Docs-Reviewed: new file is a StoreApp sub-component (LicenseAcceptDialog), not a new desktop app; no user-facing docs affected * fix(store): start install-progress poller on license-accept install; clearer empty weights-license label Folds two Kilo review findings on jaylfc#1645: - handleLicenseAccept did not set busy=true, so the card's install-progress poller (keyed on busy, cleared when the 412 gate returned) never ran during the accepted install and the card sat idle behind the dialog. Flip busy back on for the second POST. - LicenseAcceptDialog now renders a clear fallback label when weights_license is empty instead of a blank "Weights license:" line.
…locking sync POST (jaylfc#1646) * fix(video-studio): async generation job (enqueue + poll) instead of blocking sync POST POST /api/video/generate used to block for the full multi-minute generation with no job id, no progress, and no way to recover from a client timeout or disconnect. It now enqueues a job and returns {job_id, status: "queued"} (202) immediately, running the actual backend call in a background task tracked in app.state._background_tasks (same pattern agent_chat_router.py already uses for fire-and-forget work). A new GET /api/video/jobs/{job_id} reports queued/running/done/error, with the saved video's filename/path in result once done. Job state is persisted in a small VideoJobStore(BaseStore), mirroring ConversionManager/TrainingManager. The store is a lazily-created singleton on app.state, mirroring routes/jobs.py's _get_queue, so no app.py lifespan wiring was needed. If no backend is configured at all, generate still fails fast with the existing 503 rather than enqueueing a doomed job. Once a backend is configured, backend-side failures (connect error, timeout, bad response) now surface on the job instead of the original request. VideoStudioApp.tsx polls the job endpoint on an interval with a capped attempt count, showing the existing generating/elapsed-time state without blocking the UI thread on a long-lived fetch. * fix(video-studio): harden video job store + poll path against Kilo findings - Use the full uuid4 hex as the job id instead of an 8-char truncation. The id is a persistent PRIMARY KEY, so a shortened id risked a collision (IntegrityError leaking as a 500 on enqueue). Add a test that two rapidly created jobs get distinct 32-char ids. - get_job now maps columns via an aiosqlite.Row dict factory instead of zipping cursor.description positionally, so it is not coupled to column order. - Guard json.loads on result_json in the status poll: a corrupt blob now downgrades to an error status with a clear message instead of 500ing the poller. - Make the background job's error-recording exception-safe and guarantee a terminal state: error recording is best-effort (logs on failure) and the done-status write is wrapped so a failed status write cannot leave the job stuck in "running". Add a test that an unexpected backend exception marks the job "error".
…pt gate, Video async generation) (jaylfc#1647)
release: promote dev to master (v1.0.0-beta.28)
…Offline) (jaylfc#1650) * feat(music-studio): render/bounce a song to a downloadable WAV Music Studio could play live and export MIDI/.taosong but produced no audio file. Add an offline Tone.Offline render that reuses the live engine's own scheduling (scheduleTrackNotes, buildSynthInstrument, volumeToDb, now exported from audio-engine.ts) so a bounce sounds like playback instead of a re-implementation of it. Sampled (smplr) instruments fetch samples from a CDN and drive playback off the real wall clock, neither of which works inside an OfflineAudioContext, so those tracks render with their closest synth substitute and the UI surfaces which tracks were swapped. Adds an "Export WAV" action next to the existing MIDI/JSON export with a rendering state and error/notice display. * docs: note wav-export files are music-studio feature source Docs-Reviewed: wav-export.ts and wav-export.test.ts are new source files inside the existing Music Studio app (desktop/src/apps/musicstudio/), not a new desktop app. They add offline WAV bounce to the existing app and register no new app in the app registry, so the apps structural rule does not require a README change. * fix(music-studio): harden WAV bounce guards per review - Reject a render when no audible track has notes (all-muted or empty) instead of inferring emptiness from the duration, so an all-muted song no longer bounces 2s of silence. - Replace the ToneAudioBuffer.get() non-null assertion with an explicit check that routes a missing render buffer to the friendly error path. - Clamp note start/duration ticks to non-negative in scheduleTrackNotes so a malformed note can't produce a negative transport position or duration in either the live engine or the offline bounce. Docs-Reviewed: source-only changes inside the existing Music Studio app; no app added or removed.
…er, asteroids) (jaylfc#1651) * feat(game-studio): add 4 playable game templates (runner, snake, tapper, asteroids) Adds Endless Runner, Neon Snake, Sky Tapper and Asteroid Miner as new starter templates alongside the existing 4, all self contained canvas 2D seeds with no external assets or network calls, matching the sandboxed preview CSP. Each was live verified in a real browser: canvas renders, no console errors, keyboard and pointer input advance state, and win/lose plus restart loops work. Docs-Reviewed: pure content addition to the existing template registry and gamestudio-seeds convention, no behavioral or pipeline change, no doc drift introduced * fix(game-studio): address Kilo review on new game templates - match-template: drop ambiguous "run"/"dodge" hints from endless-runner so they no longer overlap platformer-lite and top-down-collector; distinctive hints (runner, endless, obstacle) still resolve it, and the existing 4 templates keep matching their prompts. - sky-tapper: use canvas.height in resetGame for consistency with the rest of the loop instead of window.innerHeight. - endless-runner: break out of the collision scan once game over is set. - templates.test: drop the redundant files-length assertion implied by the index.html toContain check. Kept the snake self-collision as an array scan: at a 22x18 grid the snake is tiny and a Set mirror would add sync code against no measurable gain (simplicity over premature optimization). Docs-Reviewed: review follow-up on the template content added in this PR, no pipeline or public-doc change
…#86) (jaylfc#1652) * feat(coding-studio): real sandboxed live preview of workspace files (#86) Replace the fully mocked Preview view with a real preview of the active workspace's own index.html. The backend assembles a self-contained HTML document by inlining local, path-jailed CSS/JS/image references (data URIs for images/fonts); external refs, data URIs, mailto links, and anchors are left untouched and nothing is ever fetched over the network. The frontend fetches that document and renders it in an iframe sandboxed to allow-scripts only (no allow-same-origin), with real loading/empty/error states and a working desktop/tablet/phone width toggle. Removed the fake todo app, fake console log, and fake url bar; dropped Open in browser and Share to Store since neither wired to a real action in this build. * chore(coding-studio): doc-gate trailer for new test files Docs-Reviewed: two new test files only (PreviewView.test.tsx, test_coding_preview.py); no new documented app surface beyond the preview endpoint described in the PR body. * fix(coding-studio): fold Kilo review findings on live preview (#86) - charge the preview byte-budget by delta (not full replacement length) for link/script/css-url inlining, matching the img path, so large local assets no longer exhaust the cap prematurely - abort the in-flight preview fetch on workspace switch/unmount so a stale response can no longer overwrite fresher state - remove the Open-in-new-tab link, which rendered workspace HTML same-origin and escaped the iframe sandbox - add a test asserting the 2MB per-asset guard leaves oversized stylesheets un-inlined - document that the script-tag regex only inlines empty external scripts; inline scripts pass through untouched Docs-Reviewed: preview fold touches only existing coding-studio files and the backend route; no user-facing docs are affected
… 4 Game Studio templates) (jaylfc#1654)
release: 1.0.0-beta.29 -> master (Coding Studio live preview, Music WAV bounce, Game Studio templates)
jaylfc#1657) * feat(governance): per-agent LLM budget hard-stops (jaylfc#160 Slice 2) Add a cross-process budget store (raw sqlite3 + WAL, mirroring the existing LiteLLM key store) so an agent's LLM spend can be capped and enforced without a Postgres dependency. - agent_budget_store.py: AgentBudgetStore tracks max_budget_usd/spend_usd per agent, opened by path from both the controller and the LiteLLM proxy subprocess via TAOS_AGENT_BUDGETS. - litellm_callback.py: async_log_success_event increments the resolved agent's spend by cost_usd on every completion. - litellm_auth.py: user_api_key_auth rejects an over-budget agent with a 429 before dispatching the call. Fails open when budgets are not configured; never checked on the master-key admin path. - llm_proxy.py: create_agent_key now records max_budget instead of silently dropping it; start() exports TAOS_AGENT_BUDGETS alongside the existing keystore path. - routes/agents.py: GET/PUT /api/agents/{name}/budget and POST /api/agents/{name}/budget/reset for admin management. Lifecycle integration (auto-pause on breach), UI, and period-based auto-reset are explicit follow-ups, not part of this slice. * fix(governance): reject non-finite budget caps (fold Kilo review, jaylfc#160 Slice 2) A NaN max_budget_usd passed the bare "< 0" check and made is_over_budget always False (spend >= NaN is False), silently disabling the cap. Reject non-finite caps (NaN/inf) at the route with 400 and, as defense in depth, in AgentBudgetStore.set_budget which raises ValueError so the store never persists a cap that cannot be enforced. Also return spend_usd as 0.0 (not 0) when no budget row exists, for consistent JSON number types. Adds regression tests at both the store and route layers. * test(governance): fix master-key budget test to not assert litellm-hashed api_key The master-key bypass test asserted result.api_key == the raw master key, but litellm's UserAPIKeyAuth hashes it, so the test failed under CI where litellm is installed (it skipped locally where litellm is absent). Assert a non-None auth object instead: reaching the assertion at all proves the bypass, since an over-budget agent would have raised 429 first.
…es (jaylfc#1655) Bumps the spa-deps group with 20 updates in the /desktop directory: | Package | From | To | | --- | --- | --- | | [@codemirror/state](https://github.com/codemirror/state) | `6.6.0` | `6.7.0` | | [@codemirror/view](https://github.com/codemirror/view) | `6.43.1` | `6.43.5` | | [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.17` | `1.1.18` | | [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.18` | `2.1.19` | | [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.10` | `2.1.11` | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.1` | `2.3.2` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.1` | `1.3.2` | | [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.15` | `1.1.16` | | [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.10` | `1.2.11` | | [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-link) | `3.26.1` | `3.27.1` | | [@tiptap/extension-underline](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-underline) | `3.26.1` | `3.27.1` | | [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) | `3.26.1` | `3.27.1` | | [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) | `3.26.1` | `3.27.1` | | [motion](https://github.com/motiondivision/motion) | `12.40.0` | `12.42.2` | | [three](https://github.com/mrdoob/three.js) | `0.184.0` | `0.185.1` | | [@playwright/test](https://github.com/microsoft/playwright) | `1.61.0` | `1.61.1` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.1` | `4.3.2` | | [@types/three](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/three) | `0.184.1` | `0.185.0` | | [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.2` | `6.0.3` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.3` | Updates `@codemirror/state` from 6.6.0 to 6.7.0 - [Changelog](https://github.com/codemirror/state/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/state/commits) Updates `@codemirror/view` from 6.43.1 to 6.43.5 - [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/view/commits) Updates `@radix-ui/react-dialog` from 1.1.17 to 1.1.18 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog) Updates `@radix-ui/react-dropdown-menu` from 2.1.18 to 2.1.19 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu) Updates `@radix-ui/react-label` from 2.1.10 to 2.1.11 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label) Updates `@radix-ui/react-select` from 2.3.1 to 2.3.2 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-switch` from 1.3.1 to 1.3.2 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `@radix-ui/react-tabs` from 1.1.15 to 1.1.16 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs) Updates `@radix-ui/react-tooltip` from 1.2.10 to 1.2.11 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip) Updates `@tiptap/extension-link` from 3.26.1 to 3.27.1 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-link/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.27.1/packages/extension-link) Updates `@tiptap/extension-underline` from 3.26.1 to 3.27.1 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-underline/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.27.1/packages/extension-underline) Updates `@tiptap/pm` from 3.26.1 to 3.27.1 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/pm/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.27.1/packages/pm) Updates `@tiptap/react` from 3.26.1 to 3.27.1 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/react/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.27.1/packages/react) Updates `@tiptap/starter-kit` from 3.26.1 to 3.27.1 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/starter-kit/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.27.1/packages/starter-kit) Updates `motion` from 12.40.0 to 12.42.2 - [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md) - [Commits](motiondivision/motion@v12.40.0...v12.42.2) Updates `three` from 0.184.0 to 0.185.1 - [Release notes](https://github.com/mrdoob/three.js/releases) - [Commits](https://github.com/mrdoob/three.js/commits) Updates `@playwright/test` from 1.61.0 to 1.61.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.61.0...v1.61.1) Updates `@tailwindcss/vite` from 4.3.1 to 4.3.2 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-vite) Updates `@types/three` from 0.184.1 to 0.185.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/three) Updates `@vitejs/plugin-react` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/[email protected]/packages/plugin-react) Updates `tailwindcss` from 4.3.1 to 4.3.2 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss) Updates `vite` from 8.0.16 to 8.1.3 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.1.3/packages/vite) --- updated-dependencies: - dependency-name: "@codemirror/state" dependency-version: 6.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@codemirror/view" dependency-version: 6.43.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@playwright/test" dependency-version: 1.61.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-dialog" dependency-version: 1.1.18 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.19 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-label" dependency-version: 2.1.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-switch" dependency-version: 1.3.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-tabs" dependency-version: 1.1.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@radix-ui/react-tooltip" dependency-version: 1.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: "@tiptap/extension-link" dependency-version: 3.27.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@tiptap/extension-underline" dependency-version: 3.27.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@tiptap/pm" dependency-version: 3.27.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@tiptap/react" dependency-version: 3.27.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@tiptap/starter-kit" dependency-version: 3.27.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@types/three" dependency-version: 0.185.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: "@vitejs/plugin-react" dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: motion dependency-version: 12.42.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: tailwindcss dependency-version: 4.3.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: spa-deps - dependency-name: three dependency-version: 0.185.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: spa-deps - dependency-name: vite dependency-version: 8.1.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: spa-deps ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…aylfc#1648) (jaylfc#1658) Beta.27 regression on RK3588 (rkllama backend): model downloads sat at 0% forever and never showed as installed, even though the weight pulled fine. Two defects: Defect 1 (progress stuck at 0%): RkllamaInstaller.install drained /api/pull's ndjson stream but discarded every line, and _run_installer never touched the DownloadTask's byte counters, so _task_to_dict computed percent=0 for the whole pull. Fixed by parsing each ndjson line for numeric completed/total and forwarding it via an optional on_progress callback threaded through start_installer_task, which updates the task's downloaded_bytes/total_bytes. Malformed lines are ignored, never raised. Defect 2 (never shows installed): rkllama pulls the weight into its own model store, not models_dir, so the disk scan never finds it and a fresh pull left the model looking not-installed until the next 30s catalog poll. Fixed by recording the install to the registry (a corroborated breadcrumb) and forcing an immediate backend-catalog refresh after a successful pull, so the model shows downloaded right away. Mock-tested only (no real RK3588 hardware): all rkllama HTTP is mocked.
…ard-stops, frontend security bumps) (jaylfc#1659)
release: 1.0.0-beta.30 -> master (rkllama download fix, agent budget hard-stops, frontend security bumps)
…ted delegation (jaylfc#161) (jaylfc#1661) * feat(governance): agent org model - roles/titles/reporting lines + gated delegation (jaylfc#161) Data model: add title + reports_to columns to the agent registry via a guarded _post_init migration (PRAGMA table_info + ALTER, mirroring the status migration). register()/update() accept and return both fields; set_reporting validates manager-exists, rejects self-report and cycles (bounded chain walk), and get_org_tree/direct_reports expose the reporting forest. Org API: GET /api/agents/org returns the reporting tree (admin only); PUT /api/agents/{canonical_id}/org sets role/title/reports_to on an entry (owner-or-admin, 400 on cycle/self/nonexistent-manager), reusing the existing registry auth pattern. Delegate action class + gated delegation: add a delegate action class to the conservative default posture (SENSITIVE_ACTION_CLASSES + classify map). POST /api/agents/{from_agent}/delegate runs the same governance gate as skill-exec (deny -> 403 + board audit + receipt; require_approval -> live grant proceeds else a blocking delegation_gate Decision is queued; allow/grant -> assign the task to to_agent and notify on the project A2A channel). Approving the Decision grants the action and completes the delegation via decisions._apply_delegation_grant, mirroring the execution_gate path and leaving it intact. Reporting line is org metadata only in this slice: it does NOT reroute approvals. UI (org chart) and approval-rerouting are follow-ups. * style(governance): remove em dashes + clear doc-gate on org-model PR Replaces em dashes across the org-model + delegation code and tests with plain hyphens, per repo style. No logic change (only the routed delegation message text loses its em dash). Docs-Reviewed: adds agent org-model + gated-delegation API routes (/api/agents/org, PUT /api/agents/{id}/org, POST /api/agents/{from}/delegate), governance endpoints gated by the existing execution-policy layer; fuller coordination docs to follow. * fix(governance): fold Kilo review findings on the org model (jaylfc#161) - CRITICAL: reports_to is no longer settable via AgentRegistryStore.update(); all reporting-line changes must go through set_reporting so the self-report, cycle, and manager-exists checks can never be bypassed. No caller used the raw update path (the org route already uses set_reporting). - WARNING: the delegation gate now returns 503 when it cannot raise an approval Decision (no decision store, or create fails) instead of a fake 202 with an empty decision_id the caller cannot poll. Skipped with reason: the board_audit task_id="agent:{from_agent}" is the exact agent-scoped convention skill_exec uses for policy-deny audit events (matches the reviewed governance code, not a defect); the set_reporting TOCTOU is admin-only and get_org_tree is already depth/cycle-guarded, so a raced cycle cannot cause runaway recursion (a BEGIN IMMEDIATE wrap risks a sqlite isolation regression on the write path, deferred to a considered follow-up).
…ates (jaylfc#1653) Updates the requirements on [uvicorn[standard]](https://github.com/Kludex/uvicorn), [croniter](https://github.com/pallets-eco/croniter), [litellm[proxy]](https://github.com/BerriAI/litellm) and [setuptools](https://github.com/pypa/setuptools) to permit the latest version. Updates `uvicorn[standard]` to 0.50.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.49.0...0.50.0) Updates `croniter` from 6.2.2 to 6.2.3 - [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.2...6.2.3) Updates `litellm[proxy]` to 1.91.0 - [Release notes](https://github.com/BerriAI/litellm/releases) - [Commits](BerriAI/litellm@v1.90.0...v1.91.0) Updates `setuptools` to 83.0.0 - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](pypa/setuptools@v82.0.1...v83.0.0) --- updated-dependencies: - dependency-name: croniter dependency-version: 6.2.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps - dependency-name: litellm[proxy] dependency-version: 1.91.0 dependency-type: direct:production dependency-group: python-deps - dependency-name: setuptools dependency-version: 83.0.0 dependency-type: direct:development dependency-group: python-deps - dependency-name: uvicorn[standard] dependency-version: 0.50.0 dependency-type: direct:production dependency-group: python-deps ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…heir ready tasks (jaylfc#164) (jaylfc#1662) * feat(governance): agent org model - roles/titles/reporting lines + gated delegation (jaylfc#161) (jaylfc#1661) * feat(governance): agent org model - roles/titles/reporting lines + gated delegation (jaylfc#161) Data model: add title + reports_to columns to the agent registry via a guarded _post_init migration (PRAGMA table_info + ALTER, mirroring the status migration). register()/update() accept and return both fields; set_reporting validates manager-exists, rejects self-report and cycles (bounded chain walk), and get_org_tree/direct_reports expose the reporting forest. Org API: GET /api/agents/org returns the reporting tree (admin only); PUT /api/agents/{canonical_id}/org sets role/title/reports_to on an entry (owner-or-admin, 400 on cycle/self/nonexistent-manager), reusing the existing registry auth pattern. Delegate action class + gated delegation: add a delegate action class to the conservative default posture (SENSITIVE_ACTION_CLASSES + classify map). POST /api/agents/{from_agent}/delegate runs the same governance gate as skill-exec (deny -> 403 + board audit + receipt; require_approval -> live grant proceeds else a blocking delegation_gate Decision is queued; allow/grant -> assign the task to to_agent and notify on the project A2A channel). Approving the Decision grants the action and completes the delegation via decisions._apply_delegation_grant, mirroring the execution_gate path and leaving it intact. Reporting line is org metadata only in this slice: it does NOT reroute approvals. UI (org chart) and approval-rerouting are follow-ups. * style(governance): remove em dashes + clear doc-gate on org-model PR Replaces em dashes across the org-model + delegation code and tests with plain hyphens, per repo style. No logic change (only the routed delegation message text loses its em dash). Docs-Reviewed: adds agent org-model + gated-delegation API routes (/api/agents/org, PUT /api/agents/{id}/org, POST /api/agents/{from}/delegate), governance endpoints gated by the existing execution-policy layer; fuller coordination docs to follow. * fix(governance): fold Kilo review findings on the org model (jaylfc#161) - CRITICAL: reports_to is no longer settable via AgentRegistryStore.update(); all reporting-line changes must go through set_reporting so the self-report, cycle, and manager-exists checks can never be bypassed. No caller used the raw update path (the org route already uses set_reporting). - WARNING: the delegation gate now returns 503 when it cannot raise an approval Decision (no decision store, or create fails) instead of a fake 202 with an empty decision_id the caller cannot poll. Skipped with reason: the board_audit task_id="agent:{from_agent}" is the exact agent-scoped convention skill_exec uses for policy-deny audit events (matches the reviewed governance code, not a defect); the set_reporting TOCTOU is admin-only and get_org_tree is already depth/cycle-guarded, so a raced cycle cannot cause runaway recursion (a BEGIN IMMEDIATE wrap risks a sqlite isolation regression on the write path, deferred to a considered follow-up). * feat(agents): opt-in heartbeat loop - wake idle running agents with ready tasks (jaylfc#164) Adds a periodic tick (registered like routine_tick_loop) that sweeps running agents each interval and, for any agent that is idle (not already holding a claimed task), wakes it with its next ready task plus that task's relational context (project/ancestry via get_task_context + format_why). Wake reuses the same best-effort notify mechanism routines use: enqueue_user_message on the agent's bridge session plus a system post to the project's a2a channel. It never drives an ACP/LLM turn directly and never deploys or execs a non-running agent. Gated behind config.server["agent_heartbeat_enabled"] (default off), re-read every tick so it can be toggled without a restart. An in-memory per-(agent, task) debounce on app.state prevents re-waking the same pending task inside a 5 minute cooldown. Adds ProjectTaskStore.list_ready_tasks_for_assignee, the per-assignee counterpart to list_ready_tasks, reusing the existing ready_tasks view. * chore(deps): bump the python-deps group across 1 directory with 4 updates (jaylfc#1653) Updates the requirements on [uvicorn[standard]](https://github.com/Kludex/uvicorn), [croniter](https://github.com/pallets-eco/croniter), [litellm[proxy]](https://github.com/BerriAI/litellm) and [setuptools](https://github.com/pypa/setuptools) to permit the latest version. Updates `uvicorn[standard]` to 0.50.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.49.0...0.50.0) Updates `croniter` from 6.2.2 to 6.2.3 - [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.2...6.2.3) Updates `litellm[proxy]` to 1.91.0 - [Release notes](https://github.com/BerriAI/litellm/releases) - [Commits](BerriAI/litellm@v1.90.0...v1.91.0) Updates `setuptools` to 83.0.0 - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](pypa/setuptools@v82.0.1...v83.0.0) --- updated-dependencies: - dependency-name: croniter dependency-version: 6.2.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps - dependency-name: litellm[proxy] dependency-version: 1.91.0 dependency-type: direct:production dependency-group: python-deps - dependency-name: setuptools dependency-version: 83.0.0 dependency-type: direct:development dependency-group: python-deps - dependency-name: uvicorn[standard] dependency-version: 0.50.0 dependency-type: direct:production dependency-group: python-deps ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(agents): fold Kilo review findings on the heartbeat loop (jaylfc#164) - CRITICAL: a failed wake no longer stamps the rewake debounce. The wake helper now reports whether the primary enqueue actually reached the agent's queue; only successful wakes are debounced, so a transient enqueue failure retries on the next tick instead of silencing the agent for the whole cooldown. - Message id collision: the heartbeat message uses a fresh uuid as its id instead of reusing the task id (a task can be announced more than once); the task stays traceable via trace_id=heartbeat-{task_id}. - Adds a regression test asserting a failed wake retries next tick. Findings against already-merged org-model code (duplicate answer routing, grant-before-completion messaging) are tracked as follow-ups, not part of this PR. * test(agents): make time.time monkeypatch exhaust-proof in heartbeat tests Under CI the warning log's LogRecord creation also calls time.time(), which exhausted the two-value iterator and surfaced as 'coroutine raised StopIteration' in test (3.12). Use next(times, default) in all three debounce tests so extra calls return the final value instead of raising. --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ds leave 0% (jaylfc#1663) The jaylfc/rkllama fork's /api/pull endpoint streams plain-text progress lines (a "Downloading ... (N MB)..." banner followed by repeated bare "NN%" lines), not Ollama-style JSON with completed/total fields. The previous fix in jaylfc#1658 assumed ndjson and silently dropped every line via a failed json.loads, so on_progress was never called and the download task stayed at 0% the whole pull. _report_pull_progress now tries JSON completed/total first (so a future Ollama-compatible build still works) and falls back to parsing a bare percent line, reporting it as percent-of-100 so the task's percent tracks the real download. Error and banner lines are still ignored without raising.
… rkllama progress fix, crypto bump) (jaylfc#1664) * release: 1.0.0-beta.31 (agent org model + delegation, opt-in heartbeat loop, rkllama plain-text progress fix, cryptography bump) * ci: re-trigger checks (spa-build vitest flaked; 2610/2610 pass locally)
release: 1.0.0-beta.31 -> master (org model + delegation, heartbeat loop, rkllama progress fix, crypto bump)
) The jsdom vitest suite grew to ~2600 tests and intermittently failed the spa-build job on the 2-core runner with no assertion - a worker OOM-killed mid-run (it passes cleanly on beefier local machines). Three changes: - vite.config.ts: bound the fork pool (maxForks 2) and give each worker a 4GB heap via poolOptions.forks.execArgv, so GC has room before OOM; add retry: 1 so a single genuinely-flaky test retries instead of failing the gate. - vitest.setup.ts: stub HTMLCanvasElement.getContext (2d) + toDataURL, which jsdom does not implement - removes the "Not implemented: getContext" log spam and keeps canvas-touching components (charts, previews) stable in tests. - ci.yml: set NODE_OPTIONS max-old-space-size for the main vitest process and retry the step once as a belt-and-suspenders so a rare transient worker death never blocks a release. Full suite stays green locally (321 files, 2610 tests) with the getContext warnings gone.
…essage (jaylfc#174) The answer_decision handler routed a generic reply to the asking agent AND each grant handler (execution, delegation) routed its own, so a gated decision sent the agent two messages. Make all three _apply_*_grant functions return whether they routed; the caller sends the generic reply only when none did. Also make the delegation reply honest: only claim the task was assigned when complete_delegation actually succeeded. A failed assignment now tells the agent to retry instead of reporting success.
…est exec message) - Delegation: write the delegate grant only after complete_delegation succeeds, so a failed assignment no longer leaks a grant that would let a later delegation skip approval. Test asserts no live grant on failure. - Execution gate: track whether the grant write persisted; only say 'you may retry' when it did, otherwise tell the agent the grant save failed and to retry (a retry would re-prompt, so the old message was misleading). - Document that _apply_app_grant intentionally always returns False (app grants send no agent reply; the caller routes the generic answer).
…adds membership (audit jaylfc#4, jaylfc#11) (jaylfc#1845)
…reaper (jaylfc#1846) - already_installed compared rev-parse HEAD (a SHA) to HAILO_OLLAMA_REF (a branch name like main), which never matched, so every re-run needlessly re-fetched and re-installed. Compare resolved commits on both sides. - The unit's ExecStartPre reaped with pkill -9 -f 'hailo-ollama', which kills any process whose command line merely contains that string (an editor, a tail). Match the exact server invocation (binary + serve + port) instead. Note: the default upstream repo (hailo-ai/hailo-ollama) is not reachable via git ls-remote, so a commit-SHA pin cannot be added here; that is tracked separately under the Hailo backend work (jaylfc#1771).
…p element geometry (audit #1) (jaylfc#1842)
…hub binds author to signing key (audit jaylfc#7, jaylfc#8) (jaylfc#1847)
…istent repo (jaylfc#1850) The installer defaulted HAILO_OLLAMA_REPO to hailo-ai/hailo-ollama, which does not exist (git ls-remote: Repository not found), so every Hailo-10H install failed at clone. The Hailo-Ollama server is not a standalone repo: it ships inside hailo-ai/hailo_model_zoo_genai and is built from source there (its README: an Ollama-compatible API written in C++ on top of HailoRT). - Point the repo at hailo_model_zoo_genai, pinned to a real commit. - Replace the Python venv/pip build with the repo's actual cmake flow (configure -> build -> install), adding the C++ toolchain + OpenSSL deps. cmake --install lands the hailo-ollama binary in /usr/local/bin and manifests under /usr/local/share/hailo-ollama. - Resolve the binary from PATH (system install), not a venv. - Start the server bare (it has no serve subcommand or --port flag) and set the listen port via OLLAMA_HOST (bind 127.0.0.1 on the managed port 7836, off the banned default 8000), per the repo's docs/USAGE.rst. Not hardware-tested locally (no Hailo-10H here); validated by bash -n and the upstream build docs. doc62fr (jaylfc#1771) tests live on real hardware and reports logs.
…MA (boot-brick) (jaylfc#1852) The canvas and task stores put their element_id index in SCHEMA: CREATE INDEX ... ON project_canvas_elements(project_id, element_id) CREATE INDEX ... ON project_tasks(project_id, element_id) BaseStore runs SCHEMA (executescript) BEFORE _post_init, so on an existing pre-element_id database the index creation raised 'no such column: element_id' before the _post_init ALTER could add the column, crashing controller boot after an upgrade. Reproduced live: the Pi bricked on startup right after pulling this code (fresh installs were fine because SCHEMA creates the table WITH element_id, so CI never exercised the migration path). Move both indexes out of SCHEMA into _post_init, created after the ALTER. Same class of bug and fix as the registry active-handle index (jaylfc#1841). Regression test seeds a pre-element_id DB and asserts both stores boot, migrate the column, and build the index.
…S3) (jaylfc#1854) Add GET /api/a2a/bus/stream, an authenticated SSE proxy to the raw bus stream, gated exactly like the messages proxy. Add the stream path to the middleware read allowlist and forward the since cursor on the messages proxy.
* docs(design): external-agent project invite (link + PIN) design doc Plan for inviting a coding agent or CLI running on a different machine into a taOS project: a Members-panel invite that mints a short link plus a 4-digit PIN, redeeming into the existing consent/auth-request flow (pre-authorized scopes chosen at mint time, same _do_approve path, full audit trail). A successful redeem is guaranteed to make the agent a project member: the invite always grants project_tasks bound to its project, which drives the member-add + a2a sync in _do_approve (bec4186). The connecting agent declares its harness (kilo, grok, opencode, claude, aider) plus an optional label, and the handle is derived as {project_slug}-{harness}[-label] (e.g. myproject-grok, myproject-kilo-frontend); canonical_id keeps the existing timestamp suffix and handle collisions get a short numeric suffix. The redeem returns a connection bundle carrying the controller's LAN, mesh, and taOSgo relay endpoints with a health-probe fallback order, the project_tasks kanban surface, the authenticated A2A bus proxy, and a delivery contract: realtime SSE stream plus a timed-check fallback with an invite-time check_interval_secs override and a since-cursor convention. Security model follows the cluster pairing code: hashed single-use codes, 15-minute TTL, attempt caps, per-IP rate limiting, revocation. Includes alternatives considered for the two contentious calls (PIN-as-approval vs consent routing; taos.my short link vs controller-direct), a six-slice build plan, and open questions. * docs(invite): add taOS-owned agent workspace isolation section * docs(invite): approved-build addendum - Phase 1 LAN/tailnet, canvas in bundle, onboarding kit Jay approved the invite design (2026-07-16) with the Claude Code game-agent driver. Records the Phase-1 scope (LAN/tailnet, controller-direct URL; taos.my relay deferred), the Lead toggle in the mint dialog, and two additions: canvas endpoints in the connection bundle when canvas scopes are granted, and an onboarding kit (capability guide) served at redeem with taOS repo + agent-guide links, Projects/Canvas capabilities, the A2A contract, and explicit memory + timed-check instructions. * docs(invite): Phase 1 implementation plan (4 slices for the fleet) * docs(invite): agent-centric multi-project model + token decision Addendum 2: external agents are first-class registry citizens that can belong to many projects, invited from the Agents app as well as project Members, free to pick up jobs across projects and talk on A2A (fleet shared this way). Decisions: project created at mint (agent joins, does not create); token model is one agent-identity token with per-project access gated by grants (supersedes the epic's project-bound token for external agents). Adds S2a (relax the token project gate to a grant lookup) and S5 (Agents-app invite + assign-to-project); S1 store project_id becomes nullable. * docs(invite): delivery is timed-check first; SSE is not agent-realtime Addendum 3: an LLM agent is turn-based, not a persistent reactor, so an open SSE stream just buffers frames it reads on its next turn (a poll). The onboarding kit leads with the timed check as the reliable floor and describes SSE as an optional optimization. S3 (stream proxy) still ships for the taOS UI and as the future wake-loop pipe. True agent-realtime is a separate wake-trigger loop (jaylfc#93/jaylfc#99), out of scope for this feature.
…ft (jaylfc#1856) * docs(gate): document the agent-JWT API surface + gate auth_middleware drift The doc-gate is structural (fires on app/route/installer/catalog file add or remove), so behavior changes inside existing route files (the identity epic's canvas + doc-review scopes, per-project grant access) drifted the coordination doc without tripping it. Fixes: - agent-coordination.md now documents the full scoped agent-JWT surface (project_tasks, canvas_read/write, project_doc_review, a2a_send/receive) + the per-project grant model + how external agents onboard (consent / invite). - doc-gate.toml: new 'agent-api' rule so changes to auth_middleware.py require updating that doc (or a Docs-Reviewed trailer); add docs/agent-manual/index.md to the referenced-paths dead-link scan. * docs: reference the invite design by issue jaylfc#1780, not the unmerged file path
…#1780 S1) (jaylfc#1855) * feat(invite): project_invites store + mint/list/revoke routes * fix(invite): make project_id nullable for agent-centric invites An Agents-app invite registers an agent with no project bound; SQLite cannot drop NOT NULL via ALTER later, so relax it before the store ships. Docs-Reviewed: invite mint/list/revoke are admin-only project-management routes (UI-consumed, not part of the agent-JWT surface); the agent-facing contract is the S2 redeem route + connection bundle, specified in docs/design/external-agent-project-invite.md
jaylfc#1857) * feat(invite): ProjectMembers "Invite external agent" UI + dialog (jaylfc#1780 S4) * style(invite): restore trailing newline on ProjectMembers.tsx Docs-Reviewed: InviteAgentDialog.tsx is a sub-component of the existing Projects app, not a new desktop app; no README app-list change is warranted (the invite UX is covered by docs/design/external-agent-project-invite.md).
…) (jaylfc#1726) unregister_worker() now releases all active leases for the worker's resources before removing the worker from the registry. Previously only the heartbeat-timeout path in _monitor_loop released leases; explicit unregister (DELETE /api/cluster/workers/{name}) leaked them. Changes: - manager.py: unregister_worker() made async, acquires _lease_lock, releases all leases for the worker (exact name match), then pops the worker. Logs lease count released. - routes/cluster.py: await the now-async unregister_worker() call. - tests: 6 new tests (5 unit + 1 HTTP integration) covering release on unregister, noop for unknown worker, worker isolation, reclaim after unregister, and lock serialization. Co-authored-by: Hogne <[email protected]>
…fc#1858) * refactor(auth): extract approve_request_record helper Move the mint machinery out of _do_approve into a shared approve_request_record(request, *, record, granted_scopes, effective_project, decided_by, project_id) helper so the project-invite redeem path can call the exact same registration/token/grant/membership flow. The consent route now just resolves the admin picker values and forwards. No behaviour change: all existing auth-request tests stay green. Also add the method-sensitive exemptions (POST /api/projects/invites/redeem and GET /i/{invite_id}) and a shared per-IP rate-limit helper in auth_middleware, plus a mark_redeemed audit method on the invite store. * feat(invite): redeem route + connection bundle + onboarding kit (jaylfc#1780 S2) Add the auth-EXEMPT POST /api/projects/invites/redeem (PIN-proof, per-IP rate-limited) and content-negotiated GET /i/{invite_id} (machine JSON redeem contract + human HTML). Redeem derives the {project_slug}-{harness} [-{label}] handle, de-dupes it against active registry agents, and either auto-approves via approve_request_record (decided_by = invite creator) or leaves a pending request (manual mode). Returns build_connection_bundle: LAN/mesh controller endpoints, scope-exact apis (canvas routes only when the scope was granted, mirroring the middleware allowlist), the timed-check delivery contract, and an onboarding kit + guide_markdown. The bundle carries no token or secret. Extends tests/test_routes_project_invites.py (auto + manual redeem, handle derivation, canvas scoping, wrong-pin 403, double-redeem 409, bundle-no- secret, guide requirements). Updates docs/agent-coordination.md with the invite redeem surface.
…artbeat fixes (jaylfc#894/jaylfc#185, continues jaylfc#1718) (jaylfc#1859) * feat(scheduler): GPU arbiter — VRAM-accounted admission + queue + eviction Add GpuArbiter module that wraps the resource Scheduler with GPU-specific admission control to prevent concurrent-load driver crashes (Xid 62). Key features: - VRAM-aware admission: checks local VRAM probes and cluster worker leases before admitting GPU tasks - Priority queue: pending GPU tasks wait when VRAM is insufficient, dequeued in priority order when resources free up - Eviction: lower-priority running tasks can be evicted to make room for higher-priority work - Lease integration: claims/releases GPU leases via ClusterManager for distributed coordination - GPU resource registration: discovery.py now registers gpu-cuda-N resources when GPU backends are healthy - GPU_POTENTIAL_CAPABILITIES constant for UI latent-capability display 19 unit tests pass. Builds on Slice 1 (VRAM endpoint, jaylfc#893 leases). Task: t_d7208884. Fixes jaylfc#894. * fix(scheduler): preempt running GPU work on eviction, not just cancel futures _evict_task previously only cancelled the _arbiter_future on the Task object. Directly-admitted tasks never have one set, and for queued-then- admitted tasks the future is resolved by _drain_queue before _run_gpu_task completes. In both cases the running GPU work keeps executing and VRAM is never freed until natural completion. Now _run_gpu_task registers its asyncio.Task in _running_tasks, and _evict_task cancels that Task directly. CancelledError propagates to the payload, the finally block cleans up VRAM/leases, and eviction actually stops running GPU work. - Added _running_tasks dict to track asyncio.Tasks - _run_gpu_task registers current task and handles CancelledError - _evict_task cancels the asyncio.Task (in addition to _arbiter_future) - _drain_queue catches CancelledError from evicted tasks to keep queue processor alive - Finally block checks if task was already evicted (entry popped) before releasing lease — prevents double-release Fixes jaylfc#894. * fix(scheduler): clear lease-release ownership in eviction vs. completion _evict_task now uses pop(task_id, None) atomically instead of the check-then-pop pattern, eliminating the TOCTOU KeyError that could occur when _run_gpu_task's finally block pops the entry between the existence check and the pop. Ownership is now explicit: whoever pops self._running first releases the lease. _evict_task's pop is the sole lease releaser for evicted tasks; _run_gpu_task's finally only releases on normal completion (when it still finds the entry). Comments document the invariant. Refs jaylfc#894. Task: t_0c86d08e. * fix(tests): remove test_gpu_arbiter.py — uses jaylfc#1689-style API incompatible with stack The file was incorrectly brought in from the jaylfc#1689 version and references VramAllocation, total_vram_mb, headroom_mb, and evict_callback that don't exist in the stack's GpuArbiter. test_gpu_arbiter_894.py covers the same territory with the correct API. * fix(scheduler): close TOCTOU race in GPU admission with in-flight VRAM reservation _addmission_check read live nvidia-smi free VRAM with no reservation, so two concurrent submit_gpu calls could both pass before either model loaded — the exact concurrent-load crash (NVIDIA Xid 62) the arbiter exists to prevent. Add _reserved_vram_mb counter and _pending_reservations dict, guarded by _reservation_lock. _reserve_and_check() atomically checks admission against (probe - _reserved_vram_mb) and reserves on success. Reservations are released in _run_gpu_task's finally block and in _evict_task. _stats() now includes reserved_vram_mb and pending_reservations for observability. Fixes: PR jaylfc#1683 review feedback #2 Tests: 168 pass (25 arbiter + 143 cluster/worker) * fix(scheduler): move lease claim inside try/finally + subtract reserved VRAM in cluster path taOS jaylfc#1705 — reservation leak fix: - Move lease claim and _running registration inside try block so the finally always releases the VRAM reservation, even on claim failure. - Await claim_lease / release_lease (both are async on ClusterManager). - Make _evict_task and evict_lowest_priority async (they await release_lease). - Subtract _reserved_vram_mb in the cluster-mode admission path, matching the local-path behaviour so in-flight reservations are accounted for. - Update tests: FakeClusterManager methods made async, all _evict_task and evict_lowest_priority calls now awaited. Tests: 16/16 arbiter + 111/111 cluster pass. * fix(scheduler): non-blocking drain + eviction-to-make-room in GPU arbiter _drain_queue no longer blocks on _run_gpu_task. The admitted task is spawned as a background asyncio Task with a done-callback that propagates the result/exception to the submitter's _arbiter_future. This keeps the drain loop responsive on subsequent ticks so eviction-to-make-room can kick in when higher-priority tasks arrive while VRAM is full. Also adds eviction-to-make-room: when admission fails for a queued task the drain now calls evict_lowest_priority with the task's priority as the floor, then re-checks admission. Lower-priority running tasks are evicted to make room; higher-priority runners are left alone. Queue processing remains intentionally serial — only one task is admitted per drain cycle to avoid flooding the GPU with concurrent loads. PR jaylfc#1683 review feedback jaylfc#4. * fix(scheduler): use _reserve_and_check in post-eviction re-admit path taOS jaylfc#1706 — TOCTOU fix: - After eviction-to-make-room frees VRAM, the re-admit in _drain_queue used _check_admission (no reservation) instead of _reserve_and_check (atomic reservation). This spawned tasks with no reservation entry, re-opening the TOCTOU window and causing _reserved_vram_mb drift. - Changed to _reserve_and_check so post-eviction admission is atomic and properly tracked. - Also fixes the missing await on evict_lowest_priority (made async by the base jaylfc#1705 fix) and sync _evict_task calls in new tests. Tests: 22/22 arbiter pass. * fix(scheduler): add _running_lock coverage to eviction paths in GpuArbiter Make evict_lowest_priority, _evict_task, release_tasks_for_worker, stats, and running_tasks async and guard _running dict access with _running_lock. Previously _running was mutated under the lock in _run_gpu_task but read/popped without it in eviction paths — safe under single-thread-no-await but fragile. Lease release and task cancellation happen outside the lock to avoid deadlock. Update all callers: drain_worker → async, route handlers, and tests. 69 targeted tests pass. * fix(scheduler): wire GpuArbiter with real VRAM probe in app.py - Added vram_probe=_probe_nvidia_vram so the arbiter uses actual GPU VRAM instead of the default (0,0) probe that makes all checks pass. - Wired cluster_manager._gpu_arbiter so eviction paths that access the cluster manager can reach the arbiter. - Added gpu_arbiter.stop() call in shutdown sequence. - Added _gpu_arbiter attribute to ClusterManager.__init__. * fix(scheduler): add _running_lock coverage to eviction paths taOS jaylfc#1707 — _running_lock coverage fix: - _evict_task now holds _running_lock during the pop, preventing races with concurrent drain operations. - stats() made async (needed for _running_lock consistency). - All async call sites now properly awaited (evict_lowest_priority, _evict_task, stats, claim_lease, release_lease). Tests: 22/22 arbiter pass. * feat(cluster): worker auto-update with graceful drain, pause, install, restart Adds drain_worker/cancel_drain to ClusterManager for graceful worker detach without dropping inflight GPU tasks (taOS jaylfc#890). ClusterManager: - drain_worker(name, graceful=True): worker enters 'draining' status, excluded from routing/catalog/lease claims. When graceful=False, all leases are force-released and worker is marked offline. - cancel_drain(name): returns draining worker to 'online'. - _monitor_loop: auto-completes drain when all leases released; force-finishes stale drains on heartbeat timeout. - _worker_for_resource, get_workers_for_capability, aggregate_catalog: all exclude draining workers. Routes: - POST /api/cluster/workers/{name}/drain — begin graceful/force drain - POST /api/cluster/workers/{name}/cancel-drain — cancel in-progress drain - POST /api/cluster/workers/{name}/update — full auto-update orchestration: drain → deploy update-worker → restart → re-register Tests: 9 new tests (25 total in test_cluster.py), 191 cluster tests pass. * fix(cluster): use exact worker-name compare in lease drain matching Replace startswith(name + ':') with _parse_resource_id exact match to prevent draining worker 'foo' from releasing leases belonging to worker 'foo-bar' (same prefix-collision class as TOCTOU fixes). * feat(scheduler): pause/resume GPU queue + hardware-aware LLM admission taOS jaylfc#796 features extracted from jaylfc#1689 and rebased onto the arbiter stack: 1. pause/resume queue control: GpuArbiter.pause()/resume() halt/restart queue processing. Running tasks finish; queued tasks wait. Paused queue skips _drain_queue in _process_queue loop. 2. Hardware-aware LLM admission: submit_gpu() accepts required_gpu_arch (e.g. 'sm_86'). _check_gpu_arch_compatibility() checks cluster workers for compatible GPU hardware via model name and compute_cap field before admitting tasks. * fix(tests): add missing await to _evict_task calls in test_gpu_arbiter_894 _evict_task was made async in commit 1789ad0 (running-lock fix). Three cleanup calls were missing await. * fix(cluster): make drain_worker/cancel_drain async, hold _lease_lock on force-release, wire arbiter task cancellation jaylfc#1690 lock fix: drain_worker force-release and _monitor_loop stale-drain path now hold _lease_lock during self._leases mutation, serialized with claim/release/sweep. drain_worker and cancel_drain are now async (consistent with claim_lease/release_lease — all callers were already in async handlers). Cross-cutting wiring: added GpuArbiter.cancel_running_for_leases() and wired it into drain_worker(graceful=False) and _monitor_loop stale-drain path. When leases are force-released, any running GPU arbiter tasks for those leases are cancelled via _evict_task — eviction is no longer a prod no-op when the arbiter is running. Added TYPE_CHECKING import for GpuArbiter type annotation on ClusterManager._gpu_arbiter. * fix(cluster): move worker.status='offline' inside _lease_lock; return (cancelled, already_completed) from cancel_running_for_leases Addresses Kilo review findings on PR jaylfc#1718: CRITICAL: Race in stale-drain path — worker.status='offline' was set after releasing _lease_lock, allowing duplicate worker.leave on next monitor tick. Now set inside the lock (both drain_worker force-release and _monitor_loop stale-drain paths). WARNING: Same pattern in drain_worker(graceful=False) — fixed. SUGGESTION: cancel_running_for_leases now returns (cancelled, already_completed) so operators can distinguish force-kills from natural completions. Callers log both counts. * fix(scheduler): exact GPU-arch + resource-on-worker match (kilo review) - _check_gpu_arch_compatibility matches compute_cap exactly so sm_8 cannot prefix-match sm_86 (keeps substring match on the freeform model string). - Add _resource_on_worker using the cluster manager's exact _parse_resource_id so gpu-node does not collide with gpu-node-2 (same guard jaylfc#1726 applied to the lease paths); used by the arch check and cluster admission. --------- Co-authored-by: Hogne <[email protected]>
…nt-gated token) (jaylfc#1862) (jaylfc#1866) * feat(agents): per-project grants (3-col unique + legacy DB migration) Change grants UNIQUE from (canonical_id, scope) to (canonical_id, scope, project_id) so one identity can hold the same scope for multiple projects. Add an idempotent _post_init migration that rebuilds the table for an existing DB still on the old 2-column unique (boot-path safe). Make add_grant NULL-safe so re-approving a (canonical_id, scope, project_id) key is idempotent even with a NULL project_id. taOS jaylfc#1862 * feat(agents): grant-gated token authz (drop baked project claim) Remove the hard token-project pin in check_agent_scope_for_project. The registry JWT's project_id claim is now advisory only; a project is authorized purely from a matching active grant (grant-gated). One project-agnostic token now authorizes any project the agent holds a grant for; projects without a grant (or with an expired grant) 403. taOS jaylfc#1862 * feat(agents): join another project (reuse identity) + admin assign route Extract add_agent_to_project shared primitive that writes per-project grants, the idempotent project_members row, and a2a channel membership. Reuse the existing active identity when an invite/approve handle collides instead of 409ing. Add POST /api/projects/{project_id}/members/assign-agent (owner-or-admin gated) so the Agents-app UI can add an existing agent to a project. Note the grant-gated multi-project model in docs/agent-coordination.md. taOS jaylfc#1862 * fix(agents): bind multi-project ADD to validated project_id (kilo review) The reuse/ADD branch bound project grants + membership to effective_project, which is safe only by the invariant that effective_project == project_id whenever project scopes are granted (the guard rejects an absent project_id). Gate and bind the branch on the EXPLICIT validated project_id instead, so an agent-supplied record.project_id can never leak into the binding - safe by construction. Regression test: an agent that self-requests project C but is approved for project B lands in B only, never C. * fix(agents): serialize add_grant so concurrent same-key writes don't race The 3-column UNIQUE with a nullable project_id forced a non-atomic DELETE-then-INSERT (INSERT OR REPLACE cannot dedup a NULL project_id). Under concurrency two same-key writes could interleave: one DELETE removes the other's row before its SELECT-back (returning None -> AttributeError on _row_to_dict), or the second INSERT hits the unique. An asyncio.Lock now serializes the delete+insert+select on the store's single connection. This is exactly the race the kilo review flagged and the pre-existing test_concurrent_mint_same_handle_makes_one_row catches.
…ents app (jaylfc#1861) (jaylfc#1867) * feat(agents): AssignAgentToProjectDialog to share an agent with a project (jaylfc#1861) TDD: failing test first, then component. POSTs to /api/projects/{pid}/members/assign-agent with canonical_id, scopes (project_tasks always present), and is_lead when the Lead toggle is set. Docs-Reviewed: AssignAgentToProjectDialog.tsx is a sub-component of the existing Agents app, not a new desktop app (mirrors the InviteAgentDialog sub-component pattern from jaylfc#1857). * feat(agents): invite + assign external agents from the Agents app (jaylfc#1861) Add an admin-only 'Invite external agent' button that opens a project picker then the existing InviteAgentDialog, and a per-row 'Assign to project' action (admin/owner, active entries) that opens AssignAgentToProjectDialog. Docs-Reviewed: RegistryPanel changes and the new InviteExternalAgentPicker sub-component are part of the existing Agents app, not a new desktop app (mirrors jaylfc#1857).
…t brick (jaylfc#1865) (jaylfc#1868) * feat(ci): add static guard for SCHEMA-index-before-migration boot brick Introduce scripts/check_schema_migrations.py, a stdlib-only static check that flags any CREATE INDEX inside a store's SCHEMA string whose columns are added later by an ALTER TABLE ADD COLUMN migration. Such indexes crash boot on existing databases because SCHEMA runs before _post_init. Find taOS jaylfc#1865. * test(ci): cover schema-migration guard Add tests/test_check_schema_migrations.py asserting the real tinyagentos tree is clean, a synthetic violating store is flagged (table t, column newcol), and a safe store with an unrelated ADD COLUMN is not flagged. * ci(doc-gate): run schema-migration guard Wire scripts/check_schema_migrations.py into the doc-gate workflow so the job fails when a SCHEMA-index-before-migration violation is present.
* docs(design): GPU work queue spec (jaylfc#1864) — owner-approved, decisions locked Unified GPU work queue: all loads + inference through the arbiter, residency management (load-on-admit, unload-idle-LRU), the net-new unload primitive, visible queue position, and audit/benchmark telemetry via one front door. Decisions locked per owner review (fail-open gateway, correlation-header attribution, real pull cancel, event-driven wakeup, RK3588 probe in scope). * docs(plan): GPU work queue phased implementation plan (jaylfc#1864) 3 phases, 22 TDD slices (A: loads+shadow+wakeup+telemetry, B: residency+unload+ RK3588 probe, C: gateway inference+correlation+benchmark+position UX). Every section-10a decision mapped to a slice; verified against current code.
…ls/ (jaylfc#1871) * docs: add taos-development-skill (SKILL.md + soul.md) to .claude/skills/ Encodes taOS coding standards, conventions, common fix patterns, testing guide, PR workflow, and architecture map so any contributing agent writes code that lands right the first time. Two files: - SKILL.md: procedural knowledge — repo layout, architectural patterns, git workflow, testing, common fix patterns, doc-gate, catalog additions - soul.md: non-negotiable rules — target dev, Python 3.11 floor, conventional commits, CLA is human-signed, code style, safety rules Fixes jaylfc#1763 * docs: address Kilo review — 3.11 floor consistency, approximate counts, CI matrix wording - WARNING fixed: SKILL.md:288 'Python 3.10+' → 'Python 3.11+' to match soul.md - SUGGESTION fixed: note now says 'approximate counts as rough orientation' instead of claiming to avoid them - SUGGESTION fixed: CI matrix description says '3.12–3.13 (3.11 nightly only)' not 'full 3.11–3.13' - soul.md: removed misleading '(3.10+)' annotation on match/case availability next to 3.11 floor * docs: add Post-Push Bot Review Cycle section to SKILL.md Documents the bot review workflow: wait for Kilo/CodeRabbit after each push, address CRITICAL/WARNING findings before blocking for human review (max 2 cycles, ~30 min worst case). * docs(skill): use canonical jaylfc/taOS slug + normalize dashes Maintainer polish on top of @hognek's taos-development-skill: - correct the repo slug from jaylfc/tinyagentos to the canonical jaylfc/taOS in every gh/git command (the skill is run verbatim, so the old redirect-only slug would query the wrong-looking repo) (Kilo review) - normalize em dashes to hyphens to match the house no-em-dash convention --------- Co-authored-by: Hogne <[email protected]> Co-authored-by: jaylfc <[email protected]>
…art handling (jaylfc#1690) (jaylfc#1872) Two regressions carried in when the worker-drain feature landed via the GPU arbiter convergence (jaylfc#1859), both correctly identified by @hognek's review rounds on jaylfc#1690 (which is superseded by jaylfc#1859 and cannot merge cleanly): 1. _monitor_loop dropped the `if worker.name == "local": continue` guard, so a >30s stall of the controller's local_heartbeat_loop marks the controller's own `local` worker offline, drops its leases, and removes local backends from routing and the aggregate catalog. Restore the guard. 2. update_worker used a broad `except Exception` that always cancelled the drain and returned 502. The deploy request blocks until update-worker restarts the worker and drops the connection, so the happy path was reported as a failure. Split the handling: disconnect (RemoteProtocolError/ReadError/ReadTimeout) = accepted, keep draining, 200 status=updating; ConnectError = unreachable, cancel drain, 502; non-2xx / unexpected = cancel drain, 502. Also retain the drain/cancel-drain notification tasks in _background_tasks so they are not GC'd mid-flight. Tests: local-worker guard in _monitor_loop; update restart-disconnect keeps drain and returns updating; update connect-error cancels drain and returns 502. Design and both fixes spotted by @hognek in jaylfc#1690.
…rl/hardware Reconcile cluster manager worker metadata on every heartbeat instead of only at initial registration, so cluster routing (e.g. Skald) does not keep using stale host_lan_ip, url, or hardware after DHCP moves or LXC restarts. Also forward LXC storage counters (storage_cap_bytes, storage_used_bytes, bytes_deduped_total) from the heartbeat route to ClusterManager.heartbeat() — these were accepted by HeartbeatBody but silently dropped by the handler. - HeartbeatBody: add host_lan_ip, url, hardware fields (optional, None-safe) - ClusterManager.heartbeat(): apply host_lan_ip, url, hardware, and storage counters when present; None preserves prior values for legacy workers - routes/cluster.py worker_heartbeat: forward all new fields + storage counters - WorkerAgent.heartbeat(): re-resolve host_lan_ip/url/hardware on every pulse - Tests: 6 new in TestHeartbeatRegistrationRefresh (all 33 cluster+protocol unit tests pass, no regressions) Fixes jaylfc#1538
hognek
force-pushed
the
fix/heartbeat-drift-refresh
branch
from
July 17, 2026 16:26
3cce536 to
cbd5f2a
Compare
- Guard host_lan_ip, url, hardware against empty string/dict in heartbeat handler (truthiness check after is-not-None to reject empty strings) - Move detect_hardware + asdict imports outside heartbeat() try/except so import failures are not swallowed as controller unreachability
hognek
added a commit
that referenced
this pull request
Jul 18, 2026
The GPG verification block in update_to_master (lines 93-141) was unreachable in production — no production caller passes gpg_fingerprint/gpg_required (only tests call the function, and none pass those params). The production GPG path goes through verify_remote_commit (auto_update.py) and switch_to_branch (routes/settings.py). Remove the dead gpg_fingerprint/gpg_required params and the 48-line GPG block. The function keeps its original merge-to-master behaviour unchanged. Kilo finding #2 on PR jaylfc#1983.
hognek
added a commit
that referenced
this pull request
Jul 18, 2026
…cel arbiter on stale-update-available, gate online event - worker/agent.py: store lifecycle_status/lifecycle_reason in WorkerAgent, resend on periodic heartbeats so controller restart doesn't revert draining/updating workers to 'online' (CodeRabbit MAJOR #1) - manager.py: cancel GPU arbiter tasks on stale-update-available worker timeout, matching the cancellation already done for stale-drain and stale-update paths (CodeRabbit MAJOR #2) - manager.py: gate recovery 'worker.online' event on worker.status == 'online' to suppress false events for workers recovering into draining/updating state (CodeRabbit MINOR jaylfc#3) - tests/test_worker.py: accept **kwargs in fake_heartbeat mocks to match expanded heartbeat() signature
hognek
added a commit
that referenced
this pull request
Jul 18, 2026
…aylfc#890 C2) (jaylfc#1903) * feat(cluster): worker-initiated graceful pause + drain protocol (jaylfc#890 C2) Add worker-initiated status transitions via heartbeat: - heartbeat() accepts optional status and drain_reason fields - Workers can self-report 'update-available', 'draining', or 'updating' - Status 'update-available' workers remain routable (still serving) - Status 'draining' triggers same behavior as controller-initiated drain - Monitor loop handles stale update-available workers Controller side (ClusterManager): - _worker_for_resource, get_workers_for_capability, aggregate_catalog now include 'update-available' alongside 'online' - Worker-initiated drain emits notifications to activity feed Worker side (WorkerAgent): - report_update_available(), initiate_self_drain(), notify_drain_complete() - Convenience methods wrapping heartbeat() with appropriate status Tests: 11 new unit tests covering state transitions, routability, catalog inclusion, lease claims, monitor loop staleness, and the full online->update-available->draining pipeline. State machine: online -> update-available -> draining -> updating -> (restart) -> online * fix(cluster): rebase onto dev + address Kilo findings (1C/1W/2S) for worker drain Rebased feat/worker-self-drain onto origin/dev, resolving conflicts in manager.py, routes/cluster.py, and worker/agent.py from recently-landed registration-drift refresh fields (jaylfc#1538). Kilo fixes: - CRITICAL: Protect 'update-available' from status-less heartbeat reversion. The periodic ~15s heartbeat with no status was silently flipping update-available workers back to online, defeating the update signal. Now 'update-available' is protected alongside 'draining' — only explicit status transitions are honoured. - WARNING: Handle 'updating' in _monitor_loop. A worker stuck/crashed in 'updating' state was never re-onlined or offlined, permanently excluded from routing/catalog. Now they are force-offlined after HEARTBEAT_TIMEOUT. - SUGGESTION: Validate status against allow-list and sanitize drain_reason in notification block to prevent operator-facing UI injection. - SUGGESTION: notify_drain_complete() now sends status='updating' heartbeat so the controller transitions from 'draining' to 'updating'. Test update: test_heartbeat_without_status_reonlines_update_available renamed to test_heartbeat_without_status_preserves_update_available and now asserts the corrected protection behavior. 72/72 targeted tests pass. * fix(cluster): protect updating status from heartbeat reversion and cancel arbiter tasks on stale-update Add 'updating' to the protected status tuple in heartbeat so a periodic status-less heartbeat doesn't revert an updating worker to 'online', which would re-enter it into routing/catalog/lease-claims mid-update and prevent the monitor loop's stale-updating branch from firing. Mirror the stale-draining branch's GPU arbiter task cancellation in the stale-updating path so that in-flight arbiter work for force-released leases isn't orphaned when an updating worker goes stale. Fixes: PR jaylfc#1903 Kilo review findings (WARNING manager.py:239, SUGGESTION manager.py:835) * fix(cluster): hoist status allow-list to module frozenset + guard against invalid status heartbeat bypass Kilo found two issues post-rebase on jaylfc#1903: 1. The drain/update status allow-list was duplicated in three places (guard condition, assignment check, notification block). Hoist to module-level _VALID_STATUSES frozenset and reference in all spots. 2. An invalid status value via heartbeat ('online', 'offline', garbage) could revert a protected (draining/update-available/updating) worker back to 'online', defeating drain protection. Tighten the guard to require status in _VALID_STATUSES when the worker is protected. Closes jaylfc#1903. * fix(cluster): persist lifecycle status across controller restart, cancel arbiter on stale-update-available, gate online event - worker/agent.py: store lifecycle_status/lifecycle_reason in WorkerAgent, resend on periodic heartbeats so controller restart doesn't revert draining/updating workers to 'online' (CodeRabbit MAJOR #1) - manager.py: cancel GPU arbiter tasks on stale-update-available worker timeout, matching the cancellation already done for stale-drain and stale-update paths (CodeRabbit MAJOR #2) - manager.py: gate recovery 'worker.online' event on worker.status == 'online' to suppress false events for workers recovering into draining/updating state (CodeRabbit MINOR jaylfc#3) - tests/test_worker.py: accept **kwargs in fake_heartbeat mocks to match expanded heartbeat() signature --------- Co-authored-by: Hogne <[email protected]>
hognek
added a commit
that referenced
this pull request
Jul 18, 2026
) * docs(design): cross-user collaboration spec (contacts, human collaborators, agent delegation, human DM) Flagship epic: add another taOS user as a contact (hub friend edge), invite them as a human project collaborator, receive their delegated agents (normal external-selfjoin identities tagged with a sponsor contact), and chat human-to-human over an instance-to-instance peer channel. Rides the shipped invite/consent/registry rails; rejects registry-abuse and bus-federation. Phased plan with a pilot-first path (hogne) and lanes for lead/hognek/fleet. Awaiting owner decisions (section 10). * docs(design): add Milestone F - collaborator community view (stats, leaderboard, live kanban, community chat) When a user collaborates on a remote project, their Projects app gets a rich read-mostly community view (a mini social-GitHub for the shared project) so they see their contribution/involvement and feel part of the team. Introduces a scoped read-only project projection served over the peer channel (never direct API access, never sensitive fields) - the read counterpart to the write-delegation in Milestone D. Refines Decision 2: rich read surface, still zero board/file write for the human. Member-scoped in v1; public view a later toggle on the same machinery. * docs(design): address 1 Kilo + 5 CodeRabbit security findings on cross-user-collab spec (jaylfc#2024) CR #1 — Add replay protection, canonical serialization, audience binding, freshness window, and persistent replay cache to envelope handshake (line 124). CR #2 — Add peer-token vs. project-level authorization: contact identity authenticates the route family, but per-operation project membership, policy, and channel membership are independently enforced (line 131). CR jaylfc#3 — Add fail-closed runtime checks on every delegated request: verify current sponsor-contact status and project membership at call time, not just at token issuance; purge outbox and unclaim in-flight tasks on revoke (line 255). CR jaylfc#4 — Require non-null, owner-validated, immutable project_id claim in minted JWTs for project_tasks and canvas_read scopes (line 251). CR jaylfc#5 — Require atomic uniqueness constraint on (contact_id, channel_id, remote_msg_id) in chat_messages, insert-before-ack, treat conflicts as successful duplicates (line 328). Kilo #1 — Clarify project.visibility as a future schema addition (projects.visibility TEXT DEFAULT 'private'), not an existing column on hub posts (line 524). Co-authored-by: Hogne <[email protected]> --------- Co-authored-by: hognek <[email protected]> Co-authored-by: Hogne <[email protected]>
hognek
added a commit
that referenced
this pull request
Jul 19, 2026
…rate-limit LRU, prune commit, token docs Fix #1 (WARNING): Document outbound_token plaintext threat model in contacts_store schema comment — token must be presented on outbound requests; at-rest encryption deferred to post-MVP. Fix #2 (WARNING): Centralized /api/peer/ authentication via router-level _peer_auth_dep dependency. Previously EXEMPT_PREFIXES bypassed all auth middleware and correctness depended on every route calling _authenticate_peer. Now every route under /api/peer/ gets bearer-auth automatically; route handlers read contact_id from request.state. Fix jaylfc#3 (SUGGESTION): Commit the opportunistic nonce prune in its own transaction so a replay (IntegrityError) rollback does not undo it. Fix jaylfc#4 (SUGGESTION): Add record_nonce(…, kind='ack') to /api/peer/ack for replay protection. A replayed ack now returns 409 Conflict, matching the /inbox and /chat contract. Fix jaylfc#5 (SUGGESTION): Add LRU fallback eviction to the rate limiter. When all 2000+ entries have active windows (no expired entries to sweep), the oldest entry is evicted to prevent unbounded dict growth under sustained load.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix: taOS jaylfc#1538 — agent-registration drift (stale IP/status).
Problem: When a worker LXC container comes up on a different IP, the host_lan_ip/url/hardware drifts. Skald routes on these fields — stale values cause hangs or 5xx.
Fix — heartbeat-driven refresh/reconciliation: WorkerAgent.heartbeat() now includes host_lan_ip, url, hardware on every pulse. ClusterManager.heartbeat() updates cached WorkerInfo when these fields are present (backward-compat: None leaves stale values untouched).
Tests: 5 new in TestHeartbeatRegistrationRefresh — all 33 cluster+protocol unit tests pass, no regressions.
Also fixes: storage_cap/storage_used/bytes_deduped were accepted by HeartbeatBody but silently dropped by the handler.
Note
Medium Risk
Changes cluster routing metadata and deploy/proxy paths that depend on
worker.urlandhost_lan_ip; behavior is backward-compatible but incorrect refresh could misroute traffic.Overview
Fixes jaylfc#1538 by reconciling worker registration metadata on every heartbeat instead of only at initial register, so cluster routing (e.g. Skald) does not keep using stale
host_lan_ip,url, orhardwareafter DHCP moves or LXC restarts.Workers now send live
host_lan_ip(via_detect_lan_ip),url, andhardwareon each pulse, alongside existing backend/capacity fields. Controller extendsHeartbeatBodyandClusterManager.heartbeat()to apply those fields when present (Noneleaves cached values for legacy agents). The heartbeat route also forwards LXC storage counters (storage_cap_bytes,storage_used_bytes,bytes_deduped_total) that were accepted on the body but previously dropped. Whenbackendsis updated,available_modelsis flattened from sidecar-enriched backend catalogs intoWorkerInfo.Tests:
TestHeartbeatRegistrationRefreshcovers IP/URL/hardware refresh, no-op onNone, and unknown workers..gitignoreaddsvenv/.Reviewed by Cursor Bugbot for commit 3cce536. Bugbot is set up for automated code reviews on this repo. Configure here.