1400 Commits

Author SHA1 Message Date
Harry Yao 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>
2026-08-22 09:21:41 +00:00
Corey Zumar 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>
2026-08-21 23:07:02 -07:00
Aravind Segu 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>
2026-08-21 15:54:45 -07:00
Tomu Hirata 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>
2026-08-21 22:24:09 +00:00
Tomu Hirata 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>
2026-08-22 07:23:32 +09:00
Pat Sukprasert 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>
2026-08-21 20:52:14 +00:00
Yuan Tang 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>
2026-08-21 16:22:23 -04:00
Lee moon soo a8b030b786 fix(runner): keep native harness alive during terminal activity (#5212)
Signed-off-by: Lee moon soo <moonsoo.lee@databricks.com>
2026-08-21 12:35:22 -07:00
Harry Yao 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>
2026-08-21 07:11:20 -07:00
Corey Zumar 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
2026-08-21 00:47:49 -07:00
Harry Yao 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>
2026-08-20 23:14:53 -07:00
Edwin He 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>
2026-08-20 19:14:41 -07:00
Zeyi (Rice) Fan 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>
2026-08-20 18:57:13 -07:00
Zeyi (Rice) Fan 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>
2026-08-21 01:32:32 +00:00
Zeyi (Rice) Fan 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>
2026-08-20 17:30:55 -07:00
Harry Yao 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>
2026-08-20 16:40:52 -07:00
Lee moon soo 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>
2026-08-20 23:31:17 +00:00
Corey Zumar 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 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>
2026-08-20 16:24:23 -07:00
Mark Tai 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>
2026-08-20 16:15:31 -07:00
Thomas Garnier 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>
2026-08-20 14:57:57 -07:00
Corey Zumar 9d54826ea5 fix(codex-native): suppress old-model startup prompt (#5116)
Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-20 12:41:04 -07:00
Corey Zumar 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>
2026-08-20 11:47:33 -07:00
Corey Zumar 3da0e9f4e0 fix(codex-native): bypass intermittent hook trust gate (#5108)
Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-20 11:16:27 -07:00
Manfred Calvo 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>
2026-08-20 11:08:59 -07:00
dosenr 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>
2026-08-20 11:00:31 -07:00
Corey Zumar fe421c98a7 fix(native): retry preflight forwarder failures (#5104)
Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-20 10:59:34 -07:00
Slađan Ristić 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>
2026-08-20 10:23:38 -07:00
Harry Yao 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>
2026-08-20 17:07:12 +00:00
Corey Zumar b86ae8e121 fix(codex): attach local environment to web turns (#5048)
Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-20 09:52:20 -07:00
Corey Zumar 0b0fc3f2f9 fix(kubernetes): resume dormant managed hosts (#5046)
Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-20 09:50:55 -07:00
Dhruv Gupta 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>
2026-08-20 15:37:02 +00:00
Dhruv Gupta 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>
2026-08-20 08:17:32 -07:00
Hubert 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>
2026-08-20 13:24:31 +02:00
Harry Yao 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>
2026-08-20 06:28:50 +00:00
Pat Sukprasert 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>
2026-08-20 14:07:36 +08:00
Pat Sukprasert 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>
2026-08-20 12:01:47 +07:00
Edwin He 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>
2026-08-19 17:10:16 -07:00
Edwin He 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>
2026-08-19 15:38:50 -07:00
Yuan Tang 752d3eb3d6 feat(web): show sub-agent harnesses in usage sessions table (#4996) 2026-08-19 17:42:51 -04:00
Dhruv Gupta 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>
2026-08-19 14:04:09 -07:00
Jeremiah lu 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>
2026-08-20 05:41:00 +09:00
Yuan Tang 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>
2026-08-19 12:38:39 -07:00
Dhruv Gupta 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>
2026-08-19 12:31:39 -07:00
Corey Zumar 8f63f3271b fix: preserve managed host logs on relaunch (#5042)
Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-19 11:17:01 -07:00
Edwin He 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>
2026-08-19 18:08:48 +00:00
Mark Tai 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>
2026-08-19 10:16:18 -07:00
Corey Zumar 05eaa253d1 fix(host): retry Databricks auth refresh (#5014)
Signed-off-by: dbczumar <corey.zumar@databricks.com>
2026-08-19 10:12:39 -07:00
Evelyn Hur 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>
2026-08-20 01:11:47 +08:00
omnigent-ci[bot] 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>
2026-08-19 05:44:29 +00:00
Daniel Lok 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
2026-08-19 08:18:47 +08:00