perf/lazy-runtime-caps-routing
2681 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c5d6d06d94 |
perf(runtime): stop pulling the server router into every client import
`RuntimeCaps.routing_settings` defaulted to `field(default_factory=_default_routing_settings)`, and that factory imports `omnigent.server.smart_routing`. Because `runtime/_globals.py` builds a module-level `RuntimeCaps()`, the factory ran on every `import omnigent.runtime` — so any CLI client that touched the runtime paid for the server's routing stack (and, transitively, `model_fallbacks` -> `onboarding.provider_config` -> `spec.parser`) before doing any work. Default the field to `None` instead. Every read already goes through `smart_routing.routing_settings(caps)`, which substitutes `RoutingSettings()` for anything that is not a `RoutingSettings` — so an unconfigured deployment resolves the same defaults as before, just at first use instead of at import. Nothing reads the attribute directly. `import omnigent.runtime`: 0.23s -> 0.04s (-190ms, -83%), and `omnigent.server.smart_routing` no longer loads in client processes. Co-authored-by: Isaac <no-reply@databricks.com> |
||
|
|
b551f669d6 |
fix(openai-agents): wrap string assistant content for the chat converter (#4824)
Resuming a conversation whose history contained an assistant message with
plain-string content crashed before reaching the model:
TypeError: string indices must be integers, not 'str'
chatcmpl_converter.py:625 in items_to_messages
A string is legal Responses-API content, but items_to_messages iterates an
assistant message's content expecting blocks. Given a string it walks the text
character by character and indexes each character, so the very first one raises.
User strings are unaffected — they reach extract_text_content, which accepts
them, and callers depend on them staying strings.
Because history is replayed on every turn, one such item ends the conversation
permanently: each retry fails identically before the model is reached, and the
only escape is to abandon the conversation.
Only the assistant branch is normalized, and only when content is a string, so
block content and user strings pass through untouched.
Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com>
|
||
|
|
ddc5f6ee60 |
chore(k8s): Address ArgoCD overlay review follow-ups and PR #4744 comments (#4982)
* Address ArgoCD overlay review follow-ups (#4977) and PR #4744 comments Add CI validation of kustomize overlays, clarify the ignoreDifferences /data vs /stringData ArgoCD normalization, separate sync-completes from app-healthy in the Ingress wave comment, add TODO(v0.29) to the bare-Pod fallback in terminate(), and update the sandbox-runners README to reflect the bare-Pod → Job migration. Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix(ci): install kustomize via official script instead of third-party action The pinned SHA for imranismail/setup-kustomize was unresolvable. Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix(docs): correct backoffLimit value in sandbox-runners README The README stated `backoffLimit: 0` but the actual code uses `_JOB_BACKOFF_LIMIT = 6` — fix the doc to match. --------- Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> |
||
|
|
473941e322 |
fix(acp): resolve the tool identity on a bare permission request (#5050)
An ACP agent may ask permission carrying only a `toolCallId` — no `title`,
`kind`, or `rawInput`. Devin does. `_extract_tool_call` then resolved the name to
the literal string "tool" with empty arguments, so:
- the approval card asked the user to approve "Devin wants to use **tool**",
preview `tool({})`, with no command shown; and
- the TOOL_CALL policy was evaluated as `{"name": "tool", "arguments": {}}`,
which no builtin rule can match — rules gate on the tool name before reading
`arguments["command"]`, so a "deny `rm -rf`" policy sat silent.
The originating `tool_call` update carries the real name and command and always
arrives first, and the executor already caches `toolCallId -> name` there to
close the right tool card. Cache the `rawInput` beside it and fall back to both
when the request omits them; values the request does carry still win. The
correlation is the protocol's own id, so no vendor `_meta` key is read.
Both caches are released when the call closes, as the name cache already was.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
||
|
|
1f575ba8de |
fix(docker): honor configured execution timeout (#5016)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
8f63f3271b |
fix: preserve managed host logs on relaunch (#5042)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
9df23f7aeb |
chore(ucode): pin OSS ucode to a fixed commit (#5043)
Omnigent's uvx setup path resolved ucode from the mutable `main` branch, so setup could silently pick up a new ucode commit between runs and break unexpectedly. Pin `_UCODE_GIT_REF` to a fixed, known-good commit (94271a78c7139220b7333bcae91e522f95ef3af3) so setup is reproducible. A full SHA is immutable, so uvx caches the built wheel by ref and reuses it across runs; drop the `--refresh-package ucode` that existed only to defeat the mutable branch's stale cache. Co-authored-by: Isaac Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> |
||
|
|
c6d1f7d5e0 |
feat(web): resume imported / host-less sessions from the web (#4905)
An imported or otherwise unbound session (no host, no runner) couldn't run from
the web: it read as reachable (so the first message dropped against a runner
that can't start) or dead-ended on the terminal reconnect path.
The fix is mostly server-side liveness. An imported transcript is a
native-harness session that only runs in a runner on a host, never in-process,
so report it as runner_online=false via a new `imported` connectivity marker
(keyed on the omnigent.import.source label — the sibling of the existing fork
`needs_workspace` marker, computed in the same query). With that, the open view
routes to the EXISTING host picker (ResumeWithDirectoryDialog) instead of the
dead end. That picker — the same one forks and new-chat use — binds the session
to an online host + workspace (defaulting to the caller's current host) and
launches a runner via the existing POST /v1/hosts/{id}/runners path. No new
host-selection UI, no new launch route.
The picker is offered only when the resume will actually work
(unboundSessionResumableInApp): the caller must OWN the session (launch_runner
requires owner — a shared non-owner 404s), and for imports the harness must
reconstruct context from the omnigent transcript so it carries onto a chosen
host. Kimi has no resume path, and kiro/qwen resume only from a local recording
that lives on the original machine, so those route to the terminal reconnect
path instead of a picker that would start blank.
Also:
- Skip the cold-boot startup grace for imports so the picker shows at once.
- Generalize ResumeWithDirectoryDialog to prefill from the session's own fields
when there is no fork source.
- `omnigent import` prints the session's browser URL instead of the bare id.
Co-authored-by: Isaac
Signed-off-by: Mark Tai <mark.tai@databricks.com>
Co-authored-by: Mark Tai <mark.tai@databricks.com>
|
||
|
|
05eaa253d1 |
fix(host): retry Databricks auth refresh (#5014)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
699809de6e |
Show actual server target in host-daemon conflict error (#4821)
The "A host daemon is already running for this server" error suggested `omnigent host stop --server ...`, where the literal `...` hid the fact that `--server` needs an argument and left users guessing which value to pass. Build the hint via the existing `_host_stop_command` helper from the conflicting record, so the message prints a ready-to-run command: the real URL for a remote daemon, or `--server ""` (the empty-string alias) for a local daemon, matching the `host --background` hint. Co-authored-by: Isaac Signed-off-by: Evelyn Hur <122575337+evelyn-hur@users.noreply.github.com> Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com> |
||
|
|
4f05fbd0ac |
[OMNI-2359] [OMNI-2350] Show the task tracker inside the chat (#5036)
* Move tasks to chat box Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * Remove tasks from tab Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * padding Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * test(web): e2e test for the in-chat Plan tracker Drives the real chat store (mocked todos) through ChatPlanAccordion: collapsed by default, expands to the task list, tracks a live completion-count update, and self-hides when the list is cleared. Co-authored-by: Isaac Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * test(server): cover per-item todo validation filter; fix e2e-test lint Adds a pytest case proving _handle_external_session_todos drops malformed todo items (bad status / non-str content / non-str activeForm / non-dict) while keeping well-formed ones, on both the session.todos SSE channel and the cached snapshot — the one todos- pipeline path the existing tests didn't exercise. Also switch the ChatPlanAccordion e2e test's Todo `type` to an `interface` to satisfy oxlint (consistent-type-definitions). Co-authored-by: Isaac Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * test(e2e_ui): browser e2e for the in-chat Plan tracker Move the tracker's e2e coverage into the Playwright suite where it can exercise the real UI: tests/e2e_ui/chat/test_plan_tracker.py seeds the session.todos contract through the events route (the forwarders' path), then asserts the pinned Plan card seeds from the snapshot on load, stays collapsed by default, expands to the task list on click, tracks a live completion count, and disappears when the list clears. Mirrors test_mcp_startup_indicator.py's seed-then-republish pattern. Adds a data-testid="plan-tracker" hook to ChatPlanAccordion, and drops the jsdom vitest e2e (web/.../ChatPlanAccordion.e2e.test.tsx) it supersedes; the ChatPlanAccordion unit test stays. Co-authored-by: Isaac Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * docs(web): align Plan accordion max-height comment with code The comment said "Cap the expanded list at 100px" while the class is max-h-[150px]; sync the number (flagged by Polly review). Comment-only. Co-authored-by: Isaac Signed-off-by: Hubert Zub <hubert.zub@gmail.com> --------- Signed-off-by: Hubert Zub <hubert.zub@gmail.com> |
||
|
|
537620909c |
[OMNI-3751] Change document view mode toggle to dropdown (#5015)
Signed-off-by: Hubert Zub <hubert.zub@databricks.com> Co-authored-by: Hubert Zub <hubert.zub@databricks.com> |
||
|
|
ed9369474f |
fix(web): stop rendering single-dollar spans as LaTeX math (#5013)
A single $ is prose far more often than a math delimiter — currency, rates like $/PR and $/session, shell variables — and single-dollar math paired any two of them up, rendering everything in between as letter-by-letter math soup. Require $$ to open math and drop the currency/env-var escaping heuristics that tried to guess prose apart from math. Explicit TeX delimiters now normalize to $$ so \(x\) still renders as inline math. Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
1fceeeb754 |
Bump version to 0.11.0.dev0 (#4989)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
71a3437168 |
docs(changelog): record v0.10.0 (#4991)
* docs(changelog): record v0.10.0 * docs(changelog): fix truncated and malformed entries in v0.10.0 Complete 18 truncated entries, add proper [Bug fix / Test/CI] tags to #4508 and #4509 (which had bare `*` bullets), and drop the internal-only [Docs] N/A entry (#4925). Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> --------- Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com> Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com> |
||
|
|
86726e0ab5 |
fix(web): keep the Working shimmer lit while background tasks run (#4906)
* fix(web): keep the Working shimmer lit while background tasks run The background-tasks pill (#4893) introduced a shared `isBackgroundTasksOnly` predicate that gated all three busy surfaces off `bgCount > 0` alone, without checking whether the agent's turn was still active. So any live turn that coincided with a background task — notably `waiting`, where the parent is parked on its async-work drain of sub-agents / background shells — had its "Working…" shimmer suppressed and replaced by the pill, misreading an active turn as finished. Make the shimmer and the pill independent surfaces: - `isBackgroundTasksOnly` now also requires the turn to be inactive (`!agentWorking`), so the shimmer yields only once the turn has genuinely ended (`idle`) with tasks lingering. - `BackgroundTaskPill` shows on `bgCount > 0` alone, decoupled from the shimmer, so both appear together while the turn is active. - `workingIndicatorLabel` no longer emits the background count (the pill owns it); the shimmer just rotates its working messages or shows "Blocked on: …". Co-authored-by: Isaac * fix(web): keep the background-task pill lit while the turn works The pill vanished the moment the "Working…" shimmer appeared, so the two surfaces were still effectively mutually exclusive. The cause was the background-shell tally being zeroed on every new turn: the server's _publish_status popped the cache on a `running` edge, the client's session_status reducer zeroed it on `running`, and the optimistic send path cleared it synchronously. All three date from the single-surface design, where the count was a LABEL on the shimmer ("N background tasks still running") that a new turn should replace with "Working…". Now that the pill is a separate surface, background shells outlive turn boundaries and the tally must persist across the turn so the pill stays lit beside the shimmer. Stop clearing on `running` in all three places; keep clearing only on an authoritative Stop-hook `0` (shell finished) and on `failed` (a dead session may never post another count). The next Stop hook re-reports the count authoritatively. Also note the server normalizes a claude-native turn-end `waiting`+count to `idle` (see _background_task_delivery_status), so the client's real "working + shell" state is `running` with a preserved count — reflected in the reworked e2e coverage. Co-authored-by: Isaac * fix(web): remove the scroll-pinned Working tab The pinned "Working…" tab (shown while scrolled up) was designed to merge its flat bottom edge into the composer, but the background-task pill now sits between them — so the tab reads as a stray rounded card floating above the pill. Remove the sticky tab entirely (WorkingStatusPin); the inline shimmer at the end of the thread is the working cue. Move the tab's one non-visual job — the sole aria-live region announcing the working state — onto the inline WorkingIndicator: a stable "Working…" in a role=status region, with the rotating visible label kept aria-hidden so it never re-announces. Screen readers still get one announcement per turn. Co-authored-by: Isaac |
||
|
|
03c7907966 |
fix(host): keep capability probes out of tunnel handshake (#4769)
## Related issue Closes [OMNI-2964](https://linear.app/omnigent/issue/OMNI-2964/fix-host-tunnel-connection-issue-when-it-fails-to-detect-hanress) ## Summary - Move harness and gateway capability discovery out of reconnect handshakes, bound startup discovery, and degrade probe failures to visible warnings with unknown metadata. - Add a backward-compatible `host.connection_error` frame so accepted tunnels can surface server-side setup failures with their stage and retryability. - Make background startup wait for the existing server-side host status before reporting success and retain reconnect regression coverage. ELI5: checking which agent CLIs are installed is optional setup information. A broken CLI should not prevent the host from introducing itself to the server, so the host now connects with that information marked unknown and refreshes it later. ```text host startup ── capability probe ──┬─ success → cached metadata └─ failure/timeout → warning + unknown │ ▼ WebSocket upgrade → host.hello → connected receive loop ▲ server setup failure → host.connection_error ``` ## Test Plan - `uv run pytest tests/host/test_frames.py tests/server/integration/test_host_tunnel_route.py tests/host/test_connect.py tests/host/test_cli_host.py -q` - `uv run pytest tests/host/test_connect.py::test_silent_connect_streak_escalates_and_slows_reconnects tests/host/test_connect.py::test_inbound_frame_resets_silent_connect_streak -q` - `uv run ruff check` on all changed Python and test files. - `uv run pyrefly check omnigent/host/connect.py omnigent/host/frames.py omnigent/server/routes/host_tunnel.py omnigent/cli.py` ## Demo N/A — backend/CLI reliability change with no visual UI. ## Type of change - [x] Bug fix - [ ] Feature - [ ] UI / frontend change - [ ] Refactor / chore - [ ] Docs - [ ] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [x] Integration tests added / updated - [ ] E2E tests added / updated - [ ] Manual verification completed - [ ] Existing tests cover this change - [ ] Not applicable ## Coverage notes Automated coverage exercises capability exceptions and timeouts, server error propagation, background registration checks, retryability, and silent reconnect backoff. ## Changelog `omnigent host` now stays connected when optional harness detection fails and surfaces server-side tunnel setup errors. Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com> |
||
|
|
adcf83ccb6 |
feat(pi): Add searchable model picker for new sessions with Databricks Unity AI Gateway OAuth (#4961)
* feat(pi): add searchable start model picker Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com> * fix(pi): harden model picker compatibility Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com> * refactor(pi): simplify model picker filtering Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com> --------- Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com> Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> Co-authored-by: Anthony Ivan <anthony.ivan@databricks.com> |
||
|
|
70ee54bdba |
feat(cli): gate naked omni invocations behind a wrapper guard (#4766)
* feat(cli): gate naked omni invocations behind a wrapper guard Operators who front the CLI with a wrapper (e.g. `isaac omni`) can set OMNIGENT_REQUIRE_WRAPPER to refuse direct `omni`/`omnigent` calls. The wrapper sets OMNIGENT_WRAPPER_BYPASS around its own invocation to pass through, and OMNIGENT_WRAPPER_COMMAND names the command to suggest in the block message. The guard runs at the top of main() before any work, and is covered by unit tests on the message logic plus subprocess e2e tests for the block and bypass paths. Co-authored-by: Isaac Signed-off-by: Mark Tai <mark.tai@databricks.com> * style(cli): drop stray blank line left by the main merge Co-authored-by: Isaac Signed-off-by: Mark Tai <mark.tai@databricks.com> --------- Signed-off-by: Mark Tai <mark.tai@databricks.com> Co-authored-by: Mark Tai <mark.tai@databricks.com> |
||
|
|
036e0b9d29 |
fix(electron): trigger sign-in from "Run on this machine" instead of looping on "No hosts" (#4972)
Clicking "Run on this machine" looped back to a "No hosts" error whenever the desktop's stored Databricks OAuth grant had expired, forcing the user to run `omni` in a terminal to complete the browser sign-in. Root cause: serverAuthed() treated any Databricks pointer record as authed without checking token freshness, so ensureServerAuth skipped `omnigent login`. The spawned `omnigent host` (no TTY) then hit the non-interactive auth guard and exited pre-connect, and connectThisMachine returned silently — stranding the user on "No hosts". Fix (contained to the desktop shell + web UI; no shared CLI change): - ensureServerAuth now decides "auth needed?" with a GET /v1/me probe (probeServerAuth) — the same signal the CLI's own pre-flight trusts — instead of the stale on-disk token file. When not authed it runs the idempotent `omnigent login`, which silently refreshes a live grant with no browser and only opens the browser for a genuine re-auth. - Spawn `omnigent host --non-interactive` so any residual auth gap fails loudly with a classifiable authError rather than hanging on a missing TTY. (No Python change — the flag already exists.) - Surface the failure in the New Chat dialog with a "Try again" affordance instead of returning silently; auth failures get sign-in-flavored copy. Threads authError through the host-control IPC result and HostActionResult. - Raise the login timeout 180s -> 305s so a human completing the browser sign-in isn't SIGKILLed mid-flow (the CLI's own OIDC deadline governs). Tests: probeServerAuth (status/redirect/token branches), ensureServerAuth (loopback/authed/unreachable/login-success/login-failure), and the New Chat dialog's error surfacing + retry. Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> |
||
|
|
fb579783ce |
fix(web): make the Working… status pin opaque in dark mode (#4962)
The pinned "Working…/Tinkering…" tab (WorkingStatusPin) used `bg-card`, which in dark mode is a translucent glass surface: `--card` is rgba(31, 39, 45, 0.6) and the global `.dark .bg-card` rule adds a backdrop-blur. Over the transcript the tab read as a see-through frosted pill floating above the composer — most visible on mobile. Switch the tab to `bg-card-solid`, the opaque `--card` variant the composer itself uses in dark mode. This makes it opaque and, by not matching the `.dark .bg-card` glass rule, lets its `border-b-0` actually merge flush into the composer instead of the glass rule re-adding a bottom edge. Light mode is unchanged (`--card` and `--card-solid` are both #fff). Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> |
||
|
|
fabdc7d25b |
feat(deploy): add ArgoCD overlay for kubernetes sandbox provider (#4788)
* feat(deploy): add ArgoCD overlay for kubernetes sandbox provider Add a Kustomize overlay that layers sync-wave annotations onto the sandbox-runners overlay so ArgoCD deploys resources in dependency order (namespaces → RBAC → config → Deployment). Includes a sample Application CR and documentation for quick-start, out-of-band credential management, and multi-environment setups via ApplicationSet. Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix(deploy): address ArgoCD overlay review feedback - Remove over-engineered sync waves; ArgoCD's built-in kind ordering already sequences Namespace → SA → Role → ConfigMap → Deployment. Waves added health gates that caused PVC deadlock (WaitForFirstConsumer blocks until a consumer Pod is scheduled) and Ingress stall (no controller → Progressing forever). - Switch from 13 name-pinned strategic merge patches (which fail silently into wave 0 on a rename) to 3 kind-regex JSON patches (31 lines vs 151). - Add Prune=false on Namespaces and PVC to prevent accidental cascade on Application deletion or stale targetRevision. - Add ignoreDifferences for omnigent-secrets (selfHeal was reverting operator credentials to the checked-in placeholder) and PVC storage (API server mutations cause perpetual SyncFailed). - Fix syncOptions: remove inert CreateNamespace=true (destination.namespace is unset), correct RespectIgnoreDifferences comment to reference the actual ignoreDifferences block. - Restructure README quick start around fork-and-push (local edits have no effect when ArgoCD reads from Git), add namespace wait between Application apply and Secret creation, document auth prerequisite (accounts provider 403s on managed runner dial-back), fix "delete Ingress" advice to use $patch: delete instead of removing base/ingress.yaml (which breaks all overlays), fix postgres composition advice (direct resource causes duplicate-base error), document deletion cascade and selfHeal behavior. Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> --------- Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> |
||
|
|
ddfa872809 |
OMNI-3743: Add session name and project information to chat title bar, fix sizing (#4940)
* OMNI-3743: Add session name and project information to chat title bar Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * Improvement Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * Another fix Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * Native app fixes Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * test(e2e-ui): regenerate visual baselines * test fixes Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * Post-review fixes Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * restore native back Signed-off-by: Hubert Zub <hubert.zub@gmail.com> --------- Signed-off-by: Hubert Zub <hubert.zub@gmail.com> Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com> |
||
|
|
21f71ddfdd |
fix(runner): retry tunnel 401/403 on an already-connected runner (#4957)
#3943 replaced the unconditionally-fatal 403 with a retry streak, which is strictly better than exiting on the first rejection but has no ever_connected condition — so a runner that already completed an upgrade still dies once three rejections land consecutively. Because delay_s is reset to the base delay on every rejection, those three attempts land within a few seconds, so a brief connectivity blip is enough to exhaust the streak: healthy tunnel to dead process in 8 seconds. Dropping off a VPN reproduces it — an intermediary answers the WS upgrade with 403 before the request reaches the server. The same runner survives or dies depending purely on whether the token refresh wins the race against the streak, and the error text tells the user to re-authenticate when the credentials were valid the whole time. The exit takes down every conversation on the runner, not just the active one, and being ungraceful it leaks detached terminal tmux servers until a later runner's reap_orphaned_terminals() sweep. The host tunnel already got this treatment in #4025: a tunnel that completed an upgrade proved its credentials, so a later 401/403 is a network-path artifact and retries indefinitely rather than forcing a manual restart. The runner path was one surface behind; this applies the same posture: - The fatal streak now applies only before the first successful upgrade. A never-connected runner still fails loud after three rejections, so a genuinely-forbidden runner does not busy-reconnect forever. - An already-connected runner keeps the escalating backoff instead of resetting to the base delay, so a sustained outage retries at the 10 s cap rather than hammering the rejecting proxy every ~0.5 s. - The retry logs at WARNING and names VPN/network as the likely cause, so a genuinely revoked credential is not silent to an operator. Token invalidation still runs on every rejection, so a plain mid-session expiry recovers on the next attempt as before. Continues #3516, which identified this fix before #3943 landed and went stale against it. That PR's ever_connected guard is reapplied here on top of #3943's streak structure, and its host-bootstrap-bearer test is carried over; the rest of its diff was superseded upstream. Tests: an already-connected runner survives a rejection streak well past the fatal bound and escalates 0.5→10 s; a 403-rejected host bootstrap bearer is swapped for the runner's own refreshable token. The existing never-connected fatal tests are unchanged and still pass. Co-authored-by: Isaac Signed-off-by: Anton Nekipelov <226657+anton-107@users.noreply.github.com> Co-authored-by: Bryan Qiu <bryan.qiu@databricks.com> |
||
|
|
1fc4b283b8 |
test(host): close the cancel race in the midspawn leak test (#4971)
test_launch_cancelled_midspawn_does_not_leak_untracked_runner signals spawn_started after Popen returns, then cancels the launch task. On a loaded machine the event loop is descheduled in that gap, _handle_launch runs to completion, and the cancel arrives after the window it is meant to exercise, so the test fails with "DID NOT RAISE CancelledError" instead of catching a leak. Hold the spawn thread inside the shielded call until the test has issued its cancel, so the cancel lands in the leak window regardless of scheduling. The assertions are unchanged, and the test still exercises the real post-spawn/pre-register window it was written for. Reproduced by inserting a 0.2s sleep between the spawn signal and the cancel, which fails identically to CI; with this change the same insertion passes. Signed-off-by: Dhruv Gupta <dhruv0811@gmail.com> Co-authored-by: Isaac <no-reply@databricks.com> |
||
|
|
83b7ff409f |
fix(web): back off silent sticky-apply PATCHes when the backend errors (#4777)
* fix(web): back off silent sticky-apply PATCHes when the backend errors The sticky model/effort applies in bindStream and refetchRunnerBackedSessionState fire on every bind/switch while the session's server-side override is still null. When the backend is erroring the PATCH never persists, so the null-override guard never closes and the applies re-fire on every rebind. During an outage that becomes a self-sustaining PATCH storm with no backpressure: the failures are swallowed (fire-and-forget .catch), so nothing slows down. Add a failure-scoped, auto-clearing client backoff. A backend-unhealthy failure (5xx / network / timeout) pauses the silent applies for a cooldown; a 404 parks that gone session; the next success clears the cooldown so stickiness resumes the moment the backend recovers. A successful send-path bind also clears it, and it feeds a failing bind into the same backoff. Normal operation is unchanged — the PATCH succeeds on the first try and nothing ever arms. Refs OMNI-2513. Co-authored-by: Isaac Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> * fix(web): let a 404-parked sticky-apply recover on the next successful bind The silent sticky-apply parks a session on a 404 so its failure doesn't pause the others. But nothing lifted that park except a page reload: the sticky applies that would clear it are themselves gated by the park, so a parked session could never re-apply. A sticky PATCH only runs after a successful snapshot GET, so a 404 there is a transient mid-bind race rather than a durable "gone". Lift the park when bindStream's snapshot GET next succeeds (proof the session exists); if it is genuinely gone that GET 404s and bindStream bails before any PATCH, so there is no storm either way. Also treat 410 Gone like 404. Refs OMNI-2513. Co-authored-by: Isaac Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> * fix(web): treat a sticky-apply 404 as a transient backend failure A 404 on the silent sticky-apply PATCH does not mean the session is gone: here it means the permission check didn't succeed (a flaky permission service), which is backend-wide and transient — the same root cause as the 5xx errors seen in the same outage. So a 404 must pause every session's applies via the global cooldown, exactly like a 5xx, rather than parking the one session that happened to 404. Collapse the per-session gone-set into the single global cooldown: every failure (4xx incl. 404, 5xx, network) arms it; the next success clears it. This removes the recovery machinery the per-session park needed — the cooldown is inherently self-clearing — and suppresses more of the storm during a real outage (the first failure pauses all sessions instead of letting each fire once before parking). Refs OMNI-2513. Co-authored-by: Isaac Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> * fix(web): reopen the sticky-apply cooldown by time, not on a success During the outage ~90% of requests failed, so ~10% still succeeded. With the cooldown clearing on any success, each of those lucky successes would reopen the gate and let the next (still-likely-failing) sticky apply fire — a flap that leaks a fresh apply on every success rather than holding. Arm the cooldown on failure only and reopen it purely by elapsed time; a success no longer clears it, so the successful fraction mid-outage can't flap the gate. This also drops the send-path from the cooldown entirely (it fails loudly on its own) and removes the success bookkeeping. Recovery is the window elapsing (≤30s), which is fine for a cosmetic sticky apply. Refs OMNI-2513. Co-authored-by: Isaac Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> * fix(web): keep the /model readout honest while sticky-apply is cooling down The sticky-model apply is skipped during the cooldown, but the readout still computed effectiveSessionOverride from the sticky model, so the /model picker briefly claimed an override the server never persisted — the inverse of the honesty this change is about. Fold the cooldown check into willApplyStickyModel so the readout and the PATCH decision share one condition: while blocked, we neither apply nor claim the override, and effectiveSessionOverride stays null to match the un-persisted server truth. Refs OMNI-2513. Co-authored-by: Isaac Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> --------- Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> |
||
|
|
a447db22cb |
feat(k8s): replace bare Pods with Jobs for automatic failover (#4744)
* feat(k8s): replace bare Pods with Jobs for automatic failover The Kubernetes sandbox launcher previously created bare Pods with restartPolicy: Never. A crashed host container was a dead end until a human retried. This change wraps the Pod template in a batch/v1 Job with restartPolicy: OnFailure and a configurable backoffLimit (default 3), so the kubelet automatically restarts a crashed host container with exponential backoff — providing automatic failover without a custom scheduler or work queue. Key changes: - build_pod_manifest() → build_job_manifest(): wraps the Pod spec in a Job with backoffLimit, activeDeadlineSeconds, and a liveness probe (pgrep -f "omnigent host") to detect stuck processes. - KubernetesSandboxLauncher now uses BatchV1Api alongside CoreV1Api. - start_host() creates a Job; _wait_for_pod_running() discovers the Job's child Pod via the job-name label selector. - terminate() deletes the Job with propagationPolicy: Foreground, cascading to its child Pods. - RBAC Role updated: added batch/v1 Jobs (create/get/delete), changed Pods from create/get/delete to list/get (Pod lifecycle is now managed by the Job controller). The host's existing WebSocket reconnect logic re-registers the tunnel automatically after a container restart, and the runner's durable conversation checkpointing recovers incomplete turns on session re-init. Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix: ruff format + unused variable lint Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix(k8s): address reviewer feedback on Job migration - RBAC: retain pods create/delete for one-release upgrade overlap window - Drop ineffective liveness probe (pgrep matches reaper's own argv) - Add bare-Pod delete fallback in terminate/best-effort for pre-migration sandboxes (Job 404 → try deleting the old bare Pod) - Restore dropped inline comments explaining security decisions Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix(k8s): address reviewer blocking feedback on Job migration 1. **Stale Pod references**: update module docstring, `_new_pod_name`, `provision`, role.yaml header to reflect Job model. Rename `_POD_DELETE_*` → `_DELETE_*`. Add version to TODO(v0.29). 2. **`_terminal_failure` reworked for OnFailure**: init container non-zero exit is no longer terminal unless Pod phase is `Failed` (backoffLimit exhausted). CrashLoopBackOff on the host container is detected even though the Pod stays in phase `Running`. `_wait_for_pod_running` now checks `_terminal_failure` BEFORE accepting `Running`. 3. **terminate no longer leaks Secrets**: each delete is independently try/caught so a 403 on Job delete still cleans up the Secret. The first error is re-raised after all deletes run. 4. **Child-Pod discovery hardened**: `_find_job_pod` re-raises 401/403 (surfaces RBAC immediately), filters out Pods with deletionTimestamp, prefers Running phase. `_wait_for_pod_running` re-discovers on 404 instead of treating it as terminal (supports Pod replacement under eviction/drain). 403 hint updated to include `jobs`. 5. **backoffLimit raised to 6**: comment clarifies it is a lifetime budget shared with init containers; 6 leaves headroom for init retries while still surfacing persistent crashes. 68 tests (62 updated + 6 new). Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> --------- Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> |
||
|
|
ce00d35f0d |
fix(harnesses): let a configured acp: agent win over a same-slug builtin row (#4927)
A configured agent named "Devin" slugifies onto `devin`, which is also an
`ACP_CLI_HARNESSES` row id, so both sources describe the same harness by the
same name. They failed in opposite directions:
- the web picker showed one row, silently the builtin — both seed the same
`builtin_agent_id`, and the row seeded second overwrote the user's entry,
dropping the `--model` their command carried;
- `omni setup` showed two identically labeled "Devin" rows, one per source.
The configured agent wins in both: it names the exact command, which a row's
fixed argv cannot express. `shadowed_builtin_acp_rows` states the rule once and
both surfaces read it, matching row ids only — an alias-shaped name ("Grok
Build" -> `grok-build`) is a separate harness id and does not shadow `grok`.
Listing only. `--harness devin` and `harness: devin` specs still resolve to the
row, and removing the config entry brings the row straight back.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
||
|
|
8aaf72c91a | feat(web): restyle chat error banner as a centered pill (#4931) | ||
|
|
65021dc1e8 |
fix(databricks): resolve harness launch models from the workspace (#4915)
* fix(databricks): resolve harness launch models from the workspace The Databricks AI Gateway has retired the legacy `databricks-*` model namespace (`501 NOT_IMPLEMENTED ... Use Unity Catalog model services (v3)`). Several managed harnesses take their launch model from the bundled MLflow provider catalog, whose Databricks ids carry exactly that retired spelling, so every gateway turn fails. `claude-native` was migrated to live Unity Catalog discovery in July; its siblings were left behind. - codex-native: `_resolve_databricks_codex_model` resolves through the live UC model-services listing (ids are `system.ai.` by construction), then ucode's cached copy, then the bundled catalog as a documented last resort. An explicit legacy `model_override` is matched against the servable ids on the bare id, so it recovers instead of failing forever; a model the workspace does not serve passes through untouched. - claude-sdk (Polly, Debby): resolve the launch model from the live listing using the family precedence claude-native itself falls back to. And on a real Databricks AI Gateway, negotiate betas (`CLAUDE_CODE_USE_GATEWAY`) instead of setting `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS`, which made Claude Code strip `interleaved-thinking` and the gateway reject the blocks with `400 ... Expected 'thinking'`. Unset an inherited disable flag around the spawn, scoped to gateway launches; a non-Databricks/mock gateway keeps the original workaround. - pi-native: resolve the launch model from the live listing. - model_catalog.fetch_databricks_model_service_entries: scope the UC listing to `schemas/system.ai` and paginate. Unscoped and unpaged it walked the whole metastore and returned one page of whatever schemas sorted first, so a workspace serving 53 models reported 2 and zero Claude entries. A repeated page token returns the pages collected so far (a partial `system.ai` list still launches) rather than raising, since callers treat an exception as "no listing" and fall back to the retired `databricks-` catalog. Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> * test(databricks): keep codex build test offline build_codex_native_server now resolves the launch model through live Unity Catalog discovery, so a build with a profile makes a real model-services call. test_build_codex_native_server_uses_profile_host_without_static_token passed only on a machine with ambient Databricks credentials and crashed the CI worker on the network call. Stub discovery offline; the test asserts the profile-host base URL + auth command, not model resolution. Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> --------- Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> |
||
|
|
6d277c4fc0 |
docs(acp): correct the env-var claim on builtin ACP CLI rows (#4925)
The Devin row's comment and auth hint both implied an environment variable can
configure or authenticate the agent (`DEVIN_MODEL`, "or set a Devin API key").
It cannot: the generic ACP spawn env is deny-by-default with no allowed prefixes,
and a catalog row has no `env_passthrough` of its own — only a user-configured
`acp:<slug>` agent can declare one. Verified against the real builder:
builtin row -> DEVIN_MODEL forwarded: False
acp: agent declaring it -> DEVIN_MODEL forwarded: True
Devin is unaffected in practice because `devin auth login` writes a credential
file it reads back at spawn, so state the file-based path instead and point a
per-model setup at an `acp:<slug>` agent carrying `--model`.
Also record the constraint once in the module docstring, since it decides whether
a future vendor can be a row at all: env-var-only vendors need a user-configured
agent, disk-credential vendors work as rows.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
||
|
|
5de71a1877 |
feat(acp): Devin as a builtin harness + catalog-derived picker identity (#4920)
* feat(harness): add Devin as a builtin ACP CLI harness Devin (Cognition's `devin` CLI) speaks ACP on stdio via `devin acp`, so it is one catalog row — like Grok Build. This makes Devin a first-class harness: it shows in `omni setup` (own auth, `devin auth login`), launches via `--harness devin`, and — with this PR's picker seeding — seeds into the web New Chat picker once the `devin` binary is on PATH, with no user `acp:` config needed. It runs Devin's account-default model; set DEVIN_MODEL to pin one. The setup overview now has two builtin ACP CLI rows (Devin, then Grok Build, sorted by id), shifting the numbered rows below; the scripted-stdin ordering / dispatch / openclaw tests are updated. Per-row catalog wiring is auto-covered by the parametrized tests in test_acp_cli_harnesses.py. Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> * fix(web): group the builtin `devin` harness under Harnesses, not Agents This PR adds `devin` to the backend ACP CLI catalog, so a seeded Devin agent carries `harness: "devin"` (a bare builtin id, not `acp:devin`). The picker's harness/agent split calls isAcpHarnessAgent, which matches `acp:*` or an id in ACP_CLI_HARNESS_IDS — a frontend mirror of ACP_CLI_HARNESSES that still listed only `grok`. So the builtin Devin fell into the "Agents" group instead of "Harnesses ▸ More". Add `devin` to ACP_CLI_HARNESS_IDS so it groups with the harnesses, beside Grok / OpenCode / Cursor, and extend the test. Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> * feat(web): derive ACP harness identity from the server catalog, not a frontend list Adding a builtin ACP harness took a frontend edit: the picker recognized ACP agents via a hardcoded id set mirroring ACP_CLI_HARNESSES, and rendered their name by capitalizing the agent slug. So a new row landed under "Agents" instead of "Harnesses" until someone remembered the mirror, and even a known row showed the wrong name — Grok Build as "Grok", a user's "My Devin Agent" as "My-devin-agent". Both facts already exist server-side and the frontend already fetches them: the harness catalog reports `capabilities.integration_mode == "acp-subprocess"` for builtin ACP rows AND user-configured `acp:<slug>` agents, plus a `label` (the vendor's for a builtin, the user's own for a configured agent). The catalog fetch just dropped both. Read them: useAvailableAgents stamps `acpHarness` and the catalog label onto each agent, isAcpHarnessAgent prefers that flag, and the id set stays only as a fallback for servers that don't report capabilities. A new builtin ACP harness is now one row in acp_cli_harnesses.py — the picker groups and names it with no frontend change, which is what this PR's Devin row should have needed. The catalog read is gated on the picker's own `enabled` so a disabled picker still issues no request, and the label is applied only to ACP-family harnesses, so a composed agent keeps its own name (Polly stays "Polly", not "Claude SDK"). Co-authored-by: Isaac Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> --------- Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com> |
||
|
|
fcf5a902af |
feat(server): seed configured ACP agents into the New Chat picker (#4909)
* feat(server): seed configured ACP agents into the New Chat picker
The web New Chat picker lists AGENTS from GET /v1/agents, and native
harnesses appear only because _ensure_default_native_agents seeds a
<harness>-ui agent for each. Nothing seeded ACP agents, so a configured
acp:<slug> agent (Devin, ...) or an installed builtin ACP CLI harness
(grok) never showed in the picker on its own — the ACP sibling of the
`omni setup` discovery gap.
Seed a picker built-in per ACP harness set up on the server's host: one
per user-configured acp:<slug> agent (in config == set up, matching
harness_is_configured), and one per builtin ACP CLI harness whose binary
is on PATH. On a host with no ACP setup (the common remote-server case)
this seeds nothing.
Two things the naive version got wrong, fixed here:
- Name, not label. Agent names must be [a-zA-Z0-9_-]+, so a display label
like "Grok Build" / "Gemini CLI" fails spec validation at load ("agent
name ... must match ..."). Seed by the slug (agent.slug / the catalog
id); the web picker capitalizes it for display (devin -> "Devin").
- Grouping. GET /v1/agents already returns a `builtin` flag
(session-scope-NULL + deterministic id), but partitionAgentsByKind
grouped by a hardcoded name allowlist, so dynamically-seeded ACP agents
fell under "Custom agents". Group by the `builtin` flag, falling back to
the allowlist only for older servers — so seeded ACP agents sit with the
harnesses.
Purely additive: only adds picker rows, never touches native seeding; a
malformed acp: block is logged and skipped, never fatal to startup.
Verified against a real machine config (Devin + kilocode + grok all seed)
and with the web unit test for partitionAgentsByKind.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(web): group generic-ACP harness agents under "Harnesses", not "Agents"
The New Chat picker builds its "Harnesses" section from
agentList.filter(isNativeCodingAgent), so the ACP agents this PR seeds (Grok,
and configured acp:<slug> agents like Devin / Kilocode) fell through to the
"Agents" group beside Polly / Debby instead of sitting with the native CLIs.
Add isAcpHarnessAgent (harness `acp:*`, or a builtin ACP CLI id like `grok`)
and widen the picker's harness/agent split to include it, so these
harness-backed picks fold into "Harnesses > More" next to OpenCode / Cursor.
Grouping-only: selection is unchanged (both sections render through the same
renderEntry, whose onSelect launches by agent id), and ACP entries show no
readiness badge (they are not not-ready host entries). Composed built-ins
(Polly / Debby) still stay under "Agents".
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
||
|
|
ba3692130d |
[OMNI-2843 fix] Fix overlaying toolbar icons (#4897)
Signed-off-by: Hubert Zub <hubert.zub@gmail.com> |
||
|
|
57df36a1ee |
feat(web): show background tasks as a composer pill, not the working shimmer (#4893)
* feat(web): show background tasks as a composer pill, not the working shimmer Once a turn ends but background shells/sub-agents outlive it, the "Working…" shimmer misreads as the agent still thinking. Route that state to a dedicated BackgroundTaskPill above the composer instead: a shared isBackgroundTasksOnly predicate gates both shimmer surfaces off and the pill on. A parked dialog (blockedOn) still wins the shimmer, since it needs an action. Co-authored-by: Isaac Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * e2e tests Signed-off-by: Hubert Zub <hubert.zub@gmail.com> --------- Signed-off-by: Hubert Zub <hubert.zub@gmail.com> |
||
|
|
fc0e2e99c6 |
perf: eliminate redundant DB queries in session create and host wake paths (#4809)
- Replace 4x set_labels + get_conversation pairs in _create_session_from_existing_agent with in-memory conv.labels.update() — saves 4 round-trips per session creation - _record_create_route_prompt: apply label in-memory instead of refetching the row - _stamp_routing_decision_label caller: apply ROUTING_DECISION_LABEL_KEY in-memory - _maybe_relaunch_managed_sandbox: replace host_store.is_online() (which calls get_host internally) with host_is_live(host) using the already-fetched host object - _maybe_wake_stale_resumable_managed_sandbox: same host_is_live fix - Update test_concurrent_relaunch_messages_kick_a_single_launch to give its dead_host SimpleNamespace the status/updated_at fields that host_is_live reads Each query is slower on managed infra, so removing these redundant reads reduces per-request latency on the hot session-creation and message-dispatch paths. Closes OMNI-3243 Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> |
||
|
|
c2439c6fd7 |
fix(windows): pass Windows process essentials through harness env filters (#4886)
On native Windows, `agent_env.BASE_ALLOW_EXACT` (the shared deny-by-default env filter used by all harness executors) did not include SYSTEMROOT, COMSPEC, USERPROFILE, or the other Windows-mandatory constants. Any harness CLI spawned via `clean_agent_env` (codex, pi, claude-sdk, antigravity, …) died instantly on spawn because Winsock/crypto cannot initialise without SYSTEMROOT — the subprocess exited before reading stdin, causing the executor to await a JSON-RPC response that never arrived and silently idle to the 600s watchdog. The constant set already existed as `WINDOWS_ENV_PASSTHROUGH` in `_platform.py` and was already wired into `os_env._DEFAULT_ENV_PASSTHROUGH` and `connect._RUNNER_ENV_ALLOWLIST`. This commit adds it to `BASE_ALLOW_EXACT` so every harness executor inherits it automatically, matching the pattern used elsewhere. Also fixes three related Windows issues surfaced in omnigent-ai/omnigent#4851: - `PYTHONUTF8` was not forwarded through `_RUNNER_ENV_ALLOWLIST`, so the host daemon / runner subprocess printed Unicode status chars (✓ ↑) on the Windows ANSI code page (cp1252), raising `UnicodeEncodeError` and killing the host tunnel in an infinite reconnect loop. - `_session_create_validation.validate_existing_host_workspace` and `_workspace_validation.validate_workspace` required `workspace.startswith("/")`, rejecting every Windows drive-letter path (C:\…) from a connected Windows host. Windows absolute paths matching `^[A-Za-z]:[/\\]` are now accepted. - `harness_install._harness_cli_version_satisfies` returned `False` on `packaging.version.InvalidVersion`, so pre-release versions like `0.146.0-alpha.9.2` (newer than the declared floor) were reported as too-old and the harness was refused at the version gate. The fix extracts the leading X.Y.Z segment as a fallback for non-PEP-440 strings. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> |
||
|
|
901aa8d12a | feat(web): white-label UI branding via config.yaml (#2857) | ||
|
|
2aba5079d4 |
feat(bench): add CLI startup latency benchmark (#4793)
* feat(bench): add CLI startup latency benchmark Measures wall-clock time from omnigent claude --server invocation to the Claude terminal being ready (signalled by 'Claude terminal ready.' spinner message, emitted just before tmux attach). Unlike the HTTP/API benchmarks in run.py, this drives the real CLI binary end-to-end against a remote server — auth, daemon tunnel, session create, runner launch, terminal boot — via pexpect. Usage: uv run --no-sync dev/benchmarks/omnigent/cli_startup.py uv run --no-sync dev/benchmarks/omnigent/cli_startup.py --also-isaac-omni --runs 10 uv run --no-sync dev/benchmarks/omnigent/cli_startup.py --output startup.json uv run --no-sync dev/benchmarks/omnigent/cli_startup.py --max-p50-ms 12000 JSON output is compatible with the existing benchmark schema. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * ci(bench): add cli-startup job to benchmark workflow Adds a new 'CLI startup latency' job that runs cli_startup.py against the ai-devtools managed workspace (OMNIGENT_REMOTE_AUTH_TOKEN secret). - Runs on nightly schedule (when secret is configured) and on workflow_dispatch with cli_startup_runs input (default 5, 0 = skip) - Skips gracefully when OMNIGENT_REMOTE_AUTH_TOKEN secret is absent - Uploads benchmark-results-cli-startup-{run_id}.json as an artifact for the Databricks trend dashboard (same schema as the HTTP benchmarks) - Renders a job summary table via report_markdown.py Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * feat(bench): align cli_startup with existing journey schema - Use RunResult/aggregate/print_results/check_thresholds/build_report from the existing framework instead of custom stats/output code - Each run is now a RunResult with all latency samples (matching the HTTP/API journey shape), not one run-per-sample - Journey names are cli_startup and isaac_omni (snake_case, no spaces) - Output table uses the same renderer as run.py - Add cli_startup_runs dispatch input and cli-startup job to benchmark.yml Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * refactor(bench): move cli_startup into journeys.py; use local bench server The cli_startup journey now lives in journeys.py alongside the other journeys, using env.base_url (the local bench server) instead of a remote Databricks URL. This aligns it with the existing pattern: needs_host=True boots the host daemon, and omnigent claude --server <local-url> connects to it for the full startup sequence. cli_startup.py becomes a thin shim that calls run.py --journeys cli_startup. benchmark.yml cli-startup job now uses run.py directly — no OMNIGENT_REMOTE_AUTH_TOKEN secret needed, just pexpect + claude CLI. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * ci(bench): fold claude CLI install into Install dependencies step Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * ci(bench): merge cli_startup into existing benchmark job (sqlite leg only) Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * remove cli_startup.py shim — use run.py --journeys cli_startup directly Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * ci(bench): run cli_startup on all matrix backends Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * ci(bench): install pexpect+claude before Run benchmark so cli_startup does not skip Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): fix policy_evaluate setup (POST /v1/agents → /v1/sessions bundle); add needs_runner to cli_startup - policy_evaluate setup was calling POST /v1/agents which is GET-only. Fix: use POST /v1/sessions multipart bundle upload (same as ensure_agent), with executor fields added to pass spec validation, and read session_id from the correct response key. - cli_startup: add needs_runner=True so the test_runner_journeys_are_capped invariant passes (needs_host implies needs_runner in BenchEnvironment but not on the Journey dataclass itself). Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * ci(bench): add pexpect+claude install to benchmark-pr.yml cli_startup is in ALL_JOURNEYS so it runs in the benchmark-pr regression check too. Without pexpect and claude installed, every iteration fails with RuntimeError. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): replace test fixture function ref in policy_evaluate with self-contained one tests.runtime.policies.conftest._always_allow is a test fixture that may not be importable in the server subprocess's PYTHONPATH in CI, causing HTTP 500 on every evaluate call. Replace with _bench_policy_allow defined directly in journeys.py, which is always importable since dev/ is on PYTHONPATH in the benchmark environment. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): gate cli_startup on OMNIGENT_BENCH_SERVER; skip gracefully when not set cli_startup conflicts with the bench environment's host daemon when run against the local bench server — omnigent claude spawns its own daemon which hits a 'host on another replica' error. Gate on OMNIGENT_BENCH_SERVER env var instead: skip with a clear RuntimeError when unset, use the remote server when set. - Remove needs_runner/needs_host (no local server contact) - Reduce max_iterations from 5 to 3 (each is ~10s) - Set OMNIGENT_BENCH_SERVER in benchmark.yml and benchmark-pr.yml - Relax test_runner_journeys_are_capped to allow non-runner journeys to cap Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * ci(bench): remove hardcoded OMNIGENT_BENCH_SERVER from workflows cli_startup skips gracefully in CI (no OMNIGENT_BENCH_SERVER set). Run it manually: OMNIGENT_BENCH_SERVER=<url> uv run --no-sync dev/benchmarks/omnigent/run.py --journeys cli_startup Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): run cli_startup against local bench server; drop OMNIGENT_BENCH_SERVER The daemon conflict was caused by needs_host=True booting a bench daemon alongside the CLI's own daemon. With needs_host=False the bench environment starts only the server; omnigent claude spawns its own daemon freely — no conflict. Result: 5.3s local vs 11s remote. CI runs it as part of the default suite with no remote credentials needed. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): use omnigent polly instead of omnigent claude for cli_startup claude-native requires the external claude CLI binary which: - Takes too long to boot on CI (90s timeout → job gets stuck) - Requires npm install of @anthropic-ai/claude-code polly (omnigent run with the bundled openai-agents harness) exercises the same startup path (daemon, session create, runner launch, runner connect) without any external binary dependency. Signal: 'Launching your agent' with a 30s timeout instead of 90s. Remove @anthropic-ai/claude-code install from both benchmark workflows. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): move cli_startup to OPT_IN_JOURNEYS; exclude from default run cli_startup against the local bench server hangs in CI — the polly runner can't complete its startup within 30s, burning 19 min (39 attempts × 30s including warmup) before failing. Move it to OPT_IN_JOURNEYS: excluded from the default set, must be run explicitly via --journeys cli_startup. resolve_journeys() looks in both registries so it still works when named. Remove pexpect install from CI workflows since it's no longer needed for the default benchmark run. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): cli_startup back in ALL_JOURNEYS; add skip_warmup flag; 60s timeout - Move cli_startup back to ALL_JOURNEYS (not needs_host; spawns its own daemon) - Add Journey.skip_warmup: when True, run_latency skips the warmup phase regardless of --warmup. Avoids 10x60s = 10min of wasted warmup hangs. - Increase timeout from 30s to 60s (CI runner is slower than local Mac) - Restore pexpect install in both benchmark workflows With skip_warmup=True and max_iterations=3: 3 runs x 3 = 9 iterations max, no warmup hangs. Worst case: 9 x 60s = 9min if all timeout (shouldn't happen). Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * debug(bench): include RuntimeError message in failure breakdown for CI visibility Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): stop stale daemons before each cli_startup iteration A leftover host daemon from the previous iteration causes the next omnigent polly to fail with 'runner tunnel rejection' or 'host is on another replica'. Run omnigent stop before spawning polly to ensure a clean slate each time. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(bench): move omnigent stop to prepare hook so it's outside the latency timer Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> --------- Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> |
||
|
|
dc10a22147 | OMNI-3247: Update in-chat error patterns (#4787) | ||
|
|
08b20956f2 |
fix(cli): honor isolated Omnigent state directories (#4822)
## Related issue [OMNI-3489](https://linear.app/omnigent/issue/OMNI-3489/honor-omnidev-state-and-config-directories-in-omnigent-cli-paths) ## Summary - Prevent `omnidev omnigent …` commands from leaking auth tokens, session logs, host daemon records, and native harness launch state into the developer's real `~/.omnigent` directory. - Make runtime state honor `OMNIGENT_DATA_DIR` while configuration independently honors `OMNIGENT_CONFIG_HOME`; harness-specific native-state overrides still take precedence. - Keep the real `HOME` and `XDG_*` environment intact so harness credentials and caches remain available, and update REPL E2E setup to seed its theme in the effective config without clobbering mock auth. **ELI5:** omnidev already gives each development pod its own labeled storage boxes, but some Omnigent code still put files in the user's shared box. Those paths now use the pod's boxes without moving the user's home directory. ```text omnidev omnigent | +-- OMNIGENT_DATA_DIR ------> tokens, logs, host/native state +-- OMNIGENT_CONFIG_HOME ---> config.yaml +-- HOME / XDG_* ------------> unchanged credentials and caches ``` ## Test Plan - `uv run --frozen pytest tests/frontends/sdk/test_user_config.py` - `uv run --frozen pytest tests/test_native_state_legacy_dirs.py` - `uv run --frozen pytest tests/host/test_cli_host.py::test_host_pid_path_honors_data_dir_at_import` - `uv run --frozen pytest tests/e2e/omnigent/test_pexpect_harness.py` - `uv run --frozen pytest tests/e2e/omnigent/test_repl_smoke.py::test_repl_smoke_single_prompt` - `cargo test --manifest-path dev/omnidev/Cargo.toml omnigent_cmd::tests` - `uv run --frozen ruff check omnigent/claude_native_state.py omnigent/cli.py omnigent/cli_auth.py omnigent/codex_native_state.py omnigent/opencode_native_state.py omnigent/repl/_session_log.py sdks/ui/omnigent_ui_sdk/terminal/_config.py tests/frontends/sdk/test_user_config.py tests/host/test_cli_host.py tests/test_native_state_legacy_dirs.py tests/e2e/omnigent/_pexpect_harness.py tests/e2e/omnigent/test_pexpect_harness.py` - `cargo fmt --manifest-path dev/omnidev/Cargo.toml --check` ## Demo N/A — non-visual CLI state-isolation fix. ## Type of change - [x] Bug fix - [ ] Feature - [ ] UI / frontend change - [ ] Refactor / chore - [ ] Docs - [ ] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [ ] Integration tests added / updated - [x] E2E tests added / updated - [ ] Manual verification completed - [ ] Existing tests cover this change - [ ] Not applicable ## Coverage notes Regression tests cover pod environment wiring, data/config override precedence, HOME fallbacks, host pidfile placement, native harness state roots, and REPL startup with an isolated config home. ## Changelog `omnidev omnigent` commands now keep runtime state and configuration inside their development pod. Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com> |
||
|
|
39c986cb06 |
feat(desktop): build macOS app for Intel + Apple Silicon (#4772)
Set build.mac.target to build both x64 and arm64 for dmg + zip so the
macOS desktop build stops shipping only the build host's architecture.
mac.artifactName already templates ${arch}, so the two arches produce
distinct files. Config only — electron-builder reads mac.target the same
way for the manual signed release build (pnpm run build:mac:release).
Closes #842
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
|
||
|
|
c8c4f81826 |
ci(electron): remove the redundant manual Electron Build workflow (#4771)
This dispatch-only workflow only produced unsigned, throwaway desktop installers as workflow artifacts — it never published a release. Nothing depends on it: it is workflow_dispatch-only (not a reusable workflow), no other workflow or action references it, and the secure release repo builds Windows + Linux itself (it merely models this workflow's steps). Rather than maintain a second, drift-prone desktop-build definition, remove it. The macOS multi-arch change lives independently in web/electron/package.json. Co-authored-by: Isaac Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> |
||
|
|
6c2daae4a9 |
fix(codex): pin shadowed config provider on resume (#4818)
## Related issue
N/A — reported and reproduced locally.
## Summary
- Pin Codex's detected `config.toml` provider when an explicit, non-default same-name Omnigent entry shadows ambient default synthesis.
- Resolve the provider once during native launch so rollout metadata, app-server, and remote TUI use the same immutable selection.
- Preserve spec, explicit-default, global-auth, subscription, and dismissed-provider precedence.
ELI5: if Codex is configured to use a gateway but Omnigent's matching provider entry is not marked default, a resumed conversation now follows Codex's actual gateway instead of falling back to unauthenticated OpenAI.
```text
Codex config detection ──► resolved native launch ──► resume rollout/TUI
Databricks Databricks Databricks
```
## Test Plan
- `uv run --frozen pytest tests/test_native_codex_provider.py -k 'config_provider_shadowed_by_nondefault_explicit_entry_still_pins or shadowed_config_detection_uses_active_profile_provider or resolve_native_codex_launch_undismissed_config_provider_routes_via_pin or resolve_native_codex_launch_dismissed_config_provider_pins_openai'`
- `uv run --frozen pytest tests/test_codex_native.py -k 'resolve_native_codex_launch_no_provider_sets_login_fallback_summary or resolve_native_codex_launch_databricks_provider_sets_summary'`
- `uv run --frozen ruff check omnigent/codex_native_app_server.py tests/test_native_codex_provider.py tests/test_codex_native.py`
- `uv run --frozen ruff format --check omnigent/codex_native_app_server.py tests/test_native_codex_provider.py tests/test_codex_native.py`
- `git diff --check`
## Demo
N/A — non-visual backend fix.
## Type of change
- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
The new tests reproduce the shadowed non-default provider state and verify active Codex profile selection. Existing tests cover dismissed providers, ordinary detected providers, explicit defaults, and no-provider summaries.
## Changelog
Resumed Codex conversations now keep using the provider selected in Codex configuration.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
|
||
|
|
73abf26b8e |
fix(web): quote server URLs in generated commands (#4817)
## Related issue https://linear.app/omnigent/issue/OMNI-3481/quote-server-urls-in-ui-connection-commands ## Summary - Prevent shells from interpreting query strings and other metacharacters in server URLs shown by the web UI. - Quote server URLs as single POSIX shell arguments across host, Lakebox, reconnect, and resume commands. - Cover command rendering and embedded quote escaping with focused tests. ## Test Plan - `cd web && npm test -- src/lib/shell.test.ts src/shell/ReconnectSessionDialog.test.tsx` - `cd web && npm test -- src/shell/NewChatDialog.test.tsx -t "quotes server URLs"` - `cd web && npm run type-check` - `cd web && ./node_modules/.bin/oxlint --deny-warnings --report-unused-disable-directives src/lib/shell.ts src/lib/shell.test.ts src/shell/NewChatDialog.tsx src/shell/NewChatDialog.test.tsx src/shell/ReconnectSessionDialog.tsx src/shell/ReconnectSessionDialog.test.tsx` - `cd web && npm exec -- prettier --check src/lib/shell.ts src/lib/shell.test.ts src/shell/NewChatDialog.tsx src/shell/NewChatDialog.test.tsx src/shell/ReconnectSessionDialog.tsx src/shell/ReconnectSessionDialog.test.tsx` ## Demo Before: ```sh omni host --server https://example.com/api?profile=dev&glob=* ``` After: ```sh omni host --server 'https://example.com/api?profile=dev&glob=*' ``` ## Type of change - [x] Bug fix - [ ] Feature - [x] UI / frontend change - [ ] Refactor / chore - [ ] Docs - [ ] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [x] Integration tests added / updated - [ ] E2E tests added / updated - [ ] Manual verification completed - [ ] Existing tests cover this change - [ ] Not applicable ## Coverage notes Unit coverage verifies shell quoting directly and rendering through both connection-command UI paths. ## Changelog Server URLs in copyable connection and reconnect commands are now safely quoted. Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com> |
||
|
|
b9d53f0a96 |
feat(server): add deployment-wide release feature flags (#4775)
## Related issue Follow-up to #4673. ## Summary - Add a typed, default-off release-feature registry driven by one comma-separated `OMNIGENT_FEATURES` environment variable, with strict validation and lifecycle metadata. - Gate the web Usage route/navigation and page-only report enrichment while preserving the existing `GET /v1/usage` CLI API. - Migrate web-driven harness installation to the same immutable startup snapshot and wire rollout configuration across Docker, Kubernetes, Render, Railway, and Databricks. ELI5: the server reads one list of enabled features when it starts, enforces that same list on backend routes, and tells the web app which controls and pages to show. ```text OMNIGENT_FEATURES | v FeatureFlags snapshot / \ backend gates GET /v1/info | v frontend gates ``` ## Test Plan - `uv run pytest tests/server/test_feature_flags.py tests/host/test_local_server.py tests/server/integration/test_utility_endpoints.py tests/server/integration/test_hosts_install_harness.py tests/server/integration/test_hosts_store_credential.py tests/server/routes/test_usage_report.py tests/server/test_openapi_drift.py -q` - `cd web && pnpm vitest run src/lib/capabilities.test.ts src/lib/harnessSetup.test.ts src/App.test.tsx src/shell/Sidebar.test.tsx` - `uv run pytest tests/e2e_ui/sessions/test_usage_page_feature.py -q` - `uv run python scripts/dump_openapi.py --check` - `pre-commit run --files <changed files>` - Verified default-off and enabled Usage route/sidebar behavior, strict unknown-feature rejection, legacy CLI usage compatibility, and harness route enforcement. ## Demo - Default off: the updated visual baselines show the original sidebar without the Usage row. - Enabled Usage page: https://github.com/user-attachments/assets/8385d4f0-47ad-430f-bf2c-06c35af6c499 ## Type of change - [ ] Bug fix - [x] Feature - [x] UI / frontend change - [ ] Refactor / chore - [x] Docs - [ ] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [x] Integration tests added / updated - [x] E2E tests added / updated - [x] Manual verification completed - [x] Existing tests cover this change - [ ] Not applicable ## Coverage notes Manually reviewed the default-off visual output and verified that the Usage route is absent while the capability is disabled. Targeted backend and frontend tests cover both flag states, capability parsing, startup snapshots, and harness enforcement. ## Changelog Usage and web-driven harness setup can now be enabled per deployment with `OMNIGENT_FEATURES`. Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com> |
||
|
|
8f194d6f9d |
test(antigravity): wait for quiescence poll progress (#4778)
## Related issue N/A — test-only reliability fix. ## Summary - Prevent the quiescence backoff regression test from exhausting an event-loop iteration budget while its polls are still completing in worker threads. - Signal the async test when the target poll count is reached and always cancel its mirror task during cleanup. ## Test Plan - `uv run pytest tests/test_antigravity_native_reader.py::test_the_quiescence_recheck_backs_off_after_agy_vetoes_a_close -q` - `uv run ruff check tests/test_antigravity_native_reader.py` ## Demo N/A — non-visual test-only change. ## Type of change - [ ] Bug fix - [ ] Feature - [ ] UI / frontend change - [ ] Refactor / chore - [ ] Docs - [x] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [ ] Integration tests added / updated - [ ] E2E tests added / updated - [ ] Manual verification completed - [ ] Existing tests cover this change - [ ] Not applicable ## Coverage notes The updated unit test exercises the existing quiescence recheck backoff behavior with deterministic cross-thread synchronization. Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com> |
||
|
|
204e99d5c6 |
fix(deps): resolve protobuf gencode/runtime mismatch in antigravity extra (#4795)
google-antigravity ships proto files compiled against protobuf 7.x (gencode version 7.35.x). With the prior `protobuf>=6,<7` core pin the runtime was always 6.x, causing: Detected incompatible Protobuf Gencode/Runtime versions when loading google/antigravity/proto/localharness.proto: gencode 7.35.0 runtime 6.33.6. Runtime version cannot be older than the linked gencode version. Fixes #4774. Changes: - Widen core `protobuf` constraint from `>=6,<7` to `>=6,<8` so the resolver can pick 7.x when needed. - Pin `protobuf>=7,<8` in the `antigravity` extra so installing `omnigent[antigravity]` always selects a 7.x runtime; the protobuf cross-version guarantee lets a 7.x runtime load our 6.x gencode. - Declare `[tool.uv] conflicts` for extra/group pairs that are mutually exclusive (antigravity vs cwsandbox/modal; lint vs cwsandbox/modal) so uv can resolve them in independent forks without a lockfile error. - Bump `grpcio-tools` floor to `>=1.83` (first release that bundles libprotoc 35.1 / protobuf 7.x gencode) and regenerate `omnigent/api/routing/v1/routing_pb2.py` so the `routing-pb2-fresh` pre-commit hook continues to pass. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> |
||
|
|
d41f491e37 |
feat(runner): auto-assign structured names to subagents (#4489)
* feat(runner): auto-assign structured names to subagents Subagents are now automatically assigned meaningful structured names (e.g. "researcher-1", "coder-2") at spawn time instead of relying on LLM-chosen titles. A background display-name generator also produces human-readable task-derived labels (e.g. "Investigate auth token refresh") that the UI prefers when available. The LLM's `title` argument to sys_session_send becomes optional — it is stored as a hint label for display-name generation but is no longer the spawn-or-continue key. The structured name is returned in the response handle; the LLM uses it (or session_id) to continue sessions. Changes span the full stack: - Entity/DB: new display_name column on conversation metadata - Runner: per-parent ordinal counter with restart recovery - Tool dispatch: auto-generate structured names, make title optional - Server: expose display_name on ChildSessionSummary, schedule background display-name generation for child sessions - Web UI: prefer display_name in graph/panel labels Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> |
||
|
|
4e13ff5b82 | fix(web): keep chat above growing composer (#4767) |