main
1400 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
72b4469d72 |
perf(host): open daemon readiness polls tight, then back off (#5190)
The host-online, runner-online and Claude-terminal-ready waits all polled on a flat 0.5s cadence. Those waits gate every native-harness launch and usually resolve on the first probe or two — a warm host is already online, a fresh runner connects in a second or two — so a flat cadence spends up to a full interval doing nothing after the thing is already ready. Replace the fixed sleep with `daemon_poll_intervals()`: open at 0.1s, grow geometrically, hold at the existing 0.5s for the long tail. Fast launches notice readiness sooner without a long wait hammering the server. `DAEMON_POLL_INTERVAL_S` keeps its name and value as the steady-state cap, so `connect.py`'s runner-exit watcher still matches it; its comment now notes the client's opening probes are tighter. 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> |
||
|
|
07afe65150 |
fix(files): browse outside the workspace behind a slash-merging proxy (#5174)
* fix(files): browse outside the workspace behind a slash-merging proxy
The file panel showed "No files in workspace" for any directory above or
outside the working folder when the app is served through the Databricks
Apps front door, even though the picker dropdown listed those very
directories.
Absolute browse locations were marked by a leading %2F in the filesystem
route's {path} segment. That %2F decodes to a boundary // which the front
door merges back to a single / (//health 301-redirects to /health), so
/Users/me arrived at the server as a workspace-relative Users/me and listed
a nonexistent path under the workspace. The picker uses the host filesystem
endpoint, which already sends bare slashes, so it was unaffected -- hence
paths in the dropdown but an empty tree.
Follow the host endpoint's own convention instead: send the path with
literal slashes and no leading %2F (nothing for a proxy to merge) and name
an absolute location out of band with ?base=host. The server re-adds the
leading slash, applies the same owner gate (an absolute path is owner-only,
so base=host is not a way around it), and re-encodes %2F only on the
internal server->runner leg, which is not proxied. A genuine leading slash
still works for a direct, unproxied client.
Covers list, lazy directory expand, search, file read, and write; the
filesystem root (base=host with an empty segment) is handled on the no-path
route.
Test plan:
- e2e_ui test_absolute_browse_survives_a_slash_merging_proxy reproduces the
front door's %2F-decode + //-merge on the session's filesystem/search
requests and asserts the tree lists an outside directory. Reverting the
web change makes it fail with "No files in workspace".
- collaboration test asserts the owner gate applies to the base=host form
(collaborator 403, owner 200), so it is not an escape hatch.
- route tests pin base=host -> absolute (%2F) forwarding, a bare path
without base staying workspace-relative, and base=host root -> "/".
Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* refactor(files): reuse the shared browse-location helpers for file read/write
Address Polly's non-blocking notes on PR #5174. fetchFileContent and
writeFileContent reimplemented the "strip leading slash + ?base=host" wire
rule inline; route them through the exported browseLocationSegment /
browseLocationBase so the slash-merge-safe contract lives in one place.
Behavior is identical (verified: same URL for relative and absolute paths).
Also note in the list_environment_root base=host branch that the delegate
(read_or_list_environment_path) runs its own owner-gated _validate_session,
so the delegation is authorized there.
Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(files): open a file at an absolute browse location by its absolute path
A follow-on to the browse-outside-workspace fix: navigating to a directory
outside the workspace (e.g. /tmp) now lists files, but OPENING one 404'd.
The tree names files by their bare name relative to the browsed location, and
openTreeFile re-attached that name only for workspace-relative locations --
for an absolute location it handed the viewer the bare name, which the viewer
then looked up under the workspace root (nonexistent) → 404.
Always re-attach the browsed location: for an in-workspace location that
yields a workspace-relative path, and for one outside the workspace the file's
absolute path, which fetchFileContent already requests host-absolutely
(base=host, per the earlier consolidation).
Tests: FilesPanel unit test (open a file at an absolute location → the
absolute path reaches onFileSelect; mutation-verified against the old
bare-name form) and a useFileContent test pinning the absolute wire form
(bare path + base=host).
Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(files): keep the browsed directory when the file viewer closes
Opening a file swaps the files panel for the FileViewer in the rail's
content slot, unmounting the panel -- and the browse location was plain
component state, so closing the viewer remounted the panel at the
workspace root instead of the directory the file was opened from
(browse to /tmp, open a log, go back -> dumped at the workspace root).
Remember the location in a module-level per-conversation cache, the same
pattern FolderTree already uses for its expanded-folders set across the
identical unmount/remount. Seeded at mount, written on every navigation
(cleared when returning to the workspace root), and re-read on an
in-place conversation switch so another session still opens at its own
root -- the cache is keyed by conversation, so no location ever leaks
across sessions.
Tests: FilesPanel unit tests for the unmount/remount round trip and the
per-conversation scoping (round trip mutation-verified), and the
outside-workspace e2e now opens a file at the browsed directory, closes
the viewer via its tab, and asserts the header still names that
directory (also mutation-verified: against a pre-fix build it fails
with the header back at the workspace path).
Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
|
||
|
|
5d7aa85132 |
fix(cli): forward non-uuid session ids on remote resume (#5218)
`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> |
||
|
|
d20c465457 |
fix(codex): serialize stdin writes to prevent parallel tool-call interleaving (#5229)
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> |
||
|
|
f443086bd6 |
fix(cli): close daemon stdin to release terminal on shell exit (#5228)
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> |
||
|
|
8103829ef8 |
fix(codex-native): honor Max/Ultra reasoning levels instead of coercing to xhigh (#5217)
* 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> |
||
|
|
03b6ee51f5 |
feat(scheduled): auto-attach cost_budget policy on fire (#4791)
* 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> |
||
|
|
a8b030b786 |
fix(runner): keep native harness alive during terminal activity (#5212)
Signed-off-by: Lee moon soo <moonsoo.lee@databricks.com> |
||
|
|
eeec1cedf0 |
fix(runner): dedupe repeat sub-agent inbox re-wake notices (#4803)
* 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>
|
||
|
|
3d537bba59 |
fix(claude-native): cold-resume a session on its persisted canonical model (#5167)
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 |
||
|
|
67f9db8bc8 |
Implement Claude Code permission mode switching (#4018)
* 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> |
||
|
|
4b6779febb |
feat(auth): refreshable credential for unattended host daemons (#4743)
* 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> |
||
|
|
f914095c1d |
fix(cli): preserve Databricks profile shorthand (#5143)
## 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> |
||
|
|
269dffacb6 |
feat(cli): add global CPU profiling (#5141)
## 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> |
||
|
|
2fbd660f55 |
fix(web): copy tmux selections to the system clipboard (#5136)
## 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> |
||
|
|
a101840bbc |
perf(native): overlap session create with the host-online poll (#5091)
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>
|
||
|
|
5689ef33ee |
perf(host): shorten tunnel recovery (#4981)
* perf(host): shorten tunnel recovery Signed-off-by: Lee moon soo <moonsoo.lee@databricks.com> * Preserve loopback reconnect tolerance Signed-off-by: Lee moon soo <moonsoo.lee@databricks.com> * Trigger CI retry Signed-off-by: Lee moon soo <moonsoo.lee@databricks.com> --------- Signed-off-by: Lee moon soo <moonsoo.lee@databricks.com> |
||
|
|
1732faf3f3 |
feat: one harness-truth source for every model surface (listings, defaults, reports, confirmed switching) (#5022)
* 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
|
||
|
|
bb5de03e50 |
feat(cli): pin databricks workspace login to a per-host profile (#5112)
`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> |
||
|
|
45424e0739 |
fix(openai-agents): preserve reasoning item IDs by default (#5065)
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> |
||
|
|
9d54826ea5 |
fix(codex-native): suppress old-model startup prompt (#5116)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
2e0098ed1f |
fix(server): keep idle sessions quiet after runner disconnect (#5113)
* 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> |
||
|
|
3da0e9f4e0 |
fix(codex-native): bypass intermittent hook trust gate (#5108)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
9a3d4dea54 |
fix(codex): surface gateway auth failures instead of a silent 600s stall (#4612)
* 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> |
||
|
|
32f0d78ebd |
fix(tools): paginate agent and session discovery (#4892)
* fix(tools): paginate agent and session discovery Signed-off-by: Robert Dosen <robert.dosen@gmail.com> * fix(tools): use cursors for discovery pagination Signed-off-by: dbczumar <corey.zumar@databricks.com> * test(e2e): cover discovery cursor compatibility Signed-off-by: dbczumar <corey.zumar@databricks.com> * fix(tools): harden discovery cursor continuation Signed-off-by: Robert Dosen <robert.dosen@gmail.com> --------- Signed-off-by: Robert Dosen <robert.dosen@gmail.com> Signed-off-by: dbczumar <corey.zumar@databricks.com> Co-authored-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
fe421c98a7 |
fix(native): retry preflight forwarder failures (#5104)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
9fc0c382be |
fix(codex): forward effective context window (#5026)
* fix(codex): forward effective context window Codex 0.147 moved modelContextWindow outside total usage. Preserve the legacy nested field as a compatibility fallback. Signed-off-by: Sladan Ristic <sladan.ristic@trigo.at> * test(codex): cover effective context window end to end Signed-off-by: dbczumar <corey.zumar@databricks.com> --------- Signed-off-by: Sladan Ristic <sladan.ristic@trigo.at> Signed-off-by: dbczumar <corey.zumar@databricks.com> Co-authored-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
636fb6a774 |
fix(cli): don't crash host stop on a stale foreign daemon record (#5012)
`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> |
||
|
|
b86ae8e121 |
fix(codex): attach local environment to web turns (#5048)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
0b0fc3f2f9 |
fix(kubernetes): resume dormant managed hosts (#5046)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
ccaa42e926 |
feat(acp): honor permission_mode so an ACP agent can run unattended (#5056)
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> |
||
|
|
adffcbbc4b |
feat(acp): offer the agent's own approval scopes on the card (#5053)
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>
|
||
|
|
ef60c1f874 |
[OMNI-3742] Enable emojis as project icons (#5099)
* [OMNI-3742] Enable emojis as project icons Signed-off-by: Hubert Zub <hubert.zub@gmail.com> * Address feedback Signed-off-by: Hubert Zub <hubert.zub@gmail.com> --------- Signed-off-by: Hubert Zub <hubert.zub@gmail.com> |
||
|
|
b8fd98ef7b |
feat(claude-native): propagate an in-terminal /rename to the Omnigent UI (#4104)
* 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> |
||
|
|
1ed6b49671 |
fix(codex-native): surface standalone turn errors (#5060)
* fix(codex-native): surface standalone turn errors Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com> * fix(codex-native): dedupe terminal error edges Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com> --------- Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com> |
||
|
|
33ec51372b |
fix(runner): reap failed Claude-native sessions (#5040)
* fix(runner): reap failed Claude-native sessions Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com> * fix(runner): tighten Claude cleanup idempotency Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com> * fix(runner): ignore vanished Claude timeout targets Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com> --------- Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com> |
||
|
|
5520a5e00d |
perf(server): collapse the per-event access-control reads into one checkout (#4737)
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>
|
||
|
|
179774eb63 |
perf(permissions): short-lived resolve_access cache on the event path (#4820)
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>
|
||
|
|
752d3eb3d6 | feat(web): show sub-agent harnesses in usage sessions table (#4996) | ||
|
|
741f2d29e4 |
fix(host,runner): reconnect host + all sessions promptly on laptop wake (#5054)
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> |
||
|
|
b551f669d6 |
fix(openai-agents): wrap string assistant content for the chat converter (#4824)
Resuming a conversation whose history contained an assistant message with
plain-string content crashed before reaching the model:
TypeError: string indices must be integers, not 'str'
chatcmpl_converter.py:625 in items_to_messages
A string is legal Responses-API content, but items_to_messages iterates an
assistant message's content expecting blocks. Given a string it walks the text
character by character and indexes each character, so the very first one raises.
User strings are unaffected — they reach extract_text_content, which accepts
them, and callers depend on them staying strings.
Because history is replayed on every turn, one such item ends the conversation
permanently: each retry fails identically before the model is reached, and the
only escape is to abandon the conversation.
Only the assistant branch is normalized, and only when content is a string, so
block content and user strings pass through untouched.
Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com>
|
||
|
|
ddc5f6ee60 |
chore(k8s): Address ArgoCD overlay review follow-ups and PR #4744 comments (#4982)
* Address ArgoCD overlay review follow-ups (#4977) and PR #4744 comments Add CI validation of kustomize overlays, clarify the ignoreDifferences /data vs /stringData ArgoCD normalization, separate sync-completes from app-healthy in the Ingress wave comment, add TODO(v0.29) to the bare-Pod fallback in terminate(), and update the sandbox-runners README to reflect the bare-Pod → Job migration. Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix(ci): install kustomize via official script instead of third-party action The pinned SHA for imranismail/setup-kustomize was unresolvable. Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> * fix(docs): correct backoffLimit value in sandbox-runners README The README stated `backoffLimit: 0` but the actual code uses `_JOB_BACKOFF_LIMIT = 6` — fix the doc to match. --------- Signed-off-by: Yuan Tang <terrytangyuan@gmail.com> |
||
|
|
473941e322 |
fix(acp): resolve the tool identity on a bare permission request (#5050)
An ACP agent may ask permission carrying only a `toolCallId` — no `title`,
`kind`, or `rawInput`. Devin does. `_extract_tool_call` then resolved the name to
the literal string "tool" with empty arguments, so:
- the approval card asked the user to approve "Devin wants to use **tool**",
preview `tool({})`, with no command shown; and
- the TOOL_CALL policy was evaluated as `{"name": "tool", "arguments": {}}`,
which no builtin rule can match — rules gate on the tool name before reading
`arguments["command"]`, so a "deny `rm -rf`" policy sat silent.
The originating `tool_call` update carries the real name and command and always
arrives first, and the executor already caches `toolCallId -> name` there to
close the right tool card. Cache the `rawInput` beside it and fall back to both
when the request omits them; values the request does carry still win. The
correlation is the protocol's own id, so no vendor `_meta` key is read.
Both caches are released when the call closes, as the name cache already was.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
|
||
|
|
8f63f3271b |
fix: preserve managed host logs on relaunch (#5042)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
9df23f7aeb |
chore(ucode): pin OSS ucode to a fixed commit (#5043)
Omnigent's uvx setup path resolved ucode from the mutable `main` branch, so setup could silently pick up a new ucode commit between runs and break unexpectedly. Pin `_UCODE_GIT_REF` to a fixed, known-good commit (94271a78c7139220b7333bcae91e522f95ef3af3) so setup is reproducible. A full SHA is immutable, so uvx caches the built wheel by ref and reuses it across runs; drop the `--refresh-package ucode` that existed only to defeat the mutable branch's stale cache. Co-authored-by: Isaac Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com> |
||
|
|
c6d1f7d5e0 |
feat(web): resume imported / host-less sessions from the web (#4905)
An imported or otherwise unbound session (no host, no runner) couldn't run from
the web: it read as reachable (so the first message dropped against a runner
that can't start) or dead-ended on the terminal reconnect path.
The fix is mostly server-side liveness. An imported transcript is a
native-harness session that only runs in a runner on a host, never in-process,
so report it as runner_online=false via a new `imported` connectivity marker
(keyed on the omnigent.import.source label — the sibling of the existing fork
`needs_workspace` marker, computed in the same query). With that, the open view
routes to the EXISTING host picker (ResumeWithDirectoryDialog) instead of the
dead end. That picker — the same one forks and new-chat use — binds the session
to an online host + workspace (defaulting to the caller's current host) and
launches a runner via the existing POST /v1/hosts/{id}/runners path. No new
host-selection UI, no new launch route.
The picker is offered only when the resume will actually work
(unboundSessionResumableInApp): the caller must OWN the session (launch_runner
requires owner — a shared non-owner 404s), and for imports the harness must
reconstruct context from the omnigent transcript so it carries onto a chosen
host. Kimi has no resume path, and kiro/qwen resume only from a local recording
that lives on the original machine, so those route to the terminal reconnect
path instead of a picker that would start blank.
Also:
- Skip the cold-boot startup grace for imports so the picker shows at once.
- Generalize ResumeWithDirectoryDialog to prefill from the session's own fields
when there is no fork source.
- `omnigent import` prints the session's browser URL instead of the bare id.
Co-authored-by: Isaac
Signed-off-by: Mark Tai <mark.tai@databricks.com>
Co-authored-by: Mark Tai <mark.tai@databricks.com>
|
||
|
|
05eaa253d1 |
fix(host): retry Databricks auth refresh (#5014)
Signed-off-by: dbczumar <corey.zumar@databricks.com> |
||
|
|
699809de6e |
Show actual server target in host-daemon conflict error (#4821)
The "A host daemon is already running for this server" error suggested `omnigent host stop --server ...`, where the literal `...` hid the fact that `--server` needs an argument and left users guessing which value to pass. Build the hint via the existing `_host_stop_command` helper from the conflicting record, so the message prints a ready-to-run command: the real URL for a remote daemon, or `--server ""` (the empty-string alias) for a local daemon, matching the `host --background` hint. Co-authored-by: Isaac Signed-off-by: Evelyn Hur <122575337+evelyn-hur@users.noreply.github.com> Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com> |
||
|
|
1fceeeb754 |
Bump version to 0.11.0.dev0 (#4989)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
86726e0ab5 |
fix(web): keep the Working shimmer lit while background tasks run (#4906)
* fix(web): keep the Working shimmer lit while background tasks run The background-tasks pill (#4893) introduced a shared `isBackgroundTasksOnly` predicate that gated all three busy surfaces off `bgCount > 0` alone, without checking whether the agent's turn was still active. So any live turn that coincided with a background task — notably `waiting`, where the parent is parked on its async-work drain of sub-agents / background shells — had its "Working…" shimmer suppressed and replaced by the pill, misreading an active turn as finished. Make the shimmer and the pill independent surfaces: - `isBackgroundTasksOnly` now also requires the turn to be inactive (`!agentWorking`), so the shimmer yields only once the turn has genuinely ended (`idle`) with tasks lingering. - `BackgroundTaskPill` shows on `bgCount > 0` alone, decoupled from the shimmer, so both appear together while the turn is active. - `workingIndicatorLabel` no longer emits the background count (the pill owns it); the shimmer just rotates its working messages or shows "Blocked on: …". Co-authored-by: Isaac * fix(web): keep the background-task pill lit while the turn works The pill vanished the moment the "Working…" shimmer appeared, so the two surfaces were still effectively mutually exclusive. The cause was the background-shell tally being zeroed on every new turn: the server's _publish_status popped the cache on a `running` edge, the client's session_status reducer zeroed it on `running`, and the optimistic send path cleared it synchronously. All three date from the single-surface design, where the count was a LABEL on the shimmer ("N background tasks still running") that a new turn should replace with "Working…". Now that the pill is a separate surface, background shells outlive turn boundaries and the tally must persist across the turn so the pill stays lit beside the shimmer. Stop clearing on `running` in all three places; keep clearing only on an authoritative Stop-hook `0` (shell finished) and on `failed` (a dead session may never post another count). The next Stop hook re-reports the count authoritatively. Also note the server normalizes a claude-native turn-end `waiting`+count to `idle` (see _background_task_delivery_status), so the client's real "working + shell" state is `running` with a preserved count — reflected in the reworked e2e coverage. Co-authored-by: Isaac * fix(web): remove the scroll-pinned Working tab The pinned "Working…" tab (shown while scrolled up) was designed to merge its flat bottom edge into the composer, but the background-task pill now sits between them — so the tab reads as a stray rounded card floating above the pill. Remove the sticky tab entirely (WorkingStatusPin); the inline shimmer at the end of the thread is the working cue. Move the tab's one non-visual job — the sole aria-live region announcing the working state — onto the inline WorkingIndicator: a stable "Working…" in a role=status region, with the rotating visible label kept aria-hidden so it never re-announces. Screen readers still get one announcement per turn. Co-authored-by: Isaac |