The journey's comment, docstring, README row and CLI description all say
it spawns "the per-chunk MessageDisplay hook exactly as Claude Code
does". That stopped being true when MessageDisplay moved to a /bin/sh
appender and evaluate-policy moved to a curl against the runner's relay.
Both are pinned by tests — test_message_display_shell_command_round_trips
asserts "python" is absent from the installed command — so the number the
journey reports (~40ms here) is not on any per-chunk or per-tool-call
path.
Left as it was, the number reads as ~40ms of blocked TUI per streamed
chunk, which would make it the largest single cost in the system and the
obvious thing to go fix. It isn't, and I went and measured a replacement
for an optimization the repo already has.
Say what it measures instead: the lifetime of a hook that is Python,
which is what the per-turn hooks (SessionStart / Stop / UserPromptSubmit
/ PreCompact / Task*), the PostToolUse TodoWrite+TaskUpdate matchers, and
the policy hook's pre-relay fallback still pay — and which is the
standing argument for keeping the hot paths off the interpreter. Naming
the tests that pin it points the next reader at the evidence rather than
at a stale comment.
No behaviour change; comments, docstring, description and README only.
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
`omnigent resume <id>` canonicalized every id through the local sqlite store's uuid rule (uuid_to_bytes), even when --server points at a remote server that owns its own id space. A deployment that keys sessions on non-uuid ids (e.g. numeric node ids) had every id rejected client-side with "Invalid session id." before any request was sent.
Only the local path binds the id to the Uuid16 column, so keep the strict uuid guard there; on the remote path forward the id untouched and let the server resolve it, matching how the runner and SDK already pass the id straight through.
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
Signed-off-by: Isaac <no-reply@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
When Codex emits parallel tool calls, the harness processes them
sequentially but _send_message's write+drain sequence is not atomic:
a concurrent caller can write() between another caller's write() and
drain(), interleaving bytes on the subprocess stdin pipe. The Codex
app-server then reads a corrupted JSON-RPC line, drops the response,
and the remaining tool outputs never arrive — causing the turn to stall
for minutes before timing out.
Fix: guard _send_message with an asyncio.Lock (_stdin_lock) so that
the write/drain pair is always atomic. The lock is initialized in
__init__ alongside the other per-session state.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The host daemon inherited stdin from the spawning shell, holding an open
fd to the pseudo-terminal (/dev/pts/N). Even with start_new_session=True,
the shell blocks on exit until all processes with that fd close it — so
users running 'isaac omni codex' in arca were forced to manually kill the
daemon before they could exit.
Passing stdin=subprocess.DEVNULL redirects the daemon's stdin to /dev/null
at spawn time, releasing the terminal fd immediately.
Fixes OMNI-3274.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Wait for deterministic LLM response content instead of the racy toolbar marker.
The test was using _wait_for_turn_complete() which waits for the toolbar's
· ready marker. Under CI load, this marker can appear before Turn 2's actual
output (the "auto-approved" audit line and LLM response) renders, causing
child.before to be captured prematurely with only the input echo. This is a
classic PTY timing race.
The fix: wait for the scripted LLM response "Following up as requested." which
only appears after the full turn has rendered, including the required
"auto-approved" audit line. This synchronization pattern is already used
successfully in test_repl_two_turns_fires_one_approval_per_turn (line 602).
Verified: 10 consecutive runs all passed (was flaky ~1-3% on CI).
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
Co-authored-by: Isaac <no-reply@databricks.com>
* fix(codex-native): honor Max/Ultra reasoning levels instead of coercing to xhigh
Codex advertises a per-model reasoning ladder via model/list: Sol reaches
`ultra`, Luna reaches `max`, and a turn at those levels completes (Sol's
`ultra` runs subagents). Omnigent's picker surfaced them, but the codex-native
effort override validated against the xhigh-capped CODEX_EFFORTS ladder, so a
web-picked Max/Ultra was silently coerced to xhigh before the wire — and the
TUI->web effort mirror stored `ultra` as `xhigh`, so the UI showed the wrong
level.
Validate codex-native efforts against the full codex ladder
(CODEX_NATIVE_EFFORTS) and add `ultra` to the session-metadata vocabulary, so a
picked level rides through unchanged and the UI shows the real level. The
SDK/Responses codex path keeps the xhigh cap + ultra/max alias, preserving
OMNI-1694's defensive fold on that backend.
OMNI-4255
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(web): follow the drafted model's effort ladder in the Configure Codex modal
The effort dropdown mapped a static effortLevels prop computed from the
committed model, so switching the model inside the modal (Sol → Luna) still
listed Sol's `ultra`, with the stale level left selected. Recompute the ladder
from the drafted model and drop a picked level the new model doesn't offer, so
the dropdown never shows a rung the model rejects and Save can't submit one.
OMNI-4255
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* chore(codex): ultra parity in REPL /effort, schema doc, and GLM effort cap
Address Polly's non-blocking notes on #5217, now that `ultra` is a
first-class effort value:
- REPL `/effort` accepts and lists `ultra` (matched EFFORT_VALUES).
- schemas.py reasoning_effort docstring enumerates `ultra`.
- GLM effort cap treats `ultra` as unsupported — every rung above `high` —
so a pinned `ultra` clamps to medium like `xhigh`/`max`.
OMNI-4255
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* feat(scheduled): auto-attach cost_budget policy on fire
Add an optional `max_cost_usd` field to scheduled tasks. When set, the
fire path attaches a `cost_budget` policy to each spawned session,
capping cumulative LLM spend at the configured limit. This prevents
runaway token spend from unattended scheduled runs.
The attachment is non-fatal: if the policy store is unavailable or the
create fails, the session proceeds uncapped (an uncapped session is
better than a dead run).
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix: chain migration after current alembic head
Point the max_cost_usd migration's down_revision to the actual current
head (d5e9f1a2b3c4) instead of z9a2b3c4d5e6, which already had another
child — creating a branch that broke alembic's single-head requirement.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix: SQLite-safe downgrade and update column set test
Use batch_alter_table in migration downgrade for SQLite compatibility.
Add max_cost_usd to the expected column set in migration tests.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix: chain migration after merged main head za2b3c4d5e6f
Repoint down_revision from d5e9f1a2b3c4 to za2b3c4d5e6f so the
migration chains after the task_summary migration that landed on
main, avoiding a dual-head conflict.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(runner): dedupe repeat sub-agent inbox re-wake notices
A parent that idles holding undrained sub-agent results gets a recovery
wake notice ("[System: sub-agent X/Y finished (completed) — N results
waiting in inbox. Call sys_read_inbox to collect.]"). In a large
homogeneous fan-out the latest child's label and the pending count
plateau, so that nudge repeats the identical line at every turn boundary
— visible spam in the parent's stream.
Record the last re-wake notice delivered per parent and skip a follow-up
re-wake that matches it verbatim. The first recovery nudge for any state
still fires (a stranded inbox is never left silent), and distinct
new-completion notices always post (their count differs), so no results
are lost — only redundant repeats are dropped.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* chore(runner): drop explanatory comments on re-wake dedupe
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* fix(runner): let a drained inbox clear the re-wake dedupe record
The re-wake dedupe records the last delivered recovery notice per parent
so a verbatim repeat is skipped. That record described outstanding work,
but was only dropped on session teardown, so it outlived the stranding
episode it belonged to. After the parent drained, a later fan-out round
that produced the same label and pending count had its recovery wake
suppressed as a duplicate — leaving the parent holding undelivered
results with the wake flag cleared. Once every child has finished
nothing re-arms that flag, so the results are never collected: the exact
strand the recovery wake exists to break, and reachable in the
homogeneous fan-out the dedupe targets.
Treat a drained inbox as ending the episode and forget the record, so a
new episode's notice is judged on its own. Suppression across an
intervening completion wake is unchanged.
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
---------
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
Co-authored-by: harry-yao_data <harry.yao@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
A pane's /model persists the exact id it runs (claude-opus-4-8), but the launch gate only accepted an exact catalog row, and a direct-login catalog spells that family as alias rows (opus -> claude-opus-5, the appended claude-opus-4-8[1m] default). Every live switch to a non-default model therefore armed a resume failure: "not in this host's current model list".
Accept a canonical Anthropic id when the endpoint serves canonical spellings and the catalog lists the id's family — the same fold /model already applies to an unpinned canonical id — and keep refusing gateways, Bedrock, and unlisted families so a stale pick still fails fast.
Covers the cold resume of a persisted pick for claude and codex in the live model-flows suite (red on claude before the fix, green after; codex has no alias layer and passes both ways), plus unit coverage of the fold and the launch gate.
Closes#5158
* feat(web): switch claude-native permission mode mid-session
Claude Code's permission mode could only be chosen when starting a
session: `--permission-mode` is a launch flag, so a running session was
stuck in whatever mode it booted with. Switching to auto mode meant
either attaching to the tmux pane and pressing shift+tab, or ending the
session and starting a new one.
Claude Code exposes no non-interactive mode command (`/permissions`
opens an interactive rules dialog, and settings files are read only at
startup), so the switch drives the TUI's own shift+tab cycle:
- `claude_native_bridge.set_permission_mode` presses `BTab` and reads
the mode footer Claude renders below its input box after each press,
until the target mode appears. The cycle is walked rather than
computed from a press count because its width varies — `auto` is only
in it for accounts that have the mode, and `bypassPermissions` only
when the session launched into it. `dontAsk`/`bypassPermissions` are
rejected up front as unreachable.
- The runner dispatches a `permission_mode_change` event and echoes back
the mode the pane actually landed on.
- PATCH /v1/sessions accepts `permission_mode` and persists it as a
label only after the runner confirms the switch, so the UI can never
claim auto mode while Claude is still prompting on every edit. The
label (not `terminal_launch_args`, which records only launch flags) is
what the UI reads back after a reload.
- The composer grows a mode picker for claude-native sessions, mirroring
the existing Codex plan-mode toggle.
The prompting mode is relabelled "Manual" to match what Claude Code
calls it in its own UI and renders in the pane footer; the wire value
stays `default`.
Test Plan
- New bridge tests cover the walk, the no-op when already in the target
mode, an unreachable mode, non-cycleable modes, a pane with no footer,
and a stale footer mid-repaint (this last case was a real bug found
against a live TUI: reading the pre-keystroke mode made the cycler lag
a mode behind and report `auto` unreachable).
- New runner tests cover the dispatch, the 503 on a failed switch, and
the non-native no-op; new server tests cover the forward, the
label-only-after-confirmation rule, and both rejection paths.
- Verified end-to-end against a real `claude` 2.1.220 TUI in tmux:
every mode reached and confirmed in the pane, including a repeated
request for the mode already active.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* feat(web): mirror in-terminal permission-mode switches to the UI
A shift+tab pressed inside the Claude Code TUI never reached the web UI:
only UI-driven switches stamped the mode label, so the composer's picker
showed a stale mode until the next switch from the web side.
Claude Code emits no event on a mode change and hook payloads only arrive
on tool use, so the rendered mode footer is the only available signal. The
forwarder polls it (throttled to 2s — each read spawns a tmux capture-pane
subprocess) and POSTs `external_permission_mode_change` when it differs
from the last reported mode. The server persists the label and publishes
`session.permission_mode` so the live picker follows the pane and a
reloading client restores the same state.
Anchor the footer scan on the input box's closing rule instead of a fixed
tail offset: the footer's height scales with concurrent subagents, and the
anchor also excludes transcript text structurally, so a mode name Claude
echoed while discussing modes can't be misread as live.
The first observation after spawn is the launch mode, not a switch, so it
seeds the baseline without posting — otherwise a passive spawn default
could clobber a mode the web UI just set. An unreadable pane reads as
"unknown" rather than a guess, and the picker hides instead of displaying
a mode the session may not be in (a `permissions.defaultMode` in a
settings file never reaches the launch args).
Also move the picker into the session-config gear modal alongside model
and effort, keeping the composer row uncluttered.
Fix: `external_permission_mode_change` was missing from the events route's
payload-validation passthrough, so every forwarder POST was rejected with
a 400 before reaching its handler — and the forwarder swallows HTTP errors
at debug level, so the mirror failed silently. Added an end-to-end test
that drives the real forwarder against a live server and asserts the event
crosses the actual SSE wire, since each layer's own mocked tests passed
while the seam between them was broken.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* test: drop redundant permission-mode cases and tighten comments
The `sse.test.ts` block duplicated `sessionEvents.test.ts`, which pins the
same envelope through `parseEventLines` and is the file whose stated job is
catching silent lift bugs. The composer case asserting the absence of the
old standalone picker guarded a selector that no longer exists anywhere.
Rewrite the comments that described the change instead of the code: the
e2e docstring recounted how the 400 was found, and a composer comment
narrated what the gear "used to" do.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* fix(server): don't 500 on a silent permission-mode PATCH
`silent` suppresses the live runner forward, so no mode is confirmed and no
label is written — but the publish read `labels_to_set[...]` unconditionally
and raised KeyError, turning a request that changed nothing into a 500.
Gate the publish on the label actually being set. That also keeps an
unconfirmed mode off the picker, matching the rule the forward path already
follows: the label is written only once the pane really moved.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* fix(ci): address all 4 failing CI checks on PR #4018
- Regenerate openapi.json to include the new SessionPermissionModeEvent
type and permission_mode field on UpdateSessionRequest; fixes Pytest
(server-rest) openapi drift check.
- Update E2E landing-page permission-mode test: the default mode label
was renamed from "Default" to "Manual" in claudePermissionMode.ts;
fixes E2E UI Tests (shard 2/3).
- Add test_claude_native_permission_mode_switch_persists to cover the
new in-chat permission-mode picker in the gear modal; extends
_patch_session_as_claude_native with a permission_mode param so the
label stub is set; resolves the E2E UI Required gate.
- Raise the asyncio yield deadline in the quiescence-recheck test from
10 000 to 50 000 to eliminate flaky timing failures on slower CI
runners; fixes Pytest (misc).
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* fix(claude-native): show the permission picker for manual-mode sessions
A session launched in manual mode had no way to leave it. Manual is the
default and writes no `--permission-mode` arg, and the forwarder discarded
its first pane observation as "just the launch mode", so nothing ever
recorded the session's mode. With no mode to render the web picker hid
itself, and the switch could never be started — not because cycling
failed, but because there was no control to press.
Post the first observation instead of seeding silently, so every live
claude-native session publishes its mode within one poll whatever it
launched in, CLI `omnigent claude` included. The overwrite this guarded
against cannot happen: the server ignores a mode equal to the stored
label, and the PATCH path only persists after the runner confirms the
pane repainted, so a stored label already matches the pane.
Verified against a real Claude Code 2.1.237 pane: manual renders
`⏸ manual mode on`, and shift+tab cycles manual → accept edits → plan →
auto → manual, matching the bridge's footer table exactly.
Three tests had encoded seed-silently as intended behavior and now assert
the launch mode reaching the wire.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* test(e2e): locate permission-mode options by data attribute
The picker's options render their label and description in nested spans, so
each option's accessible name is "Auto Auto-runs; a classifier blocks risky
actions" — never the bare label. `get_by_role("option", name="Auto",
exact=True)` could therefore never match, and the click timed out after 30s.
Tag each option with `data-permission-mode` and select on that, matching how
the sibling model and effort rows already expose `data-model-id` and
`data-effort-level` for the same reason.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
---------
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
Co-authored-by: harry-yao_data <harry.yao@databricks.com>
Co-authored-by: Isaac <no-reply@databricks.com>
## Related issue
N/A
## Summary
- Show the installed and available Omnigent Desktop versions in the shell-owned update prompt, with session-safety copy on its own line and unclipped card chrome.
- Carry the effective desktop version through updater status events and keep the native up-to-date dialog consistent.
- Add a development-only semantic-version override that checks the production update feed, making the real update flow reproducible without changing packaged behavior.
ELI5: development can pretend the installed desktop app is older, compare it with the production feed, and pass that same version into the update card.
```text
OMNIGENT_DESKTOP_VERSION_OVERRIDE
|
v
electron-updater baseline ---> production update feed
|
v
update status + current version ---> shell overlay
```
## Test Plan
- `node --test web/electron/test/desktop_updater.test.js`
- `node --test web/electron/test/update-main.test.js`
- `pnpm --filter web exec vitest run src/components/UpdateBanner.test.tsx`
- `pnpm --filter web type-check`
- Targeted `oxlint` over the changed Electron and web sources/tests.
- `pnpm --filter web build:overlay`
- Development flow: `OMNIGENT_DESKTOP_VERSION_OVERRIDE=0.9.0 pnpm start`, then **Server → Check for Updates…** against the production feed.
## Demo
N/A — the UI is rendered in an Electron-owned transparent child window; the focused component assertions and overlay production build cover the final copy and layout contract.
## Type of change
- [ ] Bug fix
- [x] Feature
- [x] 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 updater unit harness covers version propagation, development override isolation, and production-feed configuration. The UpdateBanner test covers available, downloading, and downloaded copy.
## Changelog
Desktop update prompts now show the installed and available versions with clearer copy and polished overlay spacing.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* feat(auth): refreshable credential for unattended host daemons
Implements login-issued refresh grants so unattended hosts can renew
their session tokens instead of crashing when the initial JWT expires.
- Server: OIDC callback persists refresh material and issues a login-scoped
renewal grant (30-day TTL, reuses device-grant store/rotation machinery)
- CLI: load_token() calls refresh_stored_token() on expiry, mints a fresh
session JWT from the grant via POST /oauth/token
- Host: treats post-connection 401/403 as retryable-with-reauth (attempts
token refresh before failing); improves error text for expired tokens
- Auth: login grants (no scope) bypass the delegated-token allowlist,
keeping full authority; delegated tokens stay scope-restricted
- Tests: new coverage for refresh cycles, OIDC mode token router, env
override of grant lifetime
Fixes OMNI-1127 / closes#1953.
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* fix(auth): address review findings for refreshable host credential
Fixes P0-P3 security and robustness issues discovered in review:
P0 (FEATURE-BREAKING): create_redeemed_grant called self._session() with
no query_name, causing TypeError and breaking the entire login-grant
feature. Now uses "insert_redeemed_device_grant" per CLAUDE.md conventions.
P1 (SECURITY): LoginRequest.issue_refresh was a client-controllable bool,
allowing XSS/form-hijack to obtain 30-day unattended credentials via
browser login. Removed the field entirely; browser /auth/login now NEVER
issues refresh material — only CLI/device flows do (server-side enforcement).
P2.1 (ROBUSTNESS): _check_cookie dropped isinstance(grant_id, str) guard,
allowing malformed grant_id claims to reach _grant_revoked(). Restored guard.
P2.2 (ROBUSTNESS): _store_entry assumed token file was dict but _load_entry
guards with isinstance(data, dict). Mirror the guard on write to prevent
TypeError on corrupt files. Treat non-dict as empty (fail-safe).
P3.1 (CLEANUP): load_token's expiry warning said "attempting automatic
refresh" but load_token never refreshes. Reworded to reflect actual behavior.
P3.2 (CLEANUP): _make_client_secret_gate was built twice (once in
create_device_auth_router, again in included create_oauth_token_router),
duplicating env reads and logs. Pass gate as parameter to avoid rebuild.
P3.3 (CLEANUP): _grant_max_lifetime_seconds() re-parsed os.environ on every
refresh/purge. Now called once at router mount, captured in closure.
Added regression tests:
- test_redeemed_grant_persistence_regression: grant row must be created
(catches P0 TypeError)
- test_browser_login_never_issues_refresh_token: browser login must NOT
return refresh_token (catches P1 client-controllable flow)
Follow-up (test reconciliation + lint, from running the suite):
- Re-point the login-grant round-trip and session-authority tests to mint
via issue_login_grant (the CLI/device path) now that browser /auth/login
no longer returns refresh material.
- Fix pre-existing runner-entry test lag: load_token mocks now accept the
min_remaining_seconds kwarg the factory passes, and drop a stray
OMNIGENT_RUNNER_DELEGATED_AUTH that contradicted a test's documented
no-delegation scenario.
- Fix a latent NameError: _grant_max_lifetime was referenced in
create_device_auth_router but only bound in create_oauth_token_router;
resolve it once at mount in the device router too.
- Remove the now-dead device_grant_store wiring from the accounts auth
router (its only use was the removed issue_refresh path).
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* fix(host): escalate to a re-auth prompt on sustained post-connect auth rejection
A host rejected with 401/403 after it has already connected retries forever, so a transient VPN or proxy drop self-heals. Until now it only logged "check your VPN/network", so a permanently-rejected credential (a revoked or expired grant) looped silently and never told the operator to re-authenticate. After a sustained streak it now escalates with a louder warning plus a stderr line naming the omnigent login command, re-emitted periodically. It stays retryable and never fatal, so a recoverable daemon is not killed.
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* test(cli): use one import style in the refresh test
Drop the mixed import omnigent.cli_auth + from-import inside test_refresh_survives_unwritable_state_dir; call store_token/refresh_stored_token via the ca alias. Resolves the code-quality bot finding on the PR.
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* fix(cli): drop dead accounts issue_refresh and cover the refresh-factory wiring
Accounts /auth/login issues no refresh material (only the OIDC CLI-ticket flow does), so the accounts-login POST no longer sends the ignored issue_refresh field, and its misleading "older servers ignore it" comment is removed. Updates the CLI accounts-login test that asserted the field.
Also adds a runner-entry test proving the load->refresh->fallback auth-token factory returns the refreshed token when the stored OIDC token has lapsed but a refresh grant is present — the integration that actually keeps an unattended host alive, previously only unit-tested at the refresh function itself.
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* fix(server): mount login-grant token router only for UnifiedAuthProvider
The elif that mounts /oauth/token for login grants when the device flow is off only checked for a grant store, leaving auth_provider typed as the base AuthProvider (pyrefly bad-argument-type at the create_oauth_token_router call) and — for a non-Unified custom provider with a store — a latent runtime failure in _resolve_signing_config. Guard the branch on isinstance(auth_provider, UnifiedAuthProvider), matching the sibling device-flow branch.
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* test(e2e): accept min_remaining_seconds in managed-runner load_token mocks
The auth-token factory now calls load_token(url, min_remaining_seconds=...); two managed-runner e2e tests monkeypatched load_token with a lambda that rejected the kwarg, raising TypeError. Accept **_kw, matching the runner-entry unit mocks.
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>
## Related issue
[OMNI-4272](https://linear.app/omnigent/issue/OMNI-4272/add-a-global-profile-option-to-omni-cli-for-profiling)
## Summary
- Rename the global CPU profiler flag from `--profile` to `--profiling` so it no longer collides with the existing Databricks `--profile NAME` option.
- Preserve the historical bare-run shorthand `omni --profile my-sp ...` while keeping CPU profiling available for every explicit command.
- Add regression coverage for both options used independently and together.
**ELI5:** `--profiling` measures performance; `--profile NAME` continues to choose Databricks credentials.
```text
omni --profiling COMMAND -> CPU profile
omni --profile my-sp -p "..." -> run with Databricks profile my-sp
```
## Test Plan
- `uv run ruff format omnigent/cli.py tests/cli/test_cli.py`
- `uv run ruff check omnigent/cli.py tests/cli/test_cli.py`
- `uv run pytest -q tests/cli/test_cli.py::test_global_profiling_writes_summary_and_timestamped_stats tests/cli/test_cli.py::test_removed_ad_hoc_detection tests/cli/test_cli.py::test_help_groups_harnesses_and_other_commands tests/cli/test_cli.py::test_run_profile_sets_databricks_config_profile_env tests/cli/test_cli.py::test_bare_run_profile_shorthand_still_selects_databricks_profile tests/cli/test_cli.py::test_global_profiling_coexists_with_run_databricks_profile`
## Demo
N/A
## 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
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
The tests exercise the real module entry point for CPU profiling and `main()` argv rewriting for the historical Databricks profile shorthand.
## Changelog
Use `omni --profiling COMMAND` for CPU profiling without conflicting with Databricks `--profile NAME` selection.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
[OMNI-4272](https://linear.app/omnigent/issue/OMNI-4272/add-a-global-profile-option-to-omni-cli-for-profiling)
## Summary
- Add a global `omni --profile COMMAND` flag so developers can profile any CLI command with Python's built-in `cProfile`.
- Print actionable Omnigent-only cumulative and self-time summaries while preserving the complete timestamped `.prof` data under `~/.omnigent/profiles/`.
- Keep the existing `omni run --profile NAME` Databricks credential option compatible, including when both profile options are used together.
**ELI5:** Put `--profile` before a command to see which Omnigent functions made it slow; open the saved `.prof` file when deeper analysis is needed.
```text
omni --profile COMMAND
|
v
cProfile
/ \
focused stderr full timestamped .prof
```
## Test Plan
- `uv run ruff format --check omnigent/cli.py tests/cli/test_cli.py`
- `uv run ruff check omnigent/cli.py tests/cli/test_cli.py`
- `uv run --group test pytest -q tests/cli/test_cli.py::test_global_profile_writes_summary_and_timestamped_stats tests/cli/test_cli.py::test_removed_ad_hoc_detection tests/cli/test_cli.py::test_help_groups_harnesses_and_other_commands tests/cli/test_cli.py::test_run_profile_sets_databricks_config_profile_env tests/cli/test_cli.py::test_global_cpu_profile_coexists_with_run_databricks_profile tests/cli/test_cli.py::test_run_profile_wins_over_preset_env tests/cli/test_cli.py::test_run_without_profile_leaves_preset_env_untouched`
- Manually ran `OMNIGENT_DATA_DIR=<temp-dir> uv run python -m omnigent --profile version` and inspected the focused summary and generated `.prof` file.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [x] 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
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Verified the real `python -m omnigent` entry point creates a loadable timestamped profile, renders only actionable Omnigent functions in the summary, and preserves the separate Databricks `run --profile NAME` behavior.
## Changelog
Use `omni --profile COMMAND` to print focused CPU profiling results and save the full profile for deeper analysis.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
https://linear.app/omnigent/issue/OMNI-4270/remove-memory-extra
## Summary
- Remove the deprecated `memory` compatibility alias now that `hindsight` is the canonical extra.
- Regenerate the normalized lockfile so published package metadata no longer advertises `memory`.
## Test Plan
- `uv run --no-sync pytest -q tests/onboarding/test_extra_install.py`
- Build the wheel and inspect its `METADATA` to confirm `hindsight` remains in `Provides-Extra` and `memory` does not.
- `git diff --check`
## Demo
N/A — packaging-only change.
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [x] Breaking change
## Test coverage
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Verified the built wheel's optional dependency metadata directly; the existing onboarding extra tests continue to pass.
## Changelog
The deprecated `omnigent[memory]` extra has been removed; use `omnigent[hindsight]` instead.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
[OMNI-4259](https://linear.app/omnigent/issue/OMNI-4259/cannot-copy-out-of-tui-in-web-desktop-app)
## Summary
- Forward tmux paste-buffer changes from control-mode terminals to the active owner’s browser clipboard, with payload limits, recent-input gating, and read-only isolation.
- Support safe tmux-generated OSC 52 writes for PTY terminals while disabling them when passthrough could let pane output bypass tmux’s clipboard policy.
- Add browser permission fallback UI, preserve terminal focus, and cover the bridge, routing, parser, and clipboard behavior with focused tests.
**ELI5:** When a terminal app says “copy this,” the runner now carries that text back to the browser, which places it on the computer’s real clipboard instead of leaving it only inside tmux.
```text
tmux copy buffer ── control notification ──> WebSocket JSON ──┐
tmux PTY copy ── safe OSC 52 ───────────────────────────────┤
v
browser clipboard
```
## Test Plan
- `uv run pytest tests/terminals/test_control_bridge.py -q`
- `uv run pytest tests/terminals/test_ws_bridge.py -k 'osc52_capability or forward_pty_to_ws' -q`
- `TMPDIR=/tmp uv run pytest tests/inner/test_terminal.py::test_server_survives_inner_process_exit_real_tmux tests/inner/test_terminal.py::test_launch_enables_csi_u_extended_keys_quietly -q`
- `uv run pytest tests/runner/test_terminal_resource_attach_ws.py -k 'selects_control_bridge_on_transport_query' -q`
- `cd web && npm test -- --run src/components/blocks/TerminalSession.test.ts src/components/blocks/TerminalView.test.tsx`
- `cd web && npx tsc -p tsconfig.app.json --noEmit`
- Targeted Ruff, Oxlint, Prettier, and `git diff --check`.
## Demo
N/A — no screenshot or recording was captured for the terminal protocol integration.
## 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
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Focused tests cover tmux buffer notifications, bounded reads, cancellation cleanup, read-only and recent-input behavior, PTY OSC 52 capability negotiation, passthrough hardening, browser parsing, permission fallback, focus restoration, and transport routing.
## Changelog
Text copied by terminal apps now reaches your system clipboard in the web terminal.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
N/A
## Summary
- Sign, notarize, staple, and validate each final macOS DMG from the existing one-command release build.
- Reuse the app notarization credentials and fail the release if Apple rejects a DMG or stapling fails.
- ELI5: the app was already sealed and checked by Apple; this also seals and checks the downloadable disk image around it.
```mermaid
flowchart LR
A[Sign and notarize app] --> B[Build and sign DMG]
B --> C[Notarize DMG]
C --> D[Staple and validate]
```
## Test Plan
- `cd web/electron && node --test test/notarizeDmg.test.js`
- `uv run pre-commit run --files web/electron/package.json web/electron/build/afterAllArtifactBuild.js web/electron/test/notarizeDmg.test.js web/electron/README.md`
- Live Apple submission was not run locally because it requires release credentials.
## Demo
N/A — non-visual release packaging change.
## Type of change
- [ ] Bug fix
- [x] 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
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Unit tests cover credential argument construction, release command sequencing, successful notarization, and rejection handling. The release hook also validates the real codesign and stapling commands when run with Apple credentials.
## Changelog
macOS DMG releases are now signed, notarized, and stapled for stronger download verification.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
N/A — maintainer-requested Electron debugging support.
- Keep the production Electron shell locked down by default while allowing packaged macOS builds to opt into DevTools through `defaults write ai.omnigent.desktop DeveloperMode -bool true`.
- Use the same developer-mode decision for the Debug menu and the main window's DevTools capability, without relaxing update security gates.
- Document the opt-in and cover the preference decision and main-process wiring.
ELI5: development builds keep their debugging tools; a production build only unlocks them when the local macOS user explicitly sets the app preference.
```text
app launch
├─ unpackaged build ───────────────────────> DevTools enabled
└─ packaged build + macOS DeveloperMode ──> DevTools enabled
otherwise ────────────────> DevTools disabled
```
- `node --test web/electron/test/developer_mode.test.js web/electron/test/main.test.js web/electron/test/update-main.test.js`
- `web/node_modules/.bin/oxlint --deny-warnings --report-unused-disable-directives web/electron/src/developer_mode.js web/electron/src/main.js web/electron/test/developer_mode.test.js web/electron/test/main.test.js web/electron/test/update-main.test.js`
- `(cd web && node_modules/.bin/tsc -b)`
- `git diff --check`
N/A — this is an opt-in main-process configuration change with no default UI change.
- [ ] Bug fix
- [x] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
- [x] Unit tests added / updated
- [x] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
The unit matrix covers development, packaged macOS opt-in/out, unsupported platforms, and read failures. The main-process harness verifies Debug-menu gating, and a wiring guard verifies the shell window uses the same decision.
Packaged macOS builds can opt into Electron Developer Tools with a local `defaults` setting.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
On a fresh launch the CLI created the session (`POST /v1/sessions`,
~2.1s) and then waited for the host daemon to register its tunnel
(`GET /v1/hosts/{id}`, ~0.6s), one after the other. Neither depends on
the other: the create needs no host, and the host poll needs no session.
`claude_native` already ran the two concurrently; every other daemon
launch path paid the sum.
Run them under one `asyncio.gather` in `omnigent run` and in the ten
remaining native harnesses (codex, pi, cursor, goose, kimi, qwen,
opencode, hermes, kiro, antigravity), so startup costs the longer of the
two rather than both. The post-create host wait is now guarded by
`fresh_session`, since the fresh path has already done it; a resume still
waits for the host on its own.
For `omnigent run` this also meant lifting the session create/fork into a
`resolve_session()` coroutine so it can run underneath the daemon
client's host poll, instead of opening and closing the SDK client first.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
Co-authored-by: harry-yao_data <harry.yao@databricks.com>
* feat(host): answer pre-launch model listings by probing the real harnesses
The pre-launch pickers were fed by catalog reconstruction — for a
Databricks-gateway codex host, serving-endpoint name enumeration: id
spellings the gateway's codex surface does not route, chat-only traps
(gpt-oss), no display names or effort ladders. The harness itself is
the only authority on what its /model picker would offer, so the host
now asks the harnesses:
- codex-native: probe_codex_model_options boots codex app-server with
the SAME Databricks materialization a session launch gets (shared
_databricks_launch_materialization, extracted from
build_codex_native_server so the two cannot drift), a persistent
probe CODEX_HOME (codex's own models_cache ETag makes refreshes
cheap), and passes model/list rows through verbatim with a single
default marker (launch pin first, else codex's own). Scoped to
Databricks-profile launches; everything else — and every probe
failure — falls open to the existing catalog path unchanged.
- claude-native: session launches (and the probe) now opt in to Claude
Code's gateway model discovery
(CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 in the ucode env; the
fetch 404s harmlessly until the gateway serves /v1/models).
probe_claude_gateway_models runs claude -p "/model" with the launch
env so the harness executes its own discovery, then reads the
harness-written gateway-models.json artifact — no discovery
semantics replicated. Rows union with the configured tier rows,
exact-id deduped. The nonessential-traffic kill-switch is stripped
from the probe env (Claude treats it as covering discovery).
- claude-sdk: SDK-mode claude is a pass-through client with no catalog
of its own, so the endpoint listing is the harness truth — served
via the existing list_models_for_worker in the exact wire spelling
the SDK sends.
Serving stays off the probe path: a new host-side cache
(omnigent/host/model_options_cache.py) keys results by a resolved-
config fingerprint, serves stale-while-revalidating with single-flight
probes, and is prewarmed per tunnel connection — measured 65ms at the
REST route warm, ~1.3s joining the prewarm probe cold. The
model-options frame is now answered from a tracked task instead of
inline on the tunnel receive loop (a cold probe there stalled every
frame — same class as a83cc707); a filesystem frame answered in 22ms
mid-probe. The REST route also stops dropping the routable_models the
frame already carries (openapi regenerated).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(host): always probe Claude Code itself for the model list
claude -p "/model" makes the harness print its own alias enumeration
headlessly, so the curated static subscription list demotes from
first resort to failure fallback. probe_claude_gateway_models
generalizes to probe_claude_model_options: it runs for every config
shape (bare subscription launches included), parses the printed
"Available:" aliases verbatim (no alias names known to the parser, so
new Claude releases flow through), and still reads the discovery
artifact when the env opts in. The host lane serves configured tier
rows (the rich spelling for pinned aliases) unioned with the
harness's printed aliases and discovered gateway rows, exact-id
deduped; the configured/static rows stand alone only when the probe
itself fails.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(host): probe Codex for every launch shape, not just Databricks routing
Same mandate as the Claude lane: the harness always answers. The probe
drops its Databricks-profile gate — a non-profile launch boots codex
app-server with whatever -c overrides the launch resolved (provider
routing, the dismissal pin, or nothing) and reads model/list verbatim,
so subscription/CLI-login and custom-provider shapes get Codex's real
visible catalog instead of the static curated list (the stale
hyphenated-id class of bug) or the raw enumeration. With no
launch-pinned model, Codex's own default marker stands. The legacy
catalog paths remain solely as the probe-failure fallback, pinned by
the existing handler tests now running with a failing probe stub.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(host): resolve Claude model aliases to concrete versions in the picker
The probed alias list answered WHICH aliases exist but not what they land
on — 'opus' could be Opus 5.0 or 4.8 and the picker couldn't say. Ask the
harness that too: each printed alias gets its own headless
'--model <alias> -p /model' run in stream-json mode, whose init event
carries the exact resolved id and whose printed 'Current model:' line
carries the human label (only the effort suffix stripped). Rows become
{id: alias, model: exact id, displayName: 'alias — label'}; the web
picker already renders displayName, so no frontend change.
Resolution runs share the enumeration run's invocation assembly so the
two cannot drift, fan out under one bounded budget (startup dominates
and stretches with box load — measured 0.7s-17s for the same command —
so one wave covers a whole alias set), and fail per-alias back to the
bare row, never the probe. Live run resolves all 10 aliases in ~6s and
surfaces facts worth not guessing: fable[1m] resolves to plain
claude-fable-5, and 'best' pins to Fable rather than Opus.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(host): show Claude picker rows as resolved versions only
Presentation pass on the resolved alias rows: the display is the
harness's resolved label alone (the alias prefix was noise), 1M-context
resolutions always say '(1M context)' even where the harness's label
omits it (sonnet[1m] prints just 'Sonnet 5'), the 'default' alias never
becomes a row (the picker renders its own Default choice, so it was a
duplicate), and aliases resolving to an earlier row's exact (model,
label) are dropped — which removes 'best' and 'fable[1m]' as the
duplicates of fable's row they currently are, without hardcoding any
alias name. Launch ids are untouched; only displayName and row
membership change.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(host): dedupe Claude picker rows by resolved model alone
opusplan resolves to claude-sonnet-5 — a model the sonnet row already
lists — so the same duplicate-model rule that removes best and
fable[1m] now covers it: one picker row per resolved model, no alias
names hardcoded. A composite-mode alias would reappear only if it ever
resolved to a model no other alias offers.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(models): drop curated picker fallbacks; sessions ride the probe
The release-curated picker stand-ins in model_fallbacks are gone — the
live harness probes are the source of truth everywhere, and a path that
cannot probe now reports nothing rather than a plausible-but-stale list
(the codex entries even carried hyphenated spellings codex itself does
not use). Smart Routing's tables stay: rankings, arm menus, and probed
exclusions are router contract data no discovery API can provide, and
the ownership test now guards those records.
Companions so nothing regresses to empty:
- The subscription sonnet_5 pick degrades to Claude's own 'sonnet'
alias instead of hunting a static list — the harness resolves it.
- The claude-sdk pre-launch lane rides the claude probe whenever the
endpoint listing is empty (the SDK drives the claude CLI, so the
CLI's aliases are its truth on subscription boxes).
- Existing sessions now match the new-session picker: the runner's
claude-model-options endpoint resolves configured rows ∪ probe once
per session via the new shared claude_model_options_with_probe (the
host lane uses the same composition, so the two cannot drift),
answering 503-pending while the probe is in flight (the server fetch
already retries those) and falling back to configured rows past a
grace so the catalog is never empty.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): friendly composer label for Claude [1m] aliases off-catalog
The composer chip prefers the session catalog's display name, but a
Claude bracket alias the catalog doesn't list (a pick made before the
catalog carried the row, e.g. on a session launched by an older runner)
fell through to the raw id — 'sonnet[1m] High'. Render that case as
'Sonnet (1M context)': title-cased family plus the context marker, no
version claimed, since only the harness knows which Sonnet the alias
lands on. Catalog hits keep the probed display name verbatim.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): version-agnostic 'Sonnet' fallback label kills the 4.6 flash
Cold-loading a claude session painted the composer chip 'Sonnet 4.6'
for the window before the session catalog arrived, then corrected to
'Sonnet 5' — the fallback label list pinned a version that only the
harness can know (reproduced via Playwright: 'Sonnet 4.6 High' at
3.96s → 'Sonnet 5 High' at 4.70s). The fallback now says just
'Sonnet'; the catalog's display name supersedes it wherever one has
arrived, so the pre-catalog window shows a coarser label, never a
wrong one. Same honesty for the sandbox new-chat picker and the
scheduled-task model dropdown, which render the same list.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(web): retire two redundant uses of the local Claude alias list
The composer chip's pre-catalog fallback now formats alias-shaped ids
mechanically (title-case family, '_N' → ' N', '[1m]' → ' (1M context)')
instead of looking them up in CLAUDE_NATIVE_MODELS — same rendering,
zero model knowledge. The sticky-model compatibility check collapses to
session-catalog membership alone: its isClaudeNativeModel conjunct was
subsumed by the catalog check it was AND-ed with, and would have
rejected catalog rows whose ids don't look Claude-ish even though the
session's own catalog offered them. The now-orphaned guard is deleted;
the list itself stays for the genuinely hostless surfaces (sandbox
picker, unpinned scheduled tasks, schema enums).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(e2e_ui): composer label stays version-free until the catalog speaks
Covers the label behaviors the model-listing work changed: with the
session catalog held back, the composer chip renders the alias
mechanically ('sonnet[1m]' → 'Sonnet (1M context)'), and only the
arriving catalog upgrades it to its display name ('Sonnet 5 (1M
context)'). Every painted label is recorded via a MutationObserver so a
transient raw id or invented version ('Sonnet 4.6') cannot hide from a
retrying expect().
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(web): offer Smart Routing in the in-session gear for native panes
The in-session composer gear withheld the Smart Routing model option
from native Claude Code / Codex sessions under a stale premise ('their
CLI bakes the model at launch') — the server has routed native panes
per turn via /model injection since the create-time gear gained the
option, and validates routing-on creates with a per-family rule. The
in-session gate now mirrors that exact rule: a router must answer for
the session's family — the external AI-Gateway router only when the
host runs the family through the gateway (read off the session's host
row; absent rows fail open like the landing), the built-in judge
anywhere. SDK/bundle sessions keep their existing flag-only gate.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* style(web): prettier over the routing-gate and label changes
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(runner): sys_list_models subscription row is an honest empty listing
The curated claude stand-ins are gone from the static subscription
path; the dispatch test now pins the empty-models shape with the
probing note, matching the model-catalog contract.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): composer gear rides the host probe until the session catalog lands
A fresh codex session's gear showed a sparse Model row and no Effort row
for ~15s: effort levels come from the session catalog's
supportedReasoningEfforts, and that catalog only resolves once codex
app-server answers model/list. The session's host already probed the
same harness for the new-chat picker, so the gear (and the composer
chip) now falls back to those cached rows — same ids the launch accepts,
~90ms warm — whenever the session's own catalog is empty; the runner's
per-session catalog supersedes them the moment it arrives. Claude
sessions get the same pre-catalog Model list for free (their effort
levels were already static).
Verified live on a fresh codex session: Effort visible 0.5s after
create+load with the session catalog still empty, offering the host
row's levels.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): keep model-options identity stable when the host fallback is idle
The pre-catalog host fallback returned a fresh empty array whenever the
session catalog was empty and no host rows existed — for EVERY session
shape, native or not. That new identity per render re-rendered each
options consumer (composer, gear, agent-info popover) on every
streaming/liveness tick, which under CI load tipped the agent-info
hover-open grace race (shard 1 failed the same popover test twice).
Substitute only when host rows actually exist; otherwise the store's own
stable array reference flows through untouched, restoring the exact
pre-fallback behavior for every session the feature doesn't apply to.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(claude-native): apply picked model aliases verbatim, never the default
Picking Fable in a gateway session's composer switched the pane to
Opus: resolve_claude_native_model_selection swapped an unpinned family
alias for the provider's default model (a degrade from the era when
the picker always showed every family alias), the vocabulary re-spelled
that default as its pinned alias, so the runner injected '/model opus'
for a 'fable' pick — and the statusLine mirror then recorded the wrong
model as the session override. Bracket aliases had sibling failures:
'/model sonnet[1m]' 503'd on a pinned env (no spelling for it) and
silently dropped the [1m] marker on a bare login (family-segment
step-down).
Picker rows are pin-backed or probe-vouched now, so a pick passes
through verbatim and Claude owns resolution:
- the resolver's no-pin gateway degrade is gone (an out-of-band
unpinned pick now fails visibly at inference instead of silently
running the default);
- bracket variants of the family aliases are their own /model
arguments in the vocabulary — the harness enumerates them itself;
- the configured∪probe union drops probe rows whose resolved model is
a bare canonical Anthropic id on an endpoint that routes its own ids
only: the pick could never work there, so the row is not offered
(a pinned family resolves to the endpoint's spelling and stays).
Reproduced at the runner layer (events → resolver → injected command):
picking 'fable' asserted '/model fable' and got '/model opus' before
the fix. An e2e_ui guard pins that the web PATCHes the picked row id
verbatim — the client layer was innocent.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(claude-native): mirror pane model switches in the catalog's vocabulary
Live verification of the verbatim-alias fix exposed the last surface in
the same family: after a web pick of 'sonnet[1m]' correctly switched the
pane, the statusLine mirror collapsed the observed model back to the
LEGACY picker vocabulary — 'databricks-claude-sonnet-5[1m]' became
'sonnet_5' — stomping the just-saved override with an id the session's
catalog doesn't list (and which a relaunch would resolve through the
custom-tier branch, silently dropping the 1M context).
_model_alias_for now speaks the catalog's row ids: 1M resolutions keep
their bracket marker ('sonnet[1m]'), and the legacy 'sonnet_5' opt-in
row is mirrored only on a config whose custom slot actually pins it —
read off the session's launch pins — since everywhere else the generic
sonnet row IS that model. This also restores the designed web→TUI
round-trip no-op: the mirrored alias now equals the persisted override,
so the server-side dedupe skips the write.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(codex-native): report the model a Default launch actually runs
A Default codex launch names no model, so nothing pinned the session's
config.toml — yet the profile still resolved a concrete model and passed it
as `-c model=`, which outranks the config copied from the user's shared
~/.codex home. The pane ran the resolved model while the session reported
the shared file's leftover one: the create dialog promised
"Default (GPT-5.6-Luna)" and the session then said GPT-5.4.
Pin the profile-resolved model in codex's own spelling, so the forwarder
mirror and the cost gate read the model this session runs. Mark that model
as the catalog default too — codex's own isDefault is its built-in
preference and named GPT-5.6-Sol on a session running Luna, which also fed
the composer gear an effort ladder the running model rejects.
Web side: fold catalog and codex spellings when resolving a session's model
onto a picker row, and stop borrowing the default row's effort levels for an
unresolved model.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(runner): keep the codex model-options test off the real launch
Session create launches Codex for real, and that launch owns the bridge
dir: it clears the state and its forwarder task rewrites both the state
and CODEX_HOME/config.toml after the response returns. On a machine
where Codex and a Databricks profile resolve, that wiped the seeded
state no matter which side of create seeded it, so the endpoint answered
503. Stub the launch; the endpoint, the bridge-state read, the
CODEX_HOME read, and the fake app-server client all stay real.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): name Codex's Default the same in both model gears
The composer gear and the new-session gear each built their own copy for
the Model row, so one session read a bare "Default" in the composer and
"Default (gpt-5.6-luna)" on the landing page, and the landing page
listed raw catalog ids where the composer listed display names. Neither
gear told the user which model Codex would actually run.
Move both labels into HarnessConfigControls next to the sentinels they
belong to and read them from there in both callers. Row ids are
untouched, so picks still submit the harness's own spelling.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(claude-native): read the custom slot instead of guessing the model row
The terminal->web mirror mapped a concrete model id onto a picker row by
looking for a family name inside the id, so a routed Opus 4.9 landed on
the `opus` row that holds 4.8: the web showed the wrong model, and posting
that row back stepped the session off its launch pin. Resolve rows by
exact comparison against the launch pins instead, and read Claude Code's
one custom model slot to name its row rather than inferring it from the
model's spelling. A `[1m]` resolution stays a distinct row from its
non-bracket sibling.
The legacy `sonnet_5` row id and the substring spellings it used to be
matched by move into claude_model_vocabulary with a 0.10.0 removal note;
the substring leg now runs only when the exact comparison misses.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(web): drop the substring model-row match the picker never calls
`isModelImplicitlySelected` guessed which picker row a bound model belonged
to by searching for the row id inside the model name, which is why `sonnet`
matched `sonnet-5` and needed a special case per generation. Its only
caller sat in the branch taken when a session has no server-supplied model
list, and every native picker kind is on that list, so the branch ran with
an empty list and the call could not select anything.
Delete the function and collapse the caller to the server-list path. The
two suites that covered it go with it; nothing else exercised it.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* docs(claude-native): say plainly that gateway model discovery never fires
The launch env asks Claude Code to discover the gateway's model inventory,
and the comment claimed the only thing holding it back was a gateway that
did not serve `/v1/models` yet. The gateway serves it now, but the same env
sets CLAUDE_CODE_USE_GATEWAY, and the CLI fires that fetch only on its
first-party provider path — so the artifact is never written and the rows
read from it are always empty.
Name that in all three places a reader lands: the flag, the probe's
env-unset list (popping the nonessential-traffic switch is not enough), and
the artifact read itself.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(model-flows): add the harness-truth e2e suite, red-first
The model-flows design lands test-first: this suite encodes the target
behavior for every flow (pre-launch picker, default labels, create→launch
pane truth, gear parity, confirmed switching, terminal-side mirroring) and
is deliberately red today in the ways the analysis measured.
Two tiers. The hermetic tier drives the real SPA over the spawned server
with the session snapshot shaped at the browser edge and SSE frames pushed
through a captured stream controller; it runs in the normal e2e_ui lane.
The live tier (`live_model_flows` marker, opt-in via
OMNIGENT_E2E_MODEL_FLOWS=1) boots a real server + host from any checkout —
OMNIGENT_E2E_MODEL_FLOWS_REPO selects which, so the identical tests
produce the red-on-main matrix — flips provider shapes the way setup
writes default claims, launches real claude/codex TUIs, and asserts pane
truth over tmux.
Recorded pre-implementation: hermetic 4 red / 2 guard-green (the design's
predicted set exactly); live rows 1 and 5 red against unmodified main
(the frozen "Sonnet 4.6" static list; the empty/erroring codex pre-launch
answer).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(models): verbatim reported_model as the single display authority
Landing-order step 3 of the model-flows design. Sessions gain a
reported_model — the model the harness last said it is actually on, in
the harness's own spelling — stored as a new key in the existing
session_overrides blob (no DDL) and served on the snapshot's llm_model
field with precedence reported ?? spec model. external_model_change
writes and dedupes against it verbatim; the user's request
(model_override) is untouched, because requests and reports are separate
roles and only reports are ever displayed.
The claude forwarder now posts the status file's model byte-for-byte:
the alias-collapse mapper (_model_alias_for / _custom_slot_row_id) is
deleted — collapsing a routed Opus 4.9 onto the opus row holding 4.8 is
the bug class this kills — and the first-observation-silent-seed rule is
gone, so the launch's own model reports within seconds of spawn and the
composer is never blank-forever. The codex forwarder already posted raw
ids and needed no change.
The web renders and highlights models from the reported value alone:
exact id/model match against the catalog, with an off-catalog report
appended as its own raw row rather than relabeled onto a same-family
row. The sticky model becomes a pure preference — the silent bind-time
and delayed-catalog model_override PATCHes are removed (they wrote
requests the pane was never asked to honor), and session.model events
land on llmModel instead of the picker selection. Cost attribution
prefers the reported model too.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(models): explicit launches from the shared catalog store
Landing-order step 4. Every native launch now pins its model explicitly,
resolved through the new on-disk catalog store
(omnigent/model_catalog_store.py — one probe result under a
launch-config fingerprint, read by every consumer), so nothing is left
to invisible CLI-private state and a stale config line can never govern
a session.
Claude: the enumeration probe runs in stream-json and captures its own
init-event model — the truthful Default — so claude_model_catalog marks
exactly one isDefault row (appending an off-list default, e.g. a
settings.json pin, as its own launchable row; never appending a bare
Anthropic spelling on an endpoint that rejects it). A Default
subscription launch passes --model with that default; an explicit
request is validated against the catalog and fails the launch loudly
when the list no longer carries it. The runner also records the launch
vocabulary onto the bridge after config resolution
(record_model_vocabulary), closing the model_env gap that made
mid-session /model conversion read the runner's ambient env.
Codex: the session-shaped probe home now links the account's real
auth.json (the catalog must answer for the account that will run — the
Sol-promised/Terra-offered mismatch dies here), a Default launch on
codex's own login resolves the account's real default instead of
inheriting the copied config line (the stale-gpt-5.4 400 class), and
build_codex_native_server emits -c model= alongside the config-copy pin
from one resolved value on every shape. A guard test pins the
argv/config-pin agreement across all provider shapes; on the profile
shape the file deliberately keeps codex's own spelling and the guard
asserts same-model rather than same-bytes (addendum).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Serve every model surface from the shared harness catalog
One catalog file per (harness, launch-config fingerprint) now backs the
pre-launch picker, launch resolution, and the in-session gear:
- Host: catalog-backed model-options handlers with a concurrent, detached
boot prewarm; probe failures answer ok+[] plus an error string the web
new-session dialog displays. The in-memory ModelOptionsCache module is
removed.
- Runner: unified GET /v1/sessions/{id}/model-options (harness-named
routes stay as deprecated aliases until 0.11.0); the claude route waits
briefly on the store's single-flight probe (503-pending past that) and
the codex route writes live listings back to the store.
- Server: model-options loads go unified-first and fall back to the
legacy route on 404; the hosts API forwards the host's error string.
- Deletions: static claude alias table, gateway-discovery artifact
machinery, the configured-union composition, and the host's codex
catalog reconstruction lanes.
- Tests isolate the catalog store per test so suites cannot touch the
developer's real ~/.omnigent cache or boot real harness CLIs.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Confirm model switches through the harness before claiming them
Switching is now ask -> pending -> harness-confirmed on every lane:
- Runner (claude): after typing /model, verify against the statusLine
snapshot the forwarder already polls (10s budget) — expected spellings
come from the session's own catalog rows; a pane that never switches
answers 503 so the server surfaces the swallowed-dialog case instead
of the row silently claiming the pick. A shape with no snapshot stays
unverifiable-but-successful.
- Runner (codex): the awaited thread/settings/update RPC is the
confirmation; a missing Codex bridge now answers 503 instead of a
silent 204, and plan-mode updates re-assert the reported model rather
than a stale override.
- Server: the visible model_change_not_applied notice now carries the
runner's own detail string.
- Web: a transient pendingModelChange marks the ask (spinner beside the
composer chip); the chip keeps the reported model until session.model
confirms, and the not-applied error (or a switch/bind) settles the
indicator.
Also repairs tests/runner/conftest.py's REAL_CLAUDE_LAUNCH_CATALOG
export, which the previous commit's lint autofix stripped after its
consumers had been verified.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Name the true Default on every picker and pin gateway models
- The web's claude lane keeps the catalog's isDefault marker and all
Default rows (new-chat select, its summary line, the session gear)
label through the one shared defaultModelLabel — both harnesses now
read "Default (X)" where X is the model a bare launch actually runs.
- Provider entries' models map (the existing flat tier keys — opus,
sonnet, haiku, fable — beside default) now pins the claude alias
vocabulary: the launch env derives ANTHROPIC_DEFAULT_*_MODEL from the
declared tiers, models.default pins its own family when that family
has no explicit key, and the declared ids become the config's
routable set. Aliases on gateway endpoints resolve inside the
gateway's own catalog instead of falling back to canonical Anthropic
ids the gateway rejects.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Make the live model-flow rig trustworthy end to end
The step-8 before/after pass surfaced six defects in the live CUJ
scaffolding itself; with them fixed the suite is 16/16 green against the
implementation branch and red against main in the documented modes:
- Drop tests/conftest.py's inherited OMNIGENT_DISABLE_CATALOG_LOOKUP for
the rig's spawned server/host — the databricks catalog was empty only
inside the rig.
- Wait for the post-create navigation with page.wait_for_url: the sync
Playwright API pumps events only inside playwright calls, so the old
time.sleep poll read a page.url frozen at the landing route forever.
- Re-read a model dropdown opened during the host's boot-probe warm-up
until rows (or the settled error) appear.
- Resolve a codex session's private CODEX_HOME through the bridge's own
state.json (the dir is named by a runner-generated bridge id).
- Row 17: no Escape after a Radix select pick (it closes the whole gear
modal), pick an effort that differs from the machine's global default,
and poll for persistence while the browser is still open (the save's
model leg holds until the pane confirms, so the effort PATCH is sent
by the page seconds later).
- Snapshot and restore ~/.claude/settings.json around the suite: the
real /model switches run under the real HOME and Claude persists every
switch as the developer's global default.
Also: useHostModelOptions retries with backoff so a picker opened during
the boot-probe warm-up fills in when the single-flight probe completes
instead of pinning the transient error until reopened.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Wait out the boot-probe warm-up when reading Default labels
The codex launch pin now resolves through live Unity-Catalog discovery
(seconds on a cold host), so a landing model label read immediately
after opening the config renders the bare sentinel while the web's
retry loop is still filling the catalog. Give the label the same
warm-up wait the dropdown read already has.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Mark a model ask pending before its PATCH, not after
The PATCH is held open while the runner drives and confirms the switch,
so the harness's session.model report usually arrives before the PATCH
resolves. Setting pendingModelChange from the response overwrote the
report's clear and stranded the spinner until the hygiene timer. The ask
is now marked pending up front (and cleared if the PATCH throws); a
store test pins the report-beats-PATCH ordering.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Type full model ids verbatim on unpinned claude sessions
Picking a full-id catalog row (e.g. the appended default
'Opus 4.8 (1M context)') stepped down to '/model opus' — the family
alias resolves to claude's CURRENT generation, silently switching to
Opus 5 instead. The confirm layer caught and surfaced it; the
translation now passes claude-* full ids verbatim on envs with no alias
pins (claude's /model accepts full ids — the probe resolves them the
same way), while pinned envs keep exact-pin-or-fail-loud.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Defer mid-turn model switches instead of failing them
A /model typed during an active turn queues in Claude's composer and
applies when the turn settles — past the 10s confirm window — so the
runner surfaced a false 'was not switched' error for a switch still on
its way, and the injection's short dialog watch could leave the late
confirm dialog parked on the pane.
The confirm loop now answers the switch dialog whenever it renders
inside the window, and a timeout with the pane mid-turn answers success:
a detached watcher keeps answering the late dialog (hint-matched Enter
only, never blind; bounded budget) and the forwarder's verbatim report
settles the picker when the switch lands. An idle-pane timeout — the
genuine swallowed case — still fails loud.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Mark a provider launch pin as the claude catalog default
Default launches on provider-configured shapes pass --model
<config.model> explicitly, so the pin — not the enumeration run's own
model — is what a Default launch actually runs. The gateway-entry shape
(one pinned alias row) went unmarked when the enumeration reported no
default, leaving the picker on a bare 'Default'. Subscription shapes
keep the enumeration-derived marker, and the appended default row only
borrows the probe's printed label when it names the same model.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Merge origin/main and green the CI suite
The merge brought main's error-pill restyle, per-frame create refactor,
and codex live UC discovery alongside this branch's model-flow work.
Fixes to keep every suite green:
- Restore the SimpleNamespace import main added to tests/host/test_connect
(the merge dropped it → ruff F821 + 2 NameErrors).
- Regenerate openapi.json for the reported_model wording (session.model
event + llm_model field descriptions).
- Update the smart-routing-create catalog tests to expect the unified
/model-options route the server now asks first (legacy alias is the
404 fallback).
- Update the runner pending-catalog test: a provider shape's launch pin
is appended as the marked default row.
- Adapt row15's e2e to main's collapsed error pill (expand to read the
detail); move the routed-modal test's seed to llm_model (routed models
arrive as the harness report now); pick codex landing options by their
decorated display name (codex options now render display names like
claude — the design's decorated-rows contract).
- Seed the in-session gear from the session's request only before any
harness report exists, so a routed session names its model.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Make row15's error-pill expand retry-safe under suite load
The single headline click could land before the disclosure handler was
wired when the suite ran the pill under load, leaving the detail
collapsed and the assertion timing out. Retry the expand until the
detail shows — same as a person clicking again.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Null-guard the native model-id fold so cursor rows don't blank the page
findNativeModelOption's fold fallback (added when the codex catalog fold
moved client-side) called comparableModelId on option.model/id without a
null guard. Cursor picker rows arrive as { id, displayName } with
model === null on the wire (typed model?: string), and the
option.model !== undefined check let null through — comparableModelId(null)
then threw 'Cannot read properties of null (reading trim)' during render,
blanking the whole chat page for any cursor-native session.
comparableModelId is now null-safe (empty fold never matches a real
target) and the fallback rejects null ids/models. Regression test covers
a cursor-shaped options list with null models.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* Add a per-harness e2e_ui render-smoke matrix (all native pickers)
One hermetic case per native model-picker harness (claude, codex, cursor,
kiro, opencode, pi): shape a seeded session as that harness with its
realistic model_options — including rows with an explicit model: null
(cursor/kiro/opencode's real wire shape, typed model?: string) and a
hostile null-id row — then render the session, open the gear, and assert
the composer renders, the model control lists the rows, and no uncaught
null-deref fires.
The null-model harness cases carry a non-matching model_override so the
model-id fold actually runs (an exact-id match would return before it),
which is precisely the path that once blanked the page. Validated red on
the pre-fix bundle (cursor/kiro/opencode crash) and green after — the
coverage the earlier per-harness tests missed by using model-omitted
(undefined) rows instead of the null shape.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
`omnigent login` to a Databricks-fronted server ran `databricks auth login --host <ws>` with no profile, so each workspace overwrote the shared DEFAULT entry in ~/.databrickscfg. Pass `--profile <first DNS label>` so distinct workspaces keep distinct profiles and DEFAULT is left alone. The OAuth grant stays host-keyed, so token resolution by host is unchanged.
Signed-off-by: Mark Tai <marktai@users.noreply.github.com>
Co-authored-by: Mark Tai <marktai@users.noreply.github.com>
The queued-message strip and sub-agent tray dock onto the composer card with a
-mb-4 negative margin that tucks their translucent bottom behind the opaque
card. BackgroundTaskPill was rendered between those trays and the card, so the
tuck landed on the pill's transparent wrapper instead: the pill's height lifted
the "Steer" strip well off the composer and left it visually detached. Render
the pill above the trays so they dock onto the card again and the pill floats
above as its own chip.
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
Route Cmd/Ctrl+N through the same new-session action as the command palette, and make Electron keep that navigation in the focused window instead of opening another app window.
Signed-off-by: Randy 🌞 <randypitcherii@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
N/A
- Backfill a previously saved server into the recent-server list after its cold load succeeds, so existing installations do not show an empty landing-page picker.
- Normalize user-entered server URLs to their origin before navigation and persistence, removing paths, queries, and fragments.
- Expand detected Databricks workspaces to the canonical `/omnigent` UI mount shared by the native shells.
ELI5: the desktop now remembers the server it successfully opened and stores the server address instead of whichever page was pasted.
```text
pasted URL -> origin normalization -> workspace discovery -> successful load -> recents
```
- `node --test web/electron/test/url.test.js web/electron/test/main.test.js`
- `uv run --no-sync pytest tests/e2e_ui/desktop/test_setup_connect.py::test_shared_url_module_defaults_scheme_in_browser`
- `pre-commit run --files tests/e2e_ui/desktop/test_setup_connect.py web/electron/README.md web/electron/src/deepLink.js web/electron/src/main.js web/electron/src/url.js web/electron/test/main.test.js web/electron/test/url.test.js web/android/README.md web/android/app/src/main/java/ai/omnigent/android/WorkspaceChromeScript.kt web/ios/Omnigent/WorkspaceURLExpander.swift web/ios/Omnigent/WorkspaceChromeScript.swift`
N/A — behavior-only change with no layout updates.
- [x] Bug fix
- [ ] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [x] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
Regression tests cover cold-load backfilling, origin-only URL normalization, and Databricks expansion to `/omnigent`.
Recent servers now appear reliably in the Electron connection screen, with pasted URLs normalized to the server origin.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Stop breaking tool-call replay on newer OpenAI reasoning models while retaining an explicit compatibility override for legacy providers.
Signed-off-by: Thomas Garnier <mxatone@gmail.com>
* fix(triage): assign an owner to duplicates left open
The issue-triage workflow classified some issues as duplicates but, with
duplicate auto-closing disabled by default, left them open. The duplicate
branch hit `exit 0` before the assignment step, so those open duplicates
were never routed to an owner — even P0/P1 ones. #4437 and #4484 both landed
this way (P1-high, triaged, unassigned).
Only exit early when the duplicate is actually closed. When closure is
disabled and the issue stays open, fall through to the normal maintainer-
author / least-loaded-owner assignment path so it gets an owner like any
other triaged open issue.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(triage): assign every triaged issue before closing duplicates
Reworks the earlier fix per review: instead of only routing duplicates
left open, assign an owner to every triaged open issue FIRST, then run
the optional duplicate closure. Closed duplicates now get an owner too,
and because it persists across a close/reopen, a reopened issue no longer
comes back unassigned (triage only fires on `opened`, not reopened).
Drop the now-redundant per-mutation OPEN re-checks on the assignment path;
the closure step keeps its guard against a concurrent human close.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(server): only fail a session when a runner drop interrupts a turn
The per-session relay failed a session on any tunnel close, but its stream
stays subscribed while the session is idle. A host going away (sleep,
restart, `omnigent host` stopped) therefore lit a red "connection to the
host dropped" banner over every bound session that had simply finished its
last turn.
Apply the mid-turn gate that `_mark_runner_sessions_offline_impl` already
used, shared as `_MID_TURN_STATUSES`. An idle drop now stays silent — no
status edge and no label clearing, so an earlier genuine failure keeps its
error — and surfaces through liveness, which already drives the reconnect
affordance independently of session status.
The mid-turn check falls back to the durable `live_status` when the
per-replica status cache is cold, so a restart mid-turn does not downgrade
a real interruption to a benign one.
Also fixes a pre-existing race in the tunnel reconciliation test, which
waited on the status flip that precedes the labels it then asserted.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(server): guard the cold-cache live-status read in the relay disconnect path
Address AI review on #4776: the durable fallback read ran unguarded inside
the disconnect handler, so a store error would escape and kill the relay
before either branch published a status. Treat an unreadable or missing row
as interrupted, and gate the scripted test frames behind the collector so
the relayed-event assertions are not scheduler-dependent.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
Follow-up to #5069, which added the +N model badge to the usage sessions
table with manual verification only. UsageSessionTable had no test file;
this adds render tests for single-model (no badge), multi-model (primary
+ correct +N count), the badge tooltip ordering (non-primary models by
descending cost), and the no-models empty case.
Co-authored-by: Isaac
* fix(codex): surface gateway auth failures instead of a silent 600s stall
When the codex SDK head can't authenticate to its model gateway, the codex
CLI retries silently and emits no turn events, so the turn stalled to the 600s
idle watchdog and died with a generic "wedged LLM or tool call" message — even
though the real cause (a 401/403 on the CLI's stderr) was in hand the whole
time.
Parse the CLI's ``unexpected status <code> ... url:`` stderr line and:
- Attribute it: record the parsed cause into the shared idle-watchdog failure
slot the scaffold already reads, so a stalled turn surfaces the real gateway
error rather than the generic reason. Adds a general record_transport_failure
alongside the native forwarder's record_post_failure — no scaffold change.
- Fail fast: an auth-class (401/403) rejection arms a fast-fail once the CLI
has also exhausted its own retry budget (a final Reconnecting N/N), so the
turn ends in seconds with a retryable=False ExecutorError naming the status,
model, and url — instead of riding the full idle watchdog. Both signals are
tracked independently (either arrival order), so a blip the CLI recovers from
never kills a healthy turn. The idle wait polls on a shorter interval to act
on the signal promptly; the 600s warn window and its cadence are unchanged.
Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
* fix(codex): reserve auth hint for auth failures
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(codex): interrupt fast-failed auth turns
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(codex): cover gateway auth fast-fail e2e
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
Signed-off-by: dbczumar <corey.zumar@databricks.com>
Co-authored-by: dbczumar <corey.zumar@databricks.com>
When a session uses multiple models, display a +N badge next to the
primary model (highest cost), matching the existing harness pattern.
Hovering the badge shows a tooltip listing the additional models.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
`omnigent host stop` could crash instead of stopping the host when the
local daemon registry held a stale record for a PID it no longer owns.
`_terminate_daemon` only wrapped `os.kill` in
`contextlib.suppress(ProcessLookupError)`. When a daemon record points at
a PID that has since been reused by another user's process (or the daemon
was started under a different account), `_pid_alive` reports it as alive
— psutil maps the permission failure to `AccessDenied`, which
`_pid_alive` treats as "alive" — so we fall through to `os.kill`, which
raises `PermissionError` (EPERM). That is not a `ProcessLookupError`, so
it propagates and crashes the CLI. `--force` didn't help: the crash
happens before the SIGKILL path.
Add `_signal_daemon_pid(record, sig)`, which signals the recorded PID and
classifies the result:
- `PermissionError` → the PID is not our daemon (stale record). Warn and
treat the record as stale — drop it instead of killing an unrelated
process or crashing.
- `ProcessLookupError` → the process already exited; stale record.
- otherwise → the signal was delivered; termination proceeds as before.
`_terminate_daemon` now uses it at both the SIGTERM and SIGKILL sites.
`_pid_alive` is deliberately unchanged — treating a foreign PID as
"alive" is the correct conservative choice for its other callers
(port/daemon reuse detection).
Tickets: OMNI-3473 (primary), OMNI-3196.
Co-authored-by: Isaac
Co-authored-by: harry-yao_data <harry.yao@databricks.com>
Every other harness family lets a spec opt out of per-tool approval prompts via
`executor.config.permission_mode` — claude-sdk (`HARNESS_CLAUDE_SDK_PERMISSION_MODE`),
cursor, claude-native, antigravity-native. Generic ACP honored no such mode, so
it asked on every `session/request_permission` no matter how the agent was
configured. A headless ACP worker — a polly sub-agent, a scheduled task,
`omni run -p` — then parks on a card nobody is watching, and there was no
supported way to lower approval volume.
`AcpAgentConfig` gains `permission_mode`, the wrap reads
`HARNESS_ACP_PERMISSION_MODE`, and both ACP spawn-env builders forward it — the
builtin-row builder too, so the builtin `devin` the picker offers behaves like a
self-registered `acp:devin`.
Deliberately narrow:
- Only `bypassPermissions` waives the card. `auto` is the default when unset, so
treating it as no-prompt (as cursor does) would silently stop every existing
ACP agent from asking; an unrecognized value prompts.
- Policy runs in every mode, matching claude-sdk's gate: a DENY still blocks and
an explicit ASK still prompts. Bypass waives the human gate, not the user's
rules.
- Bypass answers each request individually and never selects the agent's own
`switch_bypass` option, which would end the request stream and take the
agent's tool calls out of policy's view.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
An ACP agent can offer several differently-scoped ways to say yes. Devin sends
six options for one shell command — allow once, allow this command class for the
session, always in this project, always everywhere, switch to bypass, reject —
and we collapsed them to Approve/Reject and always answered `allow_once`. That is
the right default, but it means the agent is told "just this once" every time and
re-asks for the same command class forever: a dogfooder hit 50+ prompts in five
minutes on one small task.
Pass the agent's options through to the approval card and reply with the one the
user picked, so a scope the agent already supports ("allow `ls` this session") is
one click and the agent itself stops asking. The card already renders an `answer`
enum as one button per choice and replies with the chosen label, so this is a
backend change only.
- `_decide_permission` returns `(allowed, option_id)`; `_permission_outcome`
echoes that option only after confirming the agent offered it, and otherwise
still prefers the narrowest grant. A blanket "always allow" is now only ever
sent because the user chose it by name.
- The choice card is skipped unless the agent offers a `reject_*` option — it
replaces the Approve/Reject buttons, so without one the user could not say no —
and unless every label is distinct, since the reply names the label.
- The adapter installs the richer bridge only on executors that declare the
attribute, leaving the SDK harnesses on the binary card.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* feat(claude-native): propagate an in-terminal /rename to the Omnigent UI
Several in-terminal state changes already mirror to Omnigent so the web
view doesn't go stale — a `/model` switch posts `external_model_change`,
thinking-level switches post `external_reasoning_effort_change`. Session
title was the gap: a `/rename` typed in the Claude Code pane appends a
`custom-title` record to the JSONL transcript, but the reader explicitly
skipped title metadata, so the web session list kept showing the stale
auto-generated first-message title and the two silently diverged.
Read that record and mirror it, following the `external_model_change`
path end to end: the bridge surfaces it on
`TranscriptReadResult.latest_custom_title`, the forwarder posts a new
`external_session_title` event, and the server persists `title` and
publishes a `session.title` SSE event so the sidebar updates without a
reload. A session renamed while it is not the open session converges via
the existing `WS /v1/sessions/updates` diff, which already carries title.
Two behaviors worth calling out:
- The rename is authoritative — it overwrites a title set in the web UI,
so it uses a plain `update_conversation` rather than the seed-only
compare-and-swap behind `/auto-title` (which exists to stop an
*automatic* titler from clobbering a human's name).
- Only the explicit `customTitle` propagates. Claude's generated
`aiTitle` is ignored because Omnigent runs its own background titler
and two auto-titlers would fight over one field.
Declined for child sessions, whose `"<agent>:<label>"` titles are
structural and get parsed back apart by the sub-agent tooling. That also
covers the legacy `:closed:` marker, which can only land on a child row —
both writers reject a non-sub-agent title — so no separate guard for it.
The forwarder dedupes by last-posted title. A steady-state poll reads
only past its byte cursor and never re-sees the record, so the dedupe is
there for the cursor rewind / restart path and to make the retry safe to
attempt on every poll; `observed_title` is sticky so a retry survives
polls whose own window carries no rename.
The web-side cache overlay is extracted from `useRenameConversation` into
a shared `overlayTitleIntoCaches` so a terminal rename and a web rename
patch exactly the same caches.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
* test(e2e-ui): cover terminal /rename streaming to the session sidebar
The E2E UI Required gate flagged that the terminal-originated rename flow
this PR adds had web/** behavior changes with no tests/e2e_ui/** coverage
of the renamed title appearing in the sidebar.
Add a Playwright test that posts the exact external_session_title event
the claude-native forwarder sends to POST /v1/sessions/{id}/events, then
asserts the open tab re-titles the sidebar row live via the session.title
SSE push (no reload, no list refetch) and that the server persisted it.
This exercises the new server publish path and the web session_title
handler end to end, a different seam from the existing PATCH -> WS updates
rename test.
Co-authored-by: Isaac
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
---------
Signed-off-by: harry-yao_data <harry.yao@databricks.com>
Co-authored-by: harry-yao_data <harry.yao@databricks.com>
Every event posted to POST /v1/sessions/{id}/events runs an access-control
check before it does anything else, and that check's reads dominate at scale.
For a session whose permissions/metadata are stable for the turn, each streamed
chunk re-pays: resolve_access (session_permissions + users) plus
get_conversation, which internally reads the conversation row + labels in one
pool checkout and the metadata row in a separate one. That is three distinct
checkouts, each with a pool_pre_ping liveness round-trip, per event — the bulk
of the ~16 queries / ~10 checkouts per event measured in #3004.
shared_read_scope() (db/utils.py): a read-only, per-request scope in which
managed_session() reuses one session per engine instead of opening a fresh
checkout on every store call. Wrapped around the access-control burst, so the
permission + conversation + metadata reads collapse to a single checkout
(single-DB); split-DB deployments keep independent checkouts per engine. Write
makers (immediate=True) never participate, so BEGIN IMMEDIATE isolation is
untouched, and the scope is a strict no-op everywhere it is not activated. The
scope is deliberately kept off any path that spans runner network I/O so it
never pins a pooled connection across a multi-second turn.
No ACL semantics change: the same rows are read, just once per request over one
connection. Verified by a new checkout-counting test that the access-control
burst issues exactly one pool checkout, plus unit tests for the scope's reuse,
nesting, write-maker bypass, per-engine keying, and cleanup.
Closes#3004
Co-authored-by: Isaac
Register the scope's session before its SQLite PRAGMAs run: those executes
force the pool checkout, so a failure there must leave the session tracked by
the scope's cleanup — otherwise the checked-out connection would leak.
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
The per-event access-control check re-reads session_permissions + users on
every streamed event, for a session whose grants are stable across the turn —
a large slice of the per-event DB load on POST /v1/sessions/{id}/events.
Cache resolve_access() results with a short TTL (default 5s,
OMNIGENT_ACL_RESOLVE_CACHE_TTL_S; 0 disables). The cache is:
- Positive-only: a no-access result is never cached, so a freshly granted user
is authorized on their next request rather than after the TTL — sharing takes
effect immediately, no grant-latency.
- Evicted on every write that changes a decision on this store: grant/revoke
(per session, which also covers the shared __public__ grant), and
reassign_user_grants / set_admin (whole cache). A generation counter, sampled
before the read and re-checked under the lock at store time, stops an
in-flight reader from re-storing a pre-commit positive on top of that
eviction — so once such a write returns, this instance serves no stale
decision.
- Keyed by (conversation_id, user_id): conversation ids are globally unique, so
eviction needs no workspace context.
- Bounded by an LRU entry cap (default 50k, OMNIGENT_ACL_RESOLVE_CACHE_MAX_ENTRIES)
so a long-lived replica cannot grow it without limit — resolve_access is on
the snapshot path too, so one-shot sessions would otherwise linger forever.
- Per store instance (per replica): conversation reads are left untouched and
always fresh, since conv fields (runner_id, labels) change mid-turn.
Staleness is bounded and one-directional: grants/raises take effect
immediately; only revoke/downgrade can lag, and only up to the TTL. Role
changes made through the separate accounts store (admin demote, user delete)
are not evicted here and propagate within the TTL. Across replicas there is no
invalidation broadcast, so a revoke can be up to the TTL late on other
replicas; that window is LEVEL_EDIT only — the destructive stop/kill path
re-gates LEVEL_OWNER separately, and a deleted session still 404s on its
uncached conversation read.
The authenticated policy-evaluate route's SQL budget drops 11 -> 8: its warm-up
request also warms this cache, so the steady-state route serves the access
decision from memory. That is the production hot path, since the check re-runs
on every event of a turn; a cold process still pays those 3 reads.
Composes with the access-control checkout collapse (#4737): that made the read
burst one checkout; this removes the permission reads entirely on a cache hit.
Part of #3004 (follow-up to the access-control checkout collapse in #4737).
Co-authored-by: Isaac
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* fix(web): validate custom date range on the usage page
Prevent users from selecting future dates or an inverted range
(start after end) in the custom date picker. Both inputs now carry
min/max constraints and auto-correct the paired value on change.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* test(web): add e2e_ui tests for usage page date range validation
Cover the new min/max constraints and auto-correction behavior
with Playwright tests so the E2E UI Required gate passes.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* style: fix formatting in usage page e2e test
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix: use timezone-aware datetime in usage page e2e test
Replace date.today() with datetime.now(tz=timezone.utc).date() to
satisfy the ruff DTZ011 rule.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
PolicyRegisteredEvent was defined and called in both policy routes but
had zero test coverage. Add three test groups:
1. _build_record serialisation (tests/test_telemetry.py): verify that
admin-scope (session_id=None) and session-scope events produce the
correct wire format — promoted top-level fields vs. params content.
2. Default-policy route emit (tests/server/routes/test_default_policies.py):
POST /v1/policies fires exactly one PolicyRegisteredEvent with
scope='admin'; a 409 conflict does not emit.
3. Session-policy route emit (tests/server/routes/test_session_policies_crud.py):
POST /v1/sessions/{id}/policies fires exactly one PolicyRegisteredEvent
with scope='session' and the correct session_id; a 409 conflict does not emit.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
On macOS sleep the OS freezes every Omnigent process and drops the network,
killing the host control-channel WebSocket and every runner (session) tunnel.
On wake nothing reconnected promptly: the only liveness signal was the
websockets keepalive ping (30s interval / 90s timeout), so a half-open
post-sleep socket took up to ~120s to be noticed — and the server had already
deregistered the host, so the desktop app showed "disconnected" that whole
time while the terminal (a one-time startup banner) still read "connected".
Add omnigent/suspend_watch.py: watch_for_resume() detects a resume by polling
a short interval and comparing wall-clock vs monotonic-clock drift. The
monotonic clock freezes during sleep on macOS/Linux while the realtime clock
keeps counting, so a resume shows up as a large divergence; a merely-blocked
event loop advances both equally, so this never false-fires on CPU stalls.
Uses time.monotonic (never loop.time, which under uvloop includes sleep on
macOS and would zero out the divergence).
Wire it into both reconnect loops:
- Host (connect.py): a watcher aborts the live tunnel (ws.transport.abort())
on wake and flags a prompt reconnect, so run() reattaches at the base
backoff instead of the escalated one (required on a loopback server, where
an abrupt close is not auto-classified as a benign recycle).
- Runner (serve.py): a per-connection watcher aborts the tunnel and notes the
resume so serve_tunnel reconnects promptly; each session self-heals on its
own event loop, so no host orchestration is needed.
Result: opening the laptop reconnects the host and all its sessions within
~5s instead of up to ~2 minutes. Windows degrades to todays keepalive
behavior (its monotonic clock counts suspend) with no regression.
Tests: unit tests for the detector (fires once on divergence, never on a
blocked loop, survives a raising callback) plus host and runner integration
tests (a simulated wake aborts the live tunnel and forces a prompt reconnect).
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
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>
* 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>
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>
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>
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>
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>
* 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>
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>
* 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>
* 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
## 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>
* 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>
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>
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>
* 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>
#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>
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>
* 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>
* 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>
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>
* 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>
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>
* 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>
* 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>
* 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>
- 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>
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>
* 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>
## 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>
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>
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>
## 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>
## 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>
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>
* 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>
* feat(web): let bulk session delete clean up worktree branches
Selecting multiple sessions and hitting delete previously showed a dead-end
warning ("Branches are not cleaned up. Use single-session delete for branch
surgery."). Since the bulk delete already fires N independent DELETE requests,
we can offer the same per-branch cleanup the single-session flow has.
The confirm modal now lists the local git branch of each selected worktree
session with a checkbox (default unchecked, since branch deletion is
irreversible) plus a Select all / Clear all toggle. Each ticked branch rides
along as ?delete_branch=true on that session's own DELETE. Sessions without a
worktree contribute no checkbox, and the list is hidden entirely when nothing
in the selection has a branch.
No server change: DELETE /v1/sessions/{id}?delete_branch=true already applies
per session. git_branch is already on each list-sourced conversation, so no
extra fetch is needed.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* fix(web): widen bulk-delete modal, stack Select all below warning
The branch checkbox list rendered in the default narrow dialog (sm:max-w-sm),
and the Select all toggle sat inline with the warning text, compressing it.
Widen the modal to sm:max-w-lg and move the toggle onto its own line below the
warning.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* fix(web): outline the Select all toggle, loosen branch-list spacing
The ghost-variant toggle had no border, so it read as oddly indented text
rather than a button — switch it to the outline variant. Bump the checkbox
list from gap-1 to gap-3 (and raise the scroll cap to max-h-56) so the
two-line branch/title rows no longer feel cramped.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* feat(web): table-style branch picker with tri-state header checkbox
Experiment: replace the list + "Select all" button with a table (Branch /
Session columns). The button becomes a header checkbox that reflects the row
selection — unchecked when none are ticked, indeterminate ([-]) for a partial
selection, checked when all are — and toggling it selects or clears every row.
Reverting to the list layout is a matter of resetting to the prior commit.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* test(web): mock useLeaveSession in bulk-delete-branch test
main added useLeaveSession to ConversationRow (#4571); the new bulk-delete
branch test mocks @/hooks/useConversations wholesale, so it must export it too.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
---------
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
## Related issue
N/A
## Summary
- Fix omnidev's Vite command after the pnpm migration: pnpm forwards script arguments directly, so the retained npm-style `--` caused Vite to ignore the configured host, port, and strict-port flag.
- Remove the separator, assert the complete forwarded argument list in the unit test, and correct the omnidev documentation.
## Test Plan
- `cargo test --manifest-path dev/omnidev/Cargo.toml process::tests::vite_forwards_configured_host_and_port_but_backend_url_stays_loopback`
- `cargo fmt --manifest-path dev/omnidev/Cargo.toml -- --check`
- `git diff --check`
- Manually ran `pnpm run dev --host 127.0.0.1 --port 43220 --strictPort` from `web/` and confirmed Vite bound to port 43220.
## Demo
N/A — this fixes local development process arguments and has no visual UI change.
## 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
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
The unit test now verifies pnpm receives the configured Vite host and port without an npm-style separator. A direct pnpm/Vite run confirmed the corrected command binds to the requested port.
## Changelog
`omnidev --vite-port` once again starts the frontend on the requested port.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
N/A
## Summary
The published `dev` extra mixed repository workflows with installable Omnigent capabilities. Move contributor-only dependencies to PEP 735 groups so package extras describe product functionality and CI installs only the workflow dependencies it executes.
- Replace the `dev` extra with local-only `lint`, `test`, and aggregate `dev` groups; configure no default groups so plain `uv sync` matches the published base package.
- Remove the retired mypy dependency/configuration, `types-PyYAML`, the orphaned `pathspec` declaration, and the duplicate `filelock` declaration.
- Migrate workflows, actions, contributor commands, tests, and development skills from `--extra dev` to the smallest required group, or no group for application/benchmark jobs.
- Compose Pyrefly's lint environment from the `lint` group plus the existing `hindsight`, `nimble`, `s3`, and `tracing` capability extras. Remove the OpenTelemetry missing-import configuration and Nimble's inline missing-import suppression so real package types remain checked.
- Update OpenShell, e2e, browser-test, Slack, and implementation-plan commands to compose capability extras with repository groups explicitly. Document why read-only/tools-less agent workflows intentionally keep runtime-only environments.
- Avoid `--all-extras`: it resolves but selects 240 product packages, including unrelated large/native integrations. Keep capability ownership explicit instead.
ELI5: product features remain extras users can install; lint and test toolboxes become private repository groups that never appear in the wheel.
```text
published wheel: base + capability extras
repository: lint group | test group | dev = lint + test
CI lint: lint + explicitly type-checked capability extras
```
## Test Plan
- `uv lock && just normalize-locks`
- Built the wheel and verified its metadata contains no `dev` extra or lint/test dependencies.
- Verified a fresh base environment imports Omnigent, excludes lint/test/pathspec packages, and imports each release benchmark script.
- `uv run --isolated --frozen --group lint --extra hindsight --extra nimble --extra s3 --extra tracing pre-commit run pyrefly --all-files`
- `uv run --isolated --frozen --group lint python scripts/gen_routing_pb2.py --check`
- Verified isolated `test` and aggregate `dev` group membership independently.
- `uv run --isolated --frozen --group test pytest tests/tools/builtins/test_hindsight.py tests/tools/builtins/test_nimble_research.py tests/stores/test_s3_artifact_store.py tests/db/test_d1_fts_dialect.py -q` (172 passed)
- `uv run --isolated --frozen --group test --extra tracing pytest tests/runtime/test_telemetry.py tests/inner/test_tracing_genai_semconv.py -q` (69 passed)
- `uv run --isolated --frozen --extra openshell --group test pytest tests/onboarding/sandboxes/test_openshell.py tests/server/test_managed_hosts.py -q` (259 passed)
- Verified load-test modules import with only `loadtest` and `agents-sdk` extras.
- Ran the exact locked lint sync against PyPI and Pyrefly passed.
- Rebased onto current `origin/main`; migrated the newly added compatibility-smoke test actions and host benchmark workflow.
- Surveyed all tracked uv install/run commands and removed every remaining published-`dev`/implicit-tooling command. Verified the documented e2e and Slack environments and collected the Kimi/live-DDG tests in fresh group-selected environments.
- `uv run --frozen pre-commit run --all-files`
## Demo
N/A — dependency metadata and CI configuration only.
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Fresh isolated environments validated the base, lint, test, aggregate dev, tracing-test, and load-test dependency boundaries. Focused tests prove retained optional clients are genuine test runtimes, while wheel inspection proves repository groups are not published.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
@@ -5,6 +5,183 @@ generated at release time from each PR's `## Changelog` section, tagged by the
PR's `Type of change` (e.g. `[UI]`); the concise, curated highlights live on the
website under `/releases`.
## [v0.10.0] — 2026-08-19
- [Bug fix / Test/CI] Host daemons now honor standard proxy environment variables without forwarding (#1029)
- [UI / Feature] Route host- and session-scoped server requests to the replica holding the host's tunnel, and signal `wrong_replica` (HTTP 400 / WS 4400) so clients re-address on a miss. (#2037)
- [Bug fix] Keep `serve-mcp` responsive to pings and additional requests during slow tool calls. (#2813)
- [UI / Feature / Docs / Test/CI] White-label the web UI from server config with custom names, headings, safe logo assets, favicon, and optional Omnigent attribution. (#2857)
- [Bug fix] A transient server hiccup no longer pins a session to the wrong working directory for the rest of the conversation. (#3017)
- [Bug fix] Claude SDK agents now discover large MCP tool definitions on demand instead of loading every schema up front. (#3134)
- [Bug fix / Docs] Sub-agents no longer inherit their parent's bundle directory (skills, local tools); resolvable children use their own, while unresolvable children never fall back to the parent's (#3567)
- [UI / Bug fix] Native-harness sessions no longer leave a stale duplicate of your message (or of the assistant's reply) pinned to the bottom of the web transcript. (#3595)
- [Feature] GitHub policy blocks tag pushes (`--tags` / `--follow-tags` / `refs/tags/` refspecs) by default; opt out with `deny_tag_push: false`. (#3620)
- [Bug fix] Server URLs and log paths in `omni host status` are now proper clickable links instead of text the terminal has to guess at (#3862)
- [UI / Bug fix / Feature] agy sessions now mirror tool calls and sub-agents into the web UI, and no longer duplicate or truncate replies (#3890)
- [Bug fix] `force_sandbox` (and any declared `os_env.sandbox`) now actually applies to a Claude Code native session's file/shell tools, not just the terminal process (#3910)
- [Bug fix] Native sessions no longer fail with "terminal failed to start" when the host daemon was launched from a directory that has since been deleted (#3974)
- [UI / Feature] The server can offer several sandbox providers at once (`sandbox.providers`), and the new-session picker lists one option per provider (#4006)
- [Bug fix / Feature / Chore] Switching between conversations is instant — background conversations stay connected, so returning to one shows messages that arrived while you were away (#4113)
- [Bug fix] `omnigent pi` now uses each model's real context window and output limit, instead of compacting early and truncating long replies on high-context models. (#4178)
- [Feature] Route a host's tunnel, its runners, and its session traffic to one replica so multi-replica deployments keep host-scoped requests sticky. (#4185)
- [Bug fix] A custom agent that declares its own provider auth now launches on codex-native instead of stalling on the Codex sign-in screen. (#4208)
- [Bug fix] `omnigent server --host 0.0.0.0` with `OMNIGENT_LOCAL_SINGLE_USER=1` no longer 401s every request and 403s the host tunnel; the single-user marker is honored on network-exposed binds, with a warning that the server serves unauthenticated requests (#4224)
- [UI / Bug fix] New chats in a project with a default base branch now fork a fresh branch off that default instead of reopening your last-used worktree (#4229)
- [UI] New-chat landing header uses tighter Otto sizing and responsive headline scale (#4233)
- [Bug fix] Changing effort or model mid-conversation on a Claude Code session no longer risks wedging the terminal or silently keeping the old setting. (#4250)
- [Bug fix] `omnigent chat <remote-url>` can now start a new conversation with an agent registered on the remote server (#4260)
- [UI / Bug fix] Collapsed "Worked for …" rows in a chat transcript now sit at an even spacing and draw their full-width divider (#4284)
- [UI / Bug fix] Voice dictation now inserts text at your cursor instead of appending it to the end of the composer (#4290)
- [UI / Feature] The file panel can now browse anywhere the session can reach, not just the folder it started in (#4306)
- [UI / Bug fix] Navigating to another session while a new one is still being created no longer yanks you into the new session when it finishes (#4307)
- [UI / Bug fix] New polly and debby sessions now show the same "Starting up…" spinner as claude-code, instead of a "Connecting…" row under the composer (#4312)
- [Bug fix] Duplicate-detection comments no longer ask you to close your issue when the matching issue is already closed — an already-fixed match now asks whether you're on a build with the fix, and treats a still-reproducing report as a regression. (#4313)
- [Bug fix] Kimi and Hermes sessions launch again — their version checks compared against the wrong version series and rejected every current CLI. (#4314)
- [UI / Feature] Organizations can preconfigure Omnigent server URLs through Android managed configuration, and they show up ready to tap in the app's server list. (#4315)
- [Feature] `omnigent host --background` starts the local server and registers this machine as a host without tying up a terminal. (#4317)
- [UI / Breaking] Reverts the shared-session approval-authority and message-attribution features (#2150 stack); session approvals are again available to any shared editor. (#4318)
- [UI] Administrators can preset the iOS app's server URLs with a managed app configuration, so managed users pick their organization's server instead of typing it (#4319)
- [Bug fix] Fixed a bug where an archived session (or an archived sub-agent) could be gated as if it had spent nothing, letting a tool call proceed over its actual budget. (#4320)
- [Feature] `omnigent start` starts the local server and registers this machine as a host — the on switch to go with `omnigent stop`. (#4321)
- [Bug fix / Test/CI] Fix `omnidev` restart loop on Linux caused by non-mutating file access events (#4330)
- [Feature / Docs] Issue triage now explains its impact assessment and priority in one bot comment instead of adding severity labels. (#4334)
- [UI / Bug fix] Opening the left sidebar no longer squeezes the chat below its minimum width — the browser/workspace panel yields instead and restores its width when the sidebar collapses. (#4337)
- [Bug fix] Pi sessions on a Databricks workspace no longer hang when the workspace model list is unavailable — non-Claude models are routed by family, and a model that truly can't be served now says so instead of never replying (#4339)
- [Chore] Files in the viewer load faster — workspace-file reads are now gzipped, cutting a 1 MB text file from ~2.3 s to ~1.3 s (#4341)
- [Bug fix / Chore] A claude-native session no longer shows "Working…" forever when Claude exits (#4344)
- [UI] The sidebar "Needs response" badge now uses the brand accent color (pink by default), matching the unread indicator. (#4346)
- [UI] Refreshed modal styling — larger rounded corners, softer drop shadow, and roomier padding (#4347)
- [Bug fix] Pi sessions on a proxy that exposes both Anthropic and OpenAI surfaces now send each model to the surface its family speaks, instead of routing everything through Anthropic and hanging (#4348)
- [Bug fix] Session snapshots no longer fail when a session contains a malformed legacy agent id. (#4350)
- [Bug fix] A session whose message the runner refuses now reports the failure and its reason instead of appearing to finish successfully. (#4354)
- [UI / Feature] Hover the collapsed sidebar toggle to peek the conversation list without pinning it open. (#4355)
- [Bug fix] Generic-ACP / Goose / Qwen turns that fail now report the exception type instead of a blank "inner executor error: " with no detail. (#4362)
- [UI / Bug fix] Messages sent from the chat UI no longer stop reaching the agent after a dropped network request (#4366)
- [UI / Chore] The Files panel is now split into separate **Files** (folder tree) and **Changes** (changed files) tabs (#4367)
- [UI / Feature] Chat messages now show their timestamp beside the Copy/Fork actions. (#4372)
- [Bug fix] A conversation link copied from your browser now works wherever a server URL is expected, instead of failing later with an opaque "Method Not Allowed" crash (#4374)
- [UI / Bug fix / Test/CI] Clicking a sidebar session that needs a response no longer runs its title under the "Needs response" tag, and the Inbox count badge now matches the sidebar's pink accent (#4375)
- [UI / Bug fix] Maximized workspace panel no longer shows chat content through a transparent background in dark mode. (#4376)
- [Bug fix] Agents now inherit your ssh-agent, so git-over-SSH and SSH-cert-authenticated tooling work in agent shells and terminals (#4377)
- [Bug fix] The sidebar remembers your session filter across reloads instead of resetting to "All sessions" (#4381)
- [Feature / Docs / Test/CI] Blaxel is now available as a sandbox provider for CLI and managed-host deployments. (#4383)
- [UI] The Chat/Terminal switcher is now a segmented toggle — both views are visible at a glance and switching takes one click (#4385)
- [Bug fix / Feature] `omnigent run --server local` runs against a local server, overriding any configured server default — and the no-AGENT `omnigent run --server ""` no longer fails with `Agent path not found: https:` (#4387)
- [UI / Bug fix] Terminal-first sessions return to chat automatically when the runner stops or disconnects, instead of stranding on an empty "No terminals available" terminal view (#4388)
- [Bug fix] The `build-omnigent` skill is available again in native `omnigent claude` and `omnigent codex` sessions (#4391)
- [Bug fix] A custom ACP agent can declare the environment variables it authenticates with via `env_passthrough`, and a stalled ACP handshake now reports which call timed out instead of failing with an empty message. (#4392)
- [Bug fix / Feature] The Copilot harness now authenticates with your existing `gh auth login` session, and a GitHub Enterprise host can be set via `omnigent setup` (#4396)
- [Bug fix] MCP server configs can now use `${VAR}` placeholders in the `url` field, not just in `headers` — so a config can be committed to version control without hardcoding the endpoint. (#4398)
- [Bug fix] `kimi-native` sub-agents honor `executor.config.yolo: true` (launching `kimi --yolo`) and `antigravity-native` sub-agents honor `permission_mode: bypassPermissions`, so server-spawned workers no longer stall on interactive approval prompts. (#4401)
- [Bug fix] Resuming a claude-native session no longer drops a message sent right after the session starts. (#4403)
- [Bug fix] A sub-agent session no longer logs a spurious "did not resolve in the parent spec" warning on every turn. (#4435)
- [UI / Bug fix] Bulk-select sessions and move them to a project in one action via the new folder icon in the selection bar. (#4452)
- [UI] Codex's bypass-approvals option now matches Claude's clean permission UX — no more red warning banners (#4467)
- [Bug fix] Compaction snapshots no longer store raw image data, which cuts the size of newly written compacted conversation rows substantially. (#4470)
- [UI / Bug fix] The new-session workspace picker now navigates to `~/…` paths and shows a clear error when a typed path doesn't exist (#4480)
- [UI / Feature] Harness launch failures now show a clear title, cause, and suggested fix instead of a raw error code and truncated log tail. (#4485)
- [UI / Feature] Sub-agents are now auto-assigned readable structured names (e.g. `researcher-1`) and a task-derived display label in the Agents panel (#4489)
- [UI] Restored the down chevrons on the new-session composer's chips and made every dropdown trigger show a pointer cursor (#4493)
- [UI / Bug fix] Modal dialogs and the workspace "Open new" menu no longer render behind the embedded browser pane (#4500)
- [Bug fix] Named `sys_session_send` no longer returns a spurious 404 on the second and later sub-agent sends from a bundled agent (#4501)
- [Bug fix] Session search no longer hangs on "Searching…" — content search is now backed by a trigram index and bounded by a timeout. (#4502)
- [Bug fix / Test/CI] Fixes an HTTP client resource leak when OpenAI Agents SDK executors shut down. (#4508)
- [Bug fix] Sessions ride out brief runner-tunnel drops (laptop sleep-wake, ingress recycles) without flashing Failed or truncating the streaming reply. (#4516)
- [Bug fix] `omnigent` no longer crashes on startup when your shell sets a SOCKS proxy (e.g. `ALL_PROXY=socks5://…`) and the `httpx[socks]` extra isn't installed (#4517)
- [UI / Bug fix] Attaching an unsupported file in a new chat now tells you why up front and keeps your message instead of losing it (#4519)
- [Bug fix] `omni` now works on machines with an HTTP proxy configured, and reports an unreachable server as a clear error instead of a crash. (#4520)
- [Bug fix] Sessions that outlive their 60-minute runner bearer no longer permanently lose runner→server auth when the token re-mint is rejected — the runner now falls back to the machine's SDK/OIDC credential. (#4521)
- [Bug fix] Harness logs now go to a file under `~/.omnigent/logs/harness/` (or the runner's log), and a failing ACP turn quotes the agent's own error output instead of dropping it. (#4523)
- [Bug fix] An `openai-agents` agent with no pinned model no longer fails with a confusing "install databricks-sdk" error when only OpenAI credentials are missing (#4526)
- [Bug fix] Claude native sessions now report per-turn token usage (`gen_ai.usage.input_tokens` / `output_tokens`) to MLflow and other OpenTelemetry backends. (#4530)
- [UI / Bug fix / Feature] Move a running session to another machine from the host badge in the composer. (#4531)
- [Bug fix] Upgrading omnigent in place no longer breaks harness launches on already-running runners (#4539)
- [Chore] The session-search index migration builds its Postgres indexes concurrently, so upgrades no longer block writes while the indexes build. (#4541)
- [Feature] The Android app now opens Databricks workspaces on their `/omnigent` app instead of the workspace landing page. (#4543)
- [Bug fix] `omnigent host` pointed at a local server that has exited now stops after ~5 minutes with a clear error instead of reconnecting forever. (#4544)
- [Bug fix] Runners no longer crash-loop at session start when signal-handler registration fails; they log one warning and keep working. (#4545)
- [Bug fix] Session search now returns results instead of timing out on large workspaces. (#4546)
- [UI / Bug fix] Modal buttons like Stop session and Clone now show a spinner while the action is running, instead of just greying out. (#4548)
- [UI / Bug fix] The desktop server picker moved from the window title bar to the bottom of the sidebar, fixing an overlap with the chat header on narrow windows — and Windows and Linux desktop now have it too (#4551)
- [UI] The embedded terminal connects in the background and stays connected across Chat/Terminal flips and recent-session switches, so opening it is near-instant instead of reconnecting every time. (#4552)
- [Bug fix] The Android app no longer shows the Databricks workspace navigation bar around Omnigent when connecting to a workspace-hosted server. (#4555)
- [UI / Feature] On the macOS desktop app, the sidebar header now shares the title-bar row with the window controls — the empty strip above the sidebar and the redundant wordmark row are gone, and the Collapse/Search/Settings buttons sit beside the traffic lights. (#4557)
- [Bug fix] Codex sessions on a ChatGPT-account or API-key login launch again, instead of failing with "model is not supported when using Codex with a ChatGPT account" (#4558)
- [UI] Connecting the iOS app to a Databricks workspace now opens Omnigent directly and hides the workspace navigation bar (#4559)
- [Bug fix] kiro sessions no longer fail the first message with a connection error when the kiro TUI is slow to start (#4562)
- [Bug fix] `omnigent host` now warns once and backs off when a server accepts connections but never responds, waits out (up to 120s) a slow-booting local server instead of stranding it — stopping it if it truly fails — recovers fast runner starts after a zygote crash, and no longer leaves empty log files behind. (#4563)
- [Bug fix] Named `sys_session_send` no longer returns a spurious 404 on the second and later sub-agent sends from a bundled agent (#4564)
- [UI / Bug fix] Deleting a session removes it from the sidebar immediately instead of waiting for the server to finish tearing it down (#4566)
- [Bug fix] Session search no longer times out on large workspaces. (#4567)
- [UI / Bug fix] Fixed iOS controls rendering under the status bar on Databricks workspace-hosted servers (#4568)
- [UI / Feature] You can now leave a session someone shared with you — pick "Leave session" from its sidebar row menu to clear it from your sidebar without asking the owner (#4571)
- [UI / Bug fix] The workspace Files panel now shows hidden files by default, and its eye icon shows whether they are visible rather than what clicking will do. (#4575)
- [Bug fix] Switching a session's agent now updates the tools its native harness can call, instead of leaving the previous agent's tools in place (#4576)
- [Bug fix] The native pane reaper no longer kills a terminal that is actively producing output when the harness status pipeline stalls; it now checks tmux's own activity clock before reaping. (#4577)
- [Bug fix] A silently stalled claude-native transcript forwarder now self-recovers within five minutes and logs exactly where it stalled, instead of freezing mirroring and session status indefinitely. (#4578)
- [Bug fix] A claude-native transcript forwarder that stops — cancelled, crashed, or returned — now always logs an attributed exit line instead of dying silently. (#4579)
- [Bug fix] A stray hook event from another Claude session can no longer silently redirect a claude-native session's transcript mirroring; session identity now changes only via SessionStart announcements. (#4580)
- [Bug fix] `omni claude` streaming, statusline, and typing during tool-running turns now respond at native speed: hook subprocesses skip the framework's eager import graph, and the blocking hook path runs as shell + a loopback `curl` relayed by the long-lived runner instead of spawning a Python interpreter per event. (#4582)
- [UI / Bug fix / Feature] Fork a sub-agent to promote it into a top-level session of its own. (#4584)
- [Bug fix] Upgrading omnigent in place no longer breaks runner launches on already-running hosts (#4587)
- [UI / Bug fix] Native-harness sessions no longer briefly drop your in-flight message bubble when an interrupt marker is reconciled at the same time. (#4591)
- [UI / Bug fix / Chore] Native assistant text now reconciles cleanly with committed transcript messages without duplicate streaming output. (#4593)
- [Bug fix] The embedded browser pane no longer lingers over the welcome screen after switching or disconnecting from a server (#4595)
- [Chore] Removed the "A new version of Omnigent is available" prompt and browser PWA install support; the desktop and mobile apps remain the installable clients. (#4617)
- [Chore] OpenTelemetry exporters and automatic instrumentors are now installed through `omnigent[tracing]` instead of the default package. (#4621)
- [Bug fix] Development builds no longer show an update reminder for the matching final release. (#4628)
- [Bug fix] `omni host` no longer prints a zygote traceback — and keeps copy-on-write runner forking — when started from a directory that contains an `omnigent` checkout (#4631)
- [UI / Bug fix] Clicking a file an agent mentions in its reply now opens it, including `path:line` citations and markdown links (#4644)
- [UI / Bug fix] Fixed long unbroken text or inline code in chat messages overflowing or getting cut off at narrow window widths. (#4651)
- [Bug fix] Fixed a duplicate assistant message that could appear after reconnecting to a Claude Code (native) session (#4656)
- [Bug fix / Chore] Resuming or forking a Claude Code session containing screenshots no longer duplicates image payloads into metadata and inflates the request past the context limit. (#4659)
- [UI / Bug fix] Archiving the current session now redirects to the home page instead of leaving you on the archived session (#4671)
- [UI / Feature] Usage page shows session costs, daily spend timeline, and breakdowns by harness and model (#4673)
- [Bug fix] Sessions whose workspace is an omnigent checkout no longer run a different omnigent than the one you installed (#4688)
- [Bug fix] `omnigent run --harness acp:<agent>` now launches the ACP agent you asked for instead of the first one configured (#4689)
- [UI / Bug fix] The desktop app now auto-selects this machine after "Run on this machine" (#4691)
- [UI / Bug fix] The managed sandbox host option (and other capability-gated UI) now appears on its own after a slow `/v1/info` probe, instead of staying hidden until a page reload. (#4694)
- [Chore] Runner-backed resource APIs now avoid redundant session reads, improving responsiveness under load. (#4695)
- [Bug fix] ACP agents now report cached-read tokens, so token usage reflects what was actually billed (#4699)
- [Bug fix] Built-in ACP agents like Grok Build now appear in `omni setup` instead of being invisible (#4700)
- [Bug fix] `omnigent run --harness acp:<slug>` now works with remote servers by resolving the slug client-side and embedding the full agent config in the spec. ACP agent settings (session_id_mode, send_model, omnigent_mcp, env_passthrough) are now preserved through embedding. (#4702)
- [Bug fix / Feature] `/model` now switches an ACP agent's model mid-conversation instead of being ignored, and keeps the chat history (#4703)
- [Bug fix] A host name set in `config.yaml` is now kept when no `host_id` is present — the id is generated instead of overwriting your chosen name. (#4708)
- [Bug fix] `omnigent resume` now lists only your own sessions, not ones shared with you (#4709)
- [UI / Bug fix] The Inbox count badge now uses the same text and background colors as the selected session item in the sidebar (#4714)
- [UI / Feature] Multi-session delete now shows a table of worktree branches you can pick to clean up (with a tri-state select-all header), instead of blocking branch cleanup behind single-session delete (#4715)
- [Bug fix] OpenCode 1.18.x installs are now accepted; the version gate no longer rejects users who installed OpenCode via its official upstream route. (#4725)
- [UI / Bug fix / Chore / Test/CI] Approval prompts now name the assistant that asked (Claude Code, Codex, Cursor, Antigravity, Kiro, Goose, Qwen Code, Hermes) instead of an internal policy id (#4735)
- [UI / Bug fix] Opening the workspace folder browser no longer flashes an "Up one level" tooltip over the listing (#4742)
- [Feature] Kubernetes sandbox runners now use Jobs with automatic restart on crash (up to 3 retries) and a liveness probe, replacing bare Pods that required manual intervention after a failure. (#4744)
- [UI / Bug fix] The chat transcript now detects a silently dead live connection and reconnects on its own — worst case 45 s, instantly on tab refocus — instead of freezing until the page is reloaded. (#4750)
- [Bug fix] Chat messages sent while the terminal's Claude composer is covered by the ctrl+r history search or a hand-opened `/model` picker now dismiss the overlay and deliver, instead of silently vanishing (#4751)
- [Bug fix] Creating a session from the web UI is faster end-to-end: the chat page opens immediately and the terminal is ready sooner — including the first session after a host restart, which no longer pays a multi-second warmup. (#4752)
- [UI / Bug fix] Answered question and plan cards stay outside the "Worked for" fold, read as settled, and survive a page reload (#4760)
- [UI / Feature] Web terminals now attach directly over loopback when the runner is on the same machine as the browser, cutting keystroke echo from ~250 ms to under 10 ms against a remote server. (#4763)
- [UI / Bug fix / Test/CI] The Sessions filter is now always visible, so session filtering is discoverable without hovering the sidebar header. (#4764)
- [Feature] New `OMNIGENT_REQUIRE_WRAPPER` env var lets operators require the CLI be run through a wrapper (e.g. `isaac omni`) and refuse direct `omni` calls (#4766)
- [UI / Bug fix / Chore / Test/CI] Chat output stays visible above a growing composer; bottom-following readers remain pinned while readers who scroll up—even just 50px—keep the same visible content. (#4767)
- [Bug fix] `omnidev --vite-port` once again starts the frontend on the requested port. (#4770)
- [Feature / Test/CI] macOS desktop app now ships an Intel (x64) build alongside Apple Silicon. (#4772)
- [UI / Feature / Docs] Usage and web-driven harness setup can now be enabled per deployment with `OMNIGENT_FEATURES`. (#4775)
- [UI / Bug fix / Feature] In-chat errors now provide expandable diagnostics and recovery-aware Retry, copy, and dismiss actions without replaying failed input, and cancelled retry requests no longer leave stale recovery results cached. (#4787)
- [Feature / Docs] ArgoCD quick-start overlay for deploying Omnigent with the kubernetes sandbox provider (#4788)
- [Bug fix] `omnigent[antigravity]` no longer crashes with a protobuf gencode/runtime version mismatch on startup. (#4795)
- [UI / Bug fix] Server URLs in copyable connection and reconnect commands are now safely quoted. (#4817)
- [Bug fix] Resumed Codex conversations now keep the authentication provider selected in Codex configuration. (#4818)
- [Bug fix] `omnidev omnigent` commands now keep runtime state and configuration inside their development pod. (#4822)
- [Bug fix] Native Windows: harness CLIs (codex, pi, claude-sdk, antigravity) no longer hang on spawn due to missing `SYSTEMROOT`/`COMSPEC`; Windows drive-letter workspace paths (`C:\…`) are now accepted; pre-release harness CLI versions (e.g. `0.146.0-alpha.9.2`) no longer fail the version gate. (#4886)
- [UI / Feature] Background tasks that keep running after a turn ends now show as a pill above the composer instead of a "Working…" spinner (#4893)
- [UI / Bug fix] Maximizing the workspace panel in the desktop app no longer tucks its tab icons under the macOS window controls. (#4897)
- [Feature] Configured ACP agents (e.g. Devin) and installed ACP CLI harnesses (e.g. Grok Build) now appear in the web New Chat picker, like the native harnesses (#4909)
- [Bug fix] Managed codex, claude-sdk (Polly/Debby), and pi harnesses resolve their launch model from the workspace's Unity Catalog model services, so they no longer fail against a Databricks AI Gateway that has retired the legacy `databricks-*` model namespace (#4915)
- [UI / Feature] Devin is now a built-in harness — set it up in `omni setup`, launch it with `--harness devin` or from the New Chat picker, no `acp:` config needed (#4920)
- [Bug fix] `omni setup` and the New Chat picker no longer show a duplicate — or silently ignore — a built-in ACP harness you configured yourself under the same name; your own command wins. (#4927)
- [UI] Chat error banners are now a compact centered pill with inline Retry, matching the design prototype (#4931)
- [UI / Feature] The chat header now shows the conversation's name, its project folder, and the sub-agent path, and title bars are a consistent 48px. (#4940)
## [v0.9.0] — 2026-08-11
- [UI / Bug fix] Recent servers remain one-click connectable and now include a separate copy action. (#2555)
This Kustomize overlay turns on the **`kubernetes`** managed-sandbox provider: a
`host_type: managed` session spawns one**runner Pod** that runs `omnigent host`
as its container entrypoint and dials back to the server over the existing
launch-token tunnel. It layers the RBAC + config the provider needs onto the
base server deployment.
`host_type: managed` session spawns a**batch/v1 Job** whose child Pod runs
`omnigent host`as its container entrypoint and dials back to the server over the
existing launch-token tunnel. It layers the RBAC + config the provider needs onto
the base server deployment.
## Launch model: entrypoint-as-host
The runner Pod's container command **is** the host. An **init container**
prepares the workspace (`mkdir` + optional `git clone`); the **main container**
then runs `omnigent host` under a tiny PID-1 reaper. The host re-parents runner
processes to PID 1, which the reaper reaps; SIGTERM is forwarded for graceful
shutdown.
The runner is launched as a **batch/v1 Job** (one Pod, `backoffLimit: 6`). The
Job's child Pod runs `omnigent host` as its container command. An **init
container** prepares the workspace (`mkdir` + optional `git clone`); the **main
container** then runs `omnigent host` under a tiny PID-1 reaper. The host
re-parents runner processes to PID 1, which the reaper reaps; SIGTERM is
forwarded for graceful shutdown.
The launch token is delivered through a **per-Pod Kubernetes Secret** referenced
The launch token is delivered through a **per-Job Kubernetes Secret** referenced
by the Pod's `secretKeyRef` — it never enters the Pod spec, a command line, or
an audit log. The launcher creates that Secret at provision and deletes it
alongside the Pod at terminate.
alongside the Job at terminate.
Because the host is **never started by `exec`-ing into an already-running
container**, this provider needs **no `pods/exec` grant** — and avoids the
exec-into-running-container class of runtime issues entirely. The server SA's
rights are the minimum the launcher calls: create/get/delete Pods, get
`pods/log` (start-failure diagnostics only), create/delete Secrets (the per-Pod
token), and list events.
rights are the minimum the launcher calls: create/get/delete Jobs,
list/get Pods (to poll the Job's child), get `pods/log` (start-failure
diagnostics only), create/delete Secrets (the per-Job token), and list events.
## Two-namespace, least-blast-radius design
| Namespace | Holds |
|---|---|
| `omnigent` | the server, its DB/PVC, its Secrets, the `omnigent-server` SA |
| `omnigent-sandboxes` | runner Pods, the per-Pod token Secrets, the harness-creds Secret, the powerless `omnigent-runner` SA, the scoped Role + RoleBinding |
| `omnigent-sandboxes` | runner Jobs (and their child Pods), the per-Job token Secrets, the harness-creds Secret, the powerless `omnigent-runner` SA, the scoped Role + RoleBinding |
The server SA's Pod/Secret rights are a **namespaced Role** bound (cross-namespace)
to `omnigent-sandboxes` only — so a compromised server can manage runner Pods but
**cannot** delete the server/DB Pods, read the server's Secrets, or execute
commands inside any Pod. The runner namespace enforces Pod Security `restricted`;
The server SA's Job/Pod/Secret rights are a **namespaced Role** bound
(cross-namespace) to `omnigent-sandboxes` only — so a compromised server can
manage runner Jobs but **cannot** delete the server/DB Pods, read the server's
Secrets, or execute commands inside any Pod. The runner namespace enforces Pod Security `restricted`;
the generated runner Pod is already restricted-compliant (non-root uid 1000, drop
`ALL` caps, `seccompProfile: RuntimeDefault`, no privilege escalation).
@@ -83,7 +84,7 @@ runner Pod unexpectedly carries no credential:
(`omnigent/server/managed_hosts.py`), e.g. "agent … is not a genuine built-in;
omitting agent label".
- A name that is not a valid label value logs a `WARNING` from
`build_pod_manifest` (`omnigent/onboarding/sandboxes/kubernetes.py`), e.g.
`build_job_manifest` (`omnigent/onboarding/sandboxes/kubernetes.py`), e.g.
"agent … is not a valid omnigent.ai/agent value; runner Pod … stays
unclassified". Note the gate upstream will already have logged this agent as
classified, so this is the line that explains the missing label.
Omnigent release features are deployment-wide, temporary rollout switches. They
are not authorization controls or user preferences.
## Configuration
Set the comma-separated `OMNIGENT_FEATURES` environment variable and restart or
redeploy the server:
```bash
OMNIGENT_FEATURES=usage_page,harness_install
```
Unset or empty means every release feature is off. Unknown names fail startup.
The former `OMNIGENT_HARNESS_INSTALL_ENABLED` switch is rejected with a
migration hint; use `OMNIGENT_FEATURES=harness_install` instead. The server resolves the set once at startup and publishes frontend-visible
values in `GET /v1/info` under `features`. Users must reload the web app after a
flag change because server capabilities are cached at page boot.
`omnigent/server/feature_flags.py` is the source of truth for known keys and
lifecycle metadata.
## Inventory
| Key | Default | Owner | Review by | Purpose |
| --- | --- | --- | --- | --- |
| `usage_page` | Off | Web | 0.11.0 | Exposes the web Usage route, sidebar navigation, timeline, and cost breakdown details. The existing `GET /v1/usage` CLI API remains available while off. |
| `harness_install` | Off | Onboarding | 0.11.0 | Allows the web UI to install or configure supported harnesses on a connected host. |
At the review release, each flag must be removed by making the feature
unconditional, removing the feature, or moving a genuinely permanent operator
policy into normal server configuration.
## Rollout and rollback
1. Deploy an immutable image with the feature absent from `OMNIGENT_FEATURES`.
2. Enable it on one deployment, consistently across all replicas.
3. Verify `GET /v1/info`, then reload and exercise the gated UI.
4. Expand by deployment cohort.
5. Roll back by removing the key and redeploying the same image.
@@ -72,11 +72,22 @@ they still work with no runner or LLM.
| Journey | Operation timed |
| --- | --- |
| `native_hook_spawn` | Spawn the per-chunk `MessageDisplay` hook exactly as Claude Code does — isolated interpreter, module entrypoint, JSON payload on stdin |
| `native_hook_spawn` | Spawn one **Python** command hook — isolated interpreter, module entrypoint, JSON payload on stdin — and time its whole lifetime |
Claude Code **blocks its TUI** on command hooks, so one hook subprocess's
lifetime is user-visible streaming latency, and the same interpreter+import
cost fronts every statusline refresh and per-tool-call policy hook. The
lifetime is user-visible latency. Read this number as *"what a hook costs if it
is Python"*.
It is **not** the per-chunk streaming cost, and treating it as one leads
straight to wasted work. The hooks that fire per chunk (`MessageDisplay`) and
per tool call (`evaluate-policy`) were deliberately moved off the interpreter —
a `/bin/sh` appender and a `curl` to the runner's relay — and
`test_message_display_shell_command_round_trips` pins that by asserting
`"python"` is absent from the installed command. What still pays this number is
the per-turn set (`SessionStart` / `Stop` / `UserPromptSubmit` / `PreCompact` /
`Task*`), the `PostToolUse``TodoWrite`+`TaskUpdate` matchers, and the policy
hook's Python fallback before the relay is up. So the journey's real job is to
keep the argument for staying off the interpreter measurable. The
journey needs no server or runner; registering it here rides hook spawn cost
on the same nightly/release regression comparison as everything else
(`omnigent/__init__` re-exports lazily so this stays ~interpreter-sized). The
@@ -50,7 +50,7 @@ Run it from anywhere inside the checkout — it walks up to the repo root
|---|---|---|
| server | `uv run omnigent --log-to-stderr server --host 127.0.0.1 --port <p> --database-uri … --artifact-location …` | Waited on via `GET /health`. |
| host | `uv run omnigent --log-to-stderr host --server http://127.0.0.1:<p>` | Started once the server is healthy. |
| vite | `pnpm run dev -- --host <host> --port <p> --strictPort` (cwd `web/`) | `OMNIGENT_URL` points its proxy at the pod's server. |
| vite | `pnpm run dev --host <host> --port <p> --strictPort` (cwd `web/`) | `OMNIGENT_URL` points its proxy at the pod's server. |
Before Vite starts (and on a manual Vite restart), omnidev runs `pnpm install`
in `web/` when needed — `node_modules/` is missing, or `package.json` /
- Python deps via `uv` only (never pip); JS/TS via `bun`. Latest stable deps.
- Pre-commit gate (must pass): `uv run ruff check --fix && uv run ruff format && uv run mypy --strict . && uv run pytest`. Never disable a lint/type rule — fix the root cause.
- Pre-commit gate (must pass): `uv sync --group dev && uv run --no-sync ruff check --fix && uv run --no-sync ruff format && uv run --no-sync pyrefly check && uv run --no-sync pytest`. Never disable a lint/type rule — fix the root cause.
- agy pinned: `AGY_EXPECTED_VERSION=1.0.10` (Docker build fails on mismatch). All RPC shapes are version-sensitive.
- [ ] **Step 1:** Grep for `antigravity_native_forwarder` / `forwarded_steps` / `update_forwarded_steps` references; confirm only the reader path remains.
- [ ] **Step 2:** Delete the forwarder module + its tests; remove the cursor fields/methods from the bridge; relocate the shared types.
- [ ] **Step 3:** Run the full gate: `uv run ruff check --fix && uv run ruff format && uv run mypy --strict . && uv run pytest` (targeted antigravity suites + server).
- [ ] **Step 3:** Run the full gate: `uv sync --group dev && uv run --no-sync ruff check --fix && uv run --no-sync ruff format && uv run --no-sync pyrefly check && uv run --no-sync pytest` (targeted antigravity suites + server).
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.