Skip to content

fix(cluster): heartbeat-driven registration refresh for host_lan_ip/url/hardware#2

Closed
hognek wants to merge 2010 commits into
masterfrom
fix/heartbeat-drift-refresh
Closed

fix(cluster): heartbeat-driven registration refresh for host_lan_ip/url/hardware#2
hognek wants to merge 2010 commits into
masterfrom
fix/heartbeat-drift-refresh

Conversation

@hognek

@hognek hognek commented Jul 6, 2026

Copy link
Copy Markdown
Owner

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.url and host_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, or hardware after DHCP moves or LXC restarts.

Workers now send live host_lan_ip (via _detect_lan_ip), url, and hardware on each pulse, alongside existing backend/capacity fields. Controller extends HeartbeatBody and ClusterManager.heartbeat() to apply those fields when present (None leaves 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. When backends is updated, available_models is flattened from sidecar-enriched backend catalogs into WorkerInfo.

Tests: TestHeartbeatRegistrationRefresh covers IP/URL/hardware refresh, no-op on None, and unknown workers. .gitignore adds venv/.

Reviewed by Cursor Bugbot for commit 3cce536. Bugbot is set up for automated code reviews on this repo. Configure here.

jaylfc and others added 30 commits July 4, 2026 21:31
…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".
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
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.
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).
jaylfc and others added 21 commits July 16, 2026 09:42
…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).
…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
hognek force-pushed the fix/heartbeat-drift-refresh branch from 3cce536 to cbd5f2a Compare July 17, 2026 16:26
- 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 hognek closed this Jul 17, 2026
@hognek
hognek deleted the fix/heartbeat-drift-refresh branch July 17, 2026 20:14
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants