Conversation
|
@cursor plz review this pr |
PR Review: Fix Windows native startup fallbacksVerdict: Approve. This is a focused, well-scoped fix for real Windows startup blockers. The approach matches existing conventions ( What this fixesThree separate Windows failures are addressed:
Code review by areaNative bridge temp rootsSwitching the four lagging bridges to On POSIX this preserves historical behavior (
|
|
@codex plz review this pr |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd4b7e4e90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| except ImportError: | ||
| return _arrow_menu_fallback(options, default=default, disabled=disabled, multi=multi) |
There was a problem hiding this comment.
Preserve back navigation in the Windows menu fallback
On Windows this new ImportError path is used for normal TTY menus because termios/tty are unavailable, but _arrow_menu_fallback only accepts numeric choices and never raises _GoBack. Most wizard menus use the default allow_back=True and callers rely on _GoBack to move to the previous step, so Windows users can get stuck completing the current menu flow or aborting the whole setup instead of going back; please add an explicit fallback back option such as q that raises _GoBack when allow_back is enabled.
Useful? React with 👍 / 👎.
|
@codex updeted,plz review again |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41cd2849eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
01a9ab9 to
4e110e2
Compare
* slack integration initial commit * fix the issue where slack server preamturely terminates the response * fix the issue where long responses could cause msg_too_long * support slack mrkdwn * address PR feedback * pass pre-commit
…atus pair (omnigent-ai#2545) An idle pi-native session kept queueing web messages client-side instead of sending them: the composer showed "Send a follow-up (queued)" with a green (idle) session dot, and only a tab switch unstuck it. The pi extension minted a fresh response_id on every external_session_status edge (agent_start running, agent_end idle). The web store clears its local "streaming" flag only when the idle edge's response_id matches the running edge that opened the turn (or when activeResponse is already null); with mismatched ids neither branch fired, so status stayed "streaming" forever. shouldQueueSend then queued every message and maybeFlushQueuedHead refused to drain (both bail on status === "streaming"). switchTo hard-resets the store, which is why a tab switch masked it. claude-native never hit this because its forwarder reuses one turn-scoped id across both edges. Mint a per-turn response_id in agent_start and reuse it in agent_end so the running/idle pair matches, matching claude-native's contract. Co-authored-by: Isaac
…-ai#2595) * feat(web): show project name in pinned session hover flyout Pinning a session lifts it out of its project folder into the flat "Pinned" sidebar section, which dropped the visual cue for which project it belongs to. Hovering a pinned, project-owned row now opens a flyout showing the session title plus a folder icon and the project name, reusing the existing project label already resolved for the kebab menu. The flyout uses the shared HoverCard primitive (Cursor-style right / top-aligned placement, matching AgentHoverCard) and is scoped to pinned rows — non-pinned rows still convey their project via the folder they sit in. Co-authored-by: Isaac * test(e2e_ui): cover pinned-row project hover flyout Add a Playwright e2e that files a session into a project, pins it (lifting it into the flat Pinned section), then hovers the pinned row and asserts the flyout surfaces the folder icon + project name and the session title. Drives the real project-move PATCH → label → pinned peel → hover flyout chain the Sidebar unit tests mock out, and exercises the browser hover that opens the Radix HoverCard (which jsdom can't). Co-authored-by: Isaac * feat(web): show full wrapping title in pinned project flyout Session titles have no length cap (the server schemas and the rename input are both unbounded), so the flyout's one-line `truncate` clipped longer titles with an ellipsis. Clamp to 3 wrapped lines instead so the full title shows and wraps while the card stays tidy — the complete text stays in the DOM. Co-authored-by: Isaac
…t-ai#2596) * fix(web): align sidebar rows to a consistent two-column grid The sidebar's top nav (New session, Search), section headers, project folders, and session rows each carried their own horizontal padding, so icons and labels landed at slightly different X positions down the list. Pull every row onto one grid: icons on the left column, labels/nested chats on the label column. New session uses gap-1 px-2, Search moves its icon to left-2 / pl-7, flat session rows drop to px-2, and nested project chats indent with pl-3 (footers follow at pl-5). Co-authored-by: Isaac * test(e2e-ui): regenerate visual baselines --------- Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
…e first turn (omnigent-ai#2478) * fix(server): widen host-bound runner-connect grace to 10s On the first message to a host-bound session, the server waits for the create-time runner's tunnel to register before forwarding. The grace was 3s, but a freshly-launched runner needs ~5.5s to boot and connect its WS tunnel. The wait timed out, abandoned the still-booting runner, and relaunched a second one from scratch — roughly doubling cold-start latency (~12.7s observed) and orphaning the first runner process. Widen the grace to 10s so the first message rides the runner that create already launched instead of relaunching. The wait stays event-driven (it wakes the instant the runner's hello frame arrives) and still exits early when the daemon convicts the runner dead, so a genuine startup failure does not now cost a full 10s. Co-authored-by: Isaac * fix(web): keep "Working…" lit when live status beats a stale offline poll The main chat's "Working…" indicator was suppressed whenever the open session's runner read offline, checked before the running/waiting status. The open-session `/health` poll is strict (runner_online true only while a tunnel is registered) and runs on a 10s cadence, so on a fresh session's first turn its first request lands while the runner is still connecting and returns runner_online=false — held for up to 10s. The authoritative `session.status: running` SSE edge arrives in that window but the gate ignored it, so the indicator never appeared. A session actively reporting running/waiting cannot have an offline runner, so let its live status win over the lagging poll: only suppress on known-offline when the session is otherwise idle (preserving the don't-spin-a-dead-session-on-a-background-shell-tally case). Surfaced by the faster host-bound runner connect (this branch): the turn now starts inside the poll's stale-offline window instead of after it. Co-authored-by: Isaac
…2599) The pinned-session project flyout (omnigent-ai#2595) opens a Radix HoverCard on a pinned, project-owned row. On a touch/mobile viewport there is no real hover, so tapping the row to navigate also opened the HoverCard, which then lingered over the chat page after navigation. Gate the flyout off below the `md` breakpoint via useIsMobileViewport(). Forcing `projectFlyoutName` to null on mobile routes the row through the plain ContextMenu/link path (no HoverCard mounted) and restores the native `title` tooltip, since every downstream branch already keys off that value. Co-authored-by: Isaac
…ages unblock (omnigent-ai#2597) The web client's maybeFlushQueuedHead gate checks s.status === 'streaming'. That status only clears to 'idle' when the idle session.status SSE carries the same response_id that set activeResponse at turn start. Pi's extension generated a new ++sequence id for every event, so the running/idle pair never matched and status stayed 'streaming' permanently — queued follow-up messages were never dispatched even after Pi finished replying. Fix: store the response_id set in agent_start in activeResponseId, and reuse the captured value in agent_end. The fallback (a fresh id) fires only when agent_end is reached without a prior agent_start response_id, which should not happen in normal operation.
…igent-ai#2247) * OMNI-1193: scheduled-task persistence foundation (SqlScheduledTask/SqlScheduledTaskRun + migration + store + tests) Co-authored-by: Isaac * OMNI-1193: drop plugins column from scheduled_tasks (reviewer: Omni resolves plugins host-side, no per-task field) Co-authored-by: Isaac * OMNI-1193: drop MySQL-illegal TEXT server_default on scheduled_tasks.metadata Co-authored-by: Isaac * OMNI-1193: store opaque scheduled_tasks text columns (prompt/metadata/error) as CompressedText Co-authored-by: Isaac * OMNI-1193: document Isaac→Omni id migration contract (mint new st_ id, keep isaac schedule_id in metadata) + fix stale metadata-Text comment Co-authored-by: Isaac * Make scheduled_tasks.owner_user_id nullable Permit NULL so a schedule created with no authenticated user (single-user / OSS mode) can leave the owner unset, matching how create_session treats the session owner as optional. Persistence-only: the fire-path resolution (null -> reserved "local" user) lands in a later PR. Co-authored-by: Isaac * Make scheduled_tasks trigger recurring-only (drop run_at_ms one-shot arm) The z6a2b3c4d5e6 migration is unreleased, so it is edited in place rather than adding a follow-up migration. Co-authored-by: Isaac * Drop completed state from scheduled_tasks (recurring-only has no terminal state) The z6a2b3c4d5e6 migration is unreleased, so the state CHECK is edited in place rather than adding a follow-up migration. Co-authored-by: Isaac * Refine scheduled_tasks schema: timezone default + index tweaks - timezone: add server_default="UTC" (model + migration) so raw inserts always get a valid zone - drop unused ix_scheduled_tasks_agent_id (no query filters by agent_id) - reshape ix_scheduled_task_runs_scheduled_task_id to (workspace_id, scheduled_task_id, scheduled_at, id) to cover list_runs()' scheduled_at DESC sort All in-place on the unreleased migration; no follow-up migration. Co-authored-by: Isaac * OMNI-1193: trim redundant scheduled_tasks column comments to match sibling tables; reword sandbox_target comment Co-authored-by: Isaac * OMNI-1193: fix ruff C416 lint in scheduled_tasks migration test Co-authored-by: Isaac * OMNI-1193: genericize external-scheduler references in scheduled_tasks Comment/docstring only — no functional code, column names, or values changed. Co-authored-by: Isaac * OMNI-1193: align sandbox_target width with hosts.sandbox_provider (String(32)) Co-authored-by: Isaac * OMNI-1193: add nullable error_code to scheduled_task_runs Short, queryable failure-classification token (String(64), no CHECK) alongside the compressed error blob, so future retry logic can distinguish retryable vs terminal failures. Threaded through the entity, migration, store, and tests. Co-authored-by: Isaac * OMNI-1193: drop sandbox_target from scheduled_tasks sandbox_target was a nullable, persist-only column with no consumer. Removed because Isaac scheduled-task proto has no compute-target field (no merge-compat value) and compute-agnosticism is expressed by the task carrying no compute preference at all — the fire path resolver decides where to run. Co-authored-by: Isaac * OMNI-1193: drop harness_override from scheduled_tasks harness is not an independent knob in Omni — it is a property of the agent (agent_id); the composer harness/agent picker selects the agent_id and there is no independent harness-override control. A routine wanting a different harness points at a different agent_id, so harness_override on scheduled_tasks was a dead column with no consumer. Only removes harness_override from the scheduled_tasks feature. model_override and reasoning_effort stay (real independent knobs), and conversations.harness_override is untouched. Co-authored-by: Isaac * OMNI-1193: align owner_user_id width to String(128) owner_user_id is written at fire time as a LEVEL_OWNER grant into session_permissions.user_id, which is String(128). Every user-identity column in the schema is String(128); the scheduled_tasks 255 was the sole outlier and, being wider than the column it feeds, a >128-char value could store but fail the grant write. 128 stays well under the MySQL utf8mb4 indexed-key ceiling, so index safety is unchanged. Co-authored-by: Isaac * OMNI-1193: align workspace width to String(2048) scheduled_tasks.workspace and conversations.workspace are the same concept (an absolute filesystem path where the runner starts). conversations uses String(2048); ours was the lone Text divergence. Neither is indexed, so this is a consistency change, not functional — matching conversations makes the mapping obvious. Co-authored-by: Isaac * OMNI-1193: fix stale scheduled_tasks doc comments Documentation-only. No schema/type/logic changes. - store module docstring: recurring-only (drop stale "or one-shot") - create() docstring: state enum is active/paused/deleted (drop stale "completed") - base_branch param docstring: genericize (drop Isaac-person name) Co-authored-by: Isaac * OMNI-1193: adapt scheduled_tasks to post-merge db_models split Upstream omnigent-ai#2341 replaced the single class Base with OmnigentBase + ConversationBase. Repoint SqlScheduledTask/SqlScheduledTaskRun to OmnigentBase (control-plane/AP tables, siblings of policies/hosts/ user_daily_cost), NOT ConversationBase (conversation data-plane, may live on a separate physical DB). Also re-parent our alembic migration: omnigent-ai#2341 added two migrations after z5, so repoint z6 down_revision z5a2b3c4d5e6 -> bb2c3d4e5f6a (the new head) to linearize the chain to a single head. Co-authored-by: Isaac * OMNI-1193: drop scheduled_tasks.metadata column Per PR review (aravind-segu): the metadata blob's only intended use was source_schedule_id provenance on rows migrated from an external scheduler — a single field better expressed as a typed column than a catch-all blob, and not written by this persistence-only PR (always "{}"). Remove it now; a typed column can be added if/when the external-scheduler merge lands. Drops the column across model, migration, entity, store ABC + impl, and updates the store + migration tests. 82 tests pass; ruff clean. * OMNI-1193: store scheduled_task ids as Binary(16) UUIDs Per PR review (aravind-segu): convert the owned scheduled-task id PKs to 16-byte UUIDs, aligning with the in-flight repo-wide Binary(16) UUID convention. Adds a Uuid16 TypeDecorator (canonical UUID string in Python, BINARY(16) on MySQL / BLOB/BYTEA elsewhere — same cross-dialect approach as the existing _CKSUM32 digest column). Converts scheduled_tasks.id, scheduled_task_runs.id, and the scheduled_task_runs.scheduled_task_id self-ref. Cross-table reference columns (agent_id, conversation_id, last_run_conversation_id) stay String since their referents (agents.id, conversations.id) remain String PKs. Updates the model, migration, entity + store docstrings, and both test suites to use UUID-valued ids. 82 tests pass; ruff + mypy clean. * OMNI-1193: add execution_target + host_id to scheduled_tasks Persist where a routine fires, for the M2 sandbox/connected-host resolver (no fire-path logic yet — persistence only, like the rest of this PR): - execution_target: connected_host | managed_sandbox — the strategy the fire path resolves at run time (connected_host → owner's live host; managed_sandbox → provision/adopt a sandbox). Int-coded enum (connected_host=1, managed_sandbox=2) matching the state/kind/status pattern, server_default=1, CHECK IN (1,2). Existing rows default to connected_host (the V1 behavior). - host_id: nullable String(64) — for connected_host, the specific host to pin (relates to hosts.host_id; no DB FK, Rule R032). NULL = owner's freshest online host; always NULL for managed_sandbox (provisioned under a deterministic id at fire time). Stays String, not Uuid16 — hosts.host_id is String and this PR doesn't own that table. No per-routine provider column (provider comes from deploy config) and no auth columns (identity rides on the resolved host). Threaded through model, migration, entity, store ABC + impl, and the enum codec, with round-trip + CHECK + default tests. 90 tests pass; ruff + mypy clean. * refactor(db): read Uuid16 back as bare hex to match schema-wide UUID convention Flip Uuid16.process_result_value from the dashed canonical form (str(uuid.UUID(...))) to the bare 32-char hex string (.hex, no dashes), aligning omnigent-ai#2247's scheduled-task id representation with omnigent-ai#2228's bare-hex form so that PR's rebase is a no-op on representation. The 16 DB bytes are unchanged — only the Python-side read-back string differs. Also flip the test id-mint helper and the byte-ordering test literals to bare hex so round-trip assertions hold, and update Uuid16 / ScheduledTask docstrings. Includes the staged migration re-chain onto the current upstream alembic head (down_revision bb2c3d4e5f6a -> 9d820f91deef). Co-authored-by: Isaac * docs(routines): strip internal PR/scheduler scaffolding from OSS comments Remove self-referential PR-sequencing language ("This PR persists …", "a later PR", "(future) scheduler", "persists the shape only") and internal migration/merge-roadmap references ("external scheduler", "reference platforms", MySQL roadmap clause) from docstrings and inline comments in the Routines feature files. No code, type, or schema changes — comment/docstring lines only. * fix(store): resolve three blocking review findings on ScheduledTaskStore Finding 1: update() could not clear host_id or last_run_conversation_id to NULL because None was overloaded as both "unchanged" and "set to NULL". Introduce a module-level _UNSET sentinel; None now means "set to NULL" for those two nullable fields. ABC kept in sync. Finding 2: delete() orphaned scheduled_task_runs rows (no DB-level FK per Rule R032, so cascade is application-owned). Delete the task's runs in the same session before removing the task row. Finding 3 (doc-only): two :param id: docstrings in db_models.py said "canonical UUID string" (dashed) when Uuid16.process_result_value returns bare 32-char hex (no dashes). Aligned with the entity and Uuid16 docs. All changes covered by new TDD tests (red → green).
… "⋯" menu (omnigent-ai#2600) Long diff lines previously overflowed with no way to wrap them, which is painful in a narrow file-viewer pane (2–3 side by side). Add a "Wrap lines" toggle that soft-wraps long lines in both diff panes (Monaco `diffWordWrap`), persisted like the other view preferences. Fold Find in file, Download, and the diff-only toggles (wrap lines, hide whitespace) into a single "View settings" (⋯) menu, mirroring GitHub's diff-settings menu and freeing toolbar width. Toggles keep the menu open; actions close it. Active state shows a check mark, except whitespace whose eye icon already flips open/closed. Co-authored-by: Isaac
* test(e2e-ui): add a populated-sidebar visual snapshot Seed a fixed session list covering every sidebar row type (Pinned, Projects group with an expanded folder + nested chat and an empty folder, flat Sessions with needs-response and running badges) so the row-alignment surface is gated. The empty-landing baseline stubs sessions empty, so that surface was previously untested — the area PR omnigent-ai#2596 touched. Determinism: page.route stubs, a fixed page.clock so relative time pills don't drift, and a no-op /v1/sessions/updates socket. Baseline PNG generated by CI in the pinned image (label update-ui-snapshot). Co-authored-by: Isaac * test(e2e-ui): regenerate visual baselines --------- Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
…g panes (omnigent-ai#2604) ## Related issue N/A ## Summary - **Reload watcher: skip gitignored files.** The pod supervisor reloaded the backend on every `*.py` change under `omnigent/`, including gitignored files the build regenerates (notably `omnigent/_build_info.py`), causing needless reloads. It now builds a gitignore matcher from the repo's root `.gitignore` and `.git/info/exclude` and skips ignored paths — including files inside ignored directories (`build/`, `dist/`, `*.egg-info/`, …), matching git. - **`--debug` flag.** Logs every observed file change into the combined pane as `watch: reload trigger <path>` or `watch: skip <path> (<reason>)`, so it's clear which change triggered (or didn't trigger) a reload. Quiet by default. - **Pager log panes.** Per-process log panes are now a `less`-style pager with line/half/full-page movement, top/bottom jumps, follow-tail, line wrap, and forward/back incremental search (see the README Keys table). ## Test Plan - `cargo build`, `cargo clippy --all-targets`, `cargo fmt --check` — clean. - `cargo test` — passes single-threaded (the parallel-only flake in `create_skips_seed_when_real_config_absent` is a pre-existing env-var race in pod.rs, unrelated to this change). - Verified `classify()` against the real repo `.gitignore`: `omnigent/cli.py` and `omnigent/inner/foo.py` reload; `_build_info.py`, `build/`, `*.egg-info/`, and `server/static/web-ui/` are skipped as gitignored; `__pycache__` and non-`.py` are skipped. - `omnidev --help` shows the new `--debug` flag. ## Demo N/A — pager-pane UI recording to be attached on the PR. ## Type of change - [ ] Bug fix - [x] Feature - [x] UI / frontend change - [ ] Refactor / chore - [ ] Docs - [ ] Test / CI - [ ] Breaking change ## Test coverage - [x] Unit tests added / updated - [ ] Integration tests added / updated - [ ] E2E tests added / updated - [x] Manual verification completed - [ ] Existing tests cover this change - [ ] Not applicable ## Coverage notes Watcher classification is covered by unit tests in `watcher.rs` (.py filter, `__pycache__`, gitignored file, file inside a gitignored dir). The gitignore behavior was additionally verified against the real repo `.gitignore`, and the `--debug`/pager panes were checked manually — the interactive TUI has no automated harness. ## Changelog `omnidev` no longer reloads on gitignored files, adds `--debug` to trace reload triggers, and its log panes are now searchable `less`-style pagers Co-authored-by: Isaac
…ent-ai#2602) The session-row kebab / right-click menu opened "Add to project" / "Move session" as a side-flyout submenu (C.Sub/SubTrigger/SubContent). On mobile there's no horizontal room for a side flyout, so it overflowed and didn't work. On mobile, the project item is now a plain menu item that swaps the menu body in place: a local `view` state ('main' | 'projects') replaces the main actions with the existing ProjectPickerMenu (search + list + Create new project) plus a chevron-left "Back" row that returns to the main view. Selecting the item and Back both preventDefault so the menu stays open rather than closing on select. Desktop keeps the native side-flyout submenu unchanged. Because the menu body is authored once through the shared MenuComponents bundle, the in-place view works for both the kebab dropdown and the right-click context menu families. Co-authored-by: Isaac
…SDK (omnigent-ai#2605) * refactor(hindsight): rename memory extra to hindsight; gate tools on SDK ## Related issue N/A ## Summary - Rename the optional install extra `memory` -> `hindsight` (the extra that pulls `hindsight-client` for the Hindsight long-term memory tools), so the extra name matches the tools it enables. Updates `pyproject.toml`, `uv.lock`, the install hint, docstrings, and `examples/remy/config.yaml`. - Hide the three Hindsight tools from the builtin list when `hindsight-client` is not installed: they're now absent from `BUILTIN_NAMES` / `INSTANTIABLE_BUILTINS` and not instantiable, and the onboarding `list_builtin_tools` helper no longer advertises them. The presence probe uses `importlib.util.find_spec` so the SDK and its deps (aiohttp, ...) stay lazy. ## Test Plan - `ruff format` + `ruff check` clean; `pre-commit run` passes on all changed files (including the `normalize-uv-lock-registry` hook). - `pytest tests/tools/builtins/test_hindsight.py tests/tools/builtins/test_registry_unified.py tests/spec/test_validator.py` -> 79 passed; full `tests/tools tests/spec tests/onboarding` -> green (one unrelated `databricks_sdk_installed` failure was an env artifact from running `--extra dev` instead of `--extra all`; passes with `--extra all`). - New `test_hindsight_tools_absent_from_registry_when_sdk_missing` hides `hindsight_client` from the finder, reloads the registry, asserts the tools are absent + not instantiable, and restores the finder in `finally` (no state leakage — verified by running it before the registry-size test). ## Demo N/A ## Type of change - [ ] Bug fix - [ ] Feature - [ ] UI / frontend change - [x] Refactor / chore - [ ] Docs - [ ] Test / CI - [x] Breaking change ## Test coverage - [x] Unit tests added / updated - [ ] Integration tests added / updated - [ ] E2E tests added / updated - [ ] Manual verification completed - [x] Existing tests cover this change - [ ] Not applicable ## Coverage notes The extra rename is exercised by the existing registry-size test (which lists the hindsight names) and the lock line. The gating is covered by the new unit test. Manually verified `_hindsight_available()` returns True with the SDK and False when hidden from the finder, in both the registry and the onboarding helper. ## Changelog `omnigent[memory]` is renamed to `omnigent[hindsight]`; the Hindsight memory tools are now hidden from the builtin list when `hindsight-client` is not installed. * fix(uv.lock): complete hindsight extra rename in lock metadata The rename commit updated the requires-dist marker but missed the provides-extras list and the package optional-dependencies mirror, so `uv sync --locked` (every CI job's install step) failed.
…#2603) * fix(spawn): clarify sys_session_send schema to prevent agent/title-in-args confusion Pi was putting 'agent', 'title', and 'session_id' inside the args object instead of as top-level fields. It also tried passing 'model' via session_id mode where it has no effect. - Tool description now explicitly states that agent/title/session_id are TOP-LEVEL fields and model/purpose go INSIDE args, with a concrete correct example. - args description now warns against putting agent/title/session_id inside args, and clarifies that model only applies on session CREATE (first named send), not on continuation or session_id sends. * revert(pi-native): remove pi_native_credentials change from sys_session_send fix * fix(pi-native): route non-Claude models to correct provider in models.json and --provider arg Two fixes for model override with non-Claude models (GLM, GPT, etc.): 1. to_models_config: don't append the selected model to the Anthropic (omnigent) provider if it already lives in an additional_providers entry (omnigent-openai/openai-completions). Previously GLM was appended to the anthropic-messages provider, causing Pi to attempt to call GLM via the wrong wire protocol. 2. pi_native_provider_launch: pass --provider omnigent-openai (not omnigent) when the selected model lives in an additional_providers entry. Previously --provider omnigent was always passed, so Pi couldn't resolve models that only exist under omnigent-openai.
## Related issue N/A ## Summary - Adds `omnigent://<hostname>/c/<session_id>` deep links to the Electron desktop shell: an OS-clicked link opens that session on that server, reusing an existing window in-place when one is already on it. - Window handling is the careful part — a pure, unit-tested `chooseDeepLinkStrategy` picks reuse-in-place (focus + tell the SPA router to navigate, no reload), reuse-with-reload (pinned but mid-SSO), open-known (frictionless new window), or consent-unknown (native dialog, since pinning a new origin is a privilege grant). The workspace mount probe runs only AFTER consent, so a link to an attacker-chosen server makes no pre-consent network request. - The window's server identity (`serverUrl`, used by `omnigent host --server`) is kept clean of the `/c/<id>` path while the load URL carries it; the mount-aware join keeps `/ml/omnigents` from being dropped. ## Test Plan - `cd web/electron && node --test` — 195 tests (19 new deep-link decision tests + wiring guards). - `cd web && npx tsc -b` clean; `npx vitest run src/hooks/useIdleNotifications.test.tsx src/lib/nativeBridge.test.ts src/shell/AppShell.test.tsx` — 160 pass. - Manual (dev, local server `127.0.0.1:6767`): warm-start reuse-in-place — with the app connected and viewing conversation A, `npm start -- 'omnigent://127.0.0.1:6767/c/<B>'` (second terminal) switches the existing window to B in-place, no reload. Confirmed via the diagnostic logs: `strategy=reuse-inplace ... send open-path /c/<B>`. Requires the web UI rebuilt (`cd web && npm run build`) since the desktop loads the server's built SPA. ## Demo N/A — no visible UI change beyond in-app navigation triggered by an external link. ## 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 Unit tests cover the pure decision logic (`web/electron/test/deepLink.test.js`: parse + the reuse/reload/open-known/consent-unknown table) and `web/electron/test/main.test.js` wiring guards (open-url/second-instance/argv ingestion, serialized queue, scheme registration, mount-aware path join, clean serverUrl, and the post-consent probe placement). The OS-dispatch + window orchestration can't be unit-tested without an Electron launch, so it was verified manually with the local server (warm-start reuse-in-place confirmed via logs). ## Changelog `omnigent://<hostname>/c/<session_id>` links open that session in the desktop app, reusing an open window on that server in-place
…mnigent-ai#2609) A claude-native cold resume rebuilds Claude Code's local transcript from Omnigent's stored items. Image tool results (screenshots) are persisted as a stringified content-block array, and the rebuild dropped that string straight into the `tool_result` content. On `claude --resume`, Claude sent the base64 to the API as plain *text*, so a single screenshot cost ~250K tokens instead of the ~1.5K an image block costs. A conversation that fit comfortably while live then overflowed the context limit on reconnect ("Prompt is too long"), and the model no longer saw the screenshots as images. Rehydrate `text`/`image` block arrays back into real content blocks so the resumed request sends images as images. Non-block outputs (plain text, other JSON shapes, API-unsupported block types) stay raw strings, so their resume behavior is unchanged. Measured on the reported conversation: base64-as-text drops from ~253K tokens to 0, with all 6 screenshots restored as image blocks. Co-authored-by: Isaac
…mnigent-ai#2610) * perf(web): reduce sessions API calls on initial page load On the landing page, ChatPage fired two redundant GET /sessions calls: - useConversations() with includeArchived=false, duplicating the sidebar's useConversations('', true) which uses the same endpoint with a different cache key - useAgents() unconditionally, even though the agent picker is only visible once a session is open Fix both: 1. ChatPage's useConversations() now passes includeArchived=true, sharing the cache key with the sidebar and eliminating the duplicate fetch. 2. useAgents gains an option; ChatPage passes enabled=!!urlConvId so the sessions?limit=100 scan is skipped on the landing screen where NewChatLandingScreen's useAvailableAgents already covers agent discovery. Net effect: 5 → 3 GET /sessions calls on initial load. * fix(web): consolidate useConversations callers to share sidebar cache key AppShell, usePermissions, RunnerHealthProvider, and useIdleNotifications all called useConversations() with the default includeArchived=false, creating a separate cache entry from the sidebar's includeArchived=true fetch and causing a duplicate GET /sessions?limit=20 call on every load. Switch all four to useConversations("", true) so they share the sidebar's ["conversations", "", true] cache key. The behavior change is minimal: these hooks only inspect existing sessions by id or aggregate counts, so seeing archived sessions in the list is either neutral or beneficial (e.g. useCanEdit can now resolve permissions on an archived session). * fix(web): fix CommandPalette cache-key mismatch after includeArchived consolidation CommandPalette was calling useConversations(query, false), designed to share AppShell's old useConversations() cache entry. After switching all callers to includeArchived=true, CommandPalette's false key no longer matched anything, reintroducing the duplicate fetch. Switch to includeArchived=true and filter archived rows client-side in the sessions memo so the palette still only lists active sessions. * test(web): update CommandPalette test for includeArchived=true
## Related issue N/A ## Summary Main's lint workflow failed because the desktop update E2E test retained extra trailing blank lines. Apply Ruff's formatting so the all-files pre-commit check remains clean. ## Test Plan - `.venv/bin/pre-commit run --all-files --show-diff-on-failure` ## Demo N/A ## Type of change - [ ] Bug fix - [ ] Feature - [ ] UI / frontend change - [ ] Refactor / chore - [ ] Docs - [x] Test / CI - [ ] Breaking change ## Test coverage - [ ] Unit tests added / updated - [ ] Integration tests added / updated - [ ] E2E tests added / updated - [ ] Manual verification completed - [ ] Existing tests cover this change - [x] Not applicable ## Coverage notes Formatting-only correction; the full all-files pre-commit suite passes. Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
…2886) * perf(web): lazy-load Shiki so it leaves the main bundle Shiki's engine (including its WASM regex engine) was pulled into the app's main entry chunk even when no code block ever rendered. Two eager importers kept it there: code-block.tsx and the @streamdown/code highlighter plugin wired into chat markdown via streamdown-security.ts. Defer both. code-block.tsx now imports shiki at highlight time inside its existing per-language cached getHighlighter helper. A new lazyCodePlugin wraps @streamdown/code, satisfying Streamdown's CodeHighlighterPlugin contract (default themes synchronously; highlight() returns null until the engine loads, then resolves tokens through the callback) while deferring the @streamdown/code import — and with it shiki — to the first highlight call. Rendering, theming, language handling, and public APIs are unchanged. Shiki now splits into a separate on-demand chunk: the main entry chunk drops from 4,551.81 kB to 4,356.25 kB (~196 kB raw, ~60 kB gzip), and Vite no longer reports the ineffective-dynamic-import warning. Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com> * test(web): prove lazy Shiki highlighting through Streamdown + harden callback Address cross-vendor review of the lazy-Shiki change. Verified lazyCodePlugin matches Streamdown's real consumption contract: HighlightedCodeBlockBody runs highlight() inside a useEffect and stores the result via setState — `let r=o.highlight({...}, c=>{i(c)}); r&&i(r);` (streamdown/dist/highlighted-body-OFNGDK62.js). Returning null keeps the raw code in state; the callback calls setState, forcing a re-render with the highlighted tokens. The highlighted body is itself React.lazy + Suspense (chunk-BO2N2NFS.js), so raw text paints first and highlighting streams in. So the null-then-callback path reliably produces highlighted output. - Add streamdownCodeHighlight.test.tsx: renders MessageResponse (which uses STREAMDOWN_PLUGINS with code: lazyCodePlugin) on a fenced code block, asserts raw code shows immediately, then waits for the lazy @streamdown/code import + callback and asserts multiple per-token colored spans appear (Streamdown colors tokens via the --sdm-c CSS custom property). - Harden highlight() against double callback invocation with a fire-once guard so the callback runs exactly once whether the real plugin resolves via its return value (sync cache hit) or its own callback. Add a unit test asserting the callback fires exactly once. - Clarify supportsLanguage: Streamdown has zero call sites for it/ getSupportedLanguages, and highlight() falls back to "text" for unknown languages, so the optimistic pre-load answer is safe. Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com> * test(e2e): assert chat code blocks lazy-load Shiki highlighting Regression guard for the lazy-Shiki change: seeds a deterministic assistant message with a fenced code block and asserts the observable syntax-highlighted token spans appear once the on-demand Shiki import resolves, proving highlighting survives the deferral. Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com> * style: apply ruff format to lazy-Shiki e2e test `ruff format` collapses the multi-line `wait_for_function` string concat onto one line; matches the pre-commit CI fix so the check passes. Co-authored-by: Isaac * test(ui-snapshot): wait for lazy Shiki highlight before chat capture The lazy-Shiki change defers `@streamdown/code`, so the fenced code block first paints raw and only re-renders with syntax-highlighted token spans once the on-demand import resolves. The visual snapshot was capturing the pre-highlight frame, drifting from the committed (highlighted) baseline and failing the UI Snapshot gate. Wait for the `--sdm-c` token spans (same signal the lazy-Shiki e2e test uses) before capture so the render is highlighted and matches the existing baseline — no baseline regen needed. Co-authored-by: Isaac * test(ui-snapshot): update chat baseline for lazy-Shiki render The lazy-Shiki change defers `@streamdown/code`; in the pinned headless Playwright renderer the fenced code block paints uncolored even after the token spans mount (confirmed across two CI runs — the DOM wait added last commit does not repaint the colors at capture). Highlighting works in a real browser, so this is a snapshot-environment artifact, not a UX regression. Adopt the CI-rendered baseline (byte-identical to the gate's render) so the visual gate matches, and keep the token-span wait so the capture is the settled post-import DOM rather than a mid-tokenization frame. Co-authored-by: Isaac * test(ui-snapshot): fix chat snapshot flake on lazy Shiki highlight The chat baseline flaked between highlighted and raw code renders. The lazy `@streamdown/code` import mounts the colored token spans a frame before the browser composites their colors, so waiting on span presence raced the paint — the screenshot sometimes caught the raw frame. Wait until the tokens resolve more than one distinct computed color (the raw fallback is a uniform `inherit`), then flush two animation frames so the colors are painted before capture. Restore the highlighted baseline as the correct target (a prior commit had adopted a raced raw render). Co-authored-by: Isaac --------- Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com> Co-authored-by: Daniel Lok <daniel.lok@databricks.com>
) Bump the SDK-proxy harness subprocess and native CLI pane idle-reap defaults from 30 minutes to 1 hour so short lulls between turns don't tear down live sessions. Both defaults intentionally mirror each other; the runner-level watchdog was already at 1 hour, so it now consistently outlives the inner reapers it contains. Both remain env-overridable. Co-authored-by: Isaac
…mnigent-ai#2989) * fix(telemetry): track sdk harness name in SessionCreatedEvent SDK sessions (claude-sdk, openai-agents, codex, etc.) previously emitted `harness: null` on the SessionCreatedEvent because only native agents have a `native_agent.harness` attribute. Fall back to `_resolve_harness(conv)`, which already handles harness_override and spec lookup, so every harness kind is now represented in telemetry. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * feat(session-ui): support HTTP headers on MCP servers in session UI Adds the ability to set, view, and edit HTTP headers (e.g. Authorization) on HTTP-transport MCP servers through the session agent info panel. Backend: - MCPServerSummary now includes a headers field; values are always [REDACTED] in API responses (only key names are exposed). - UpsertMCPServerRequest accepts headers: dict[str, str] | None. None preserves existing headers; {} clears them. - New _apply_headers() helper replaces the old _preserve_keys() call for headers so edits via the UI actually take effect rather than always restoring the bundle's headers. - Fixed sessions.py and builtin_agents.py MCPServerSummary construction to populate headers (previously always returned {}), which caused headers to disappear when reopening the edit dialog. Frontend: - McpFormState/UpsertMcpServerInput/McpServerSummary all carry headers. - McpServerManagerDialog shows a key-value editor for HTTP headers (add row with +, remove with x, values show as [REDACTED] for existing headers). - Fixed AgentInfoButton popover closing when the MCP manager Dialog opens: uses onInteractOutside/onFocusOutside on PopoverContent to suppress Radix's outside-click dismiss while a nested dialog is open. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(create-agent): accept KEY: VALUE format in headers textarea parseKVLines only split on '=' so users typing the natural HTTP header format (Authorization: Bearer ...) got silently dropped. Now accepts both '=' and ':' as separators, taking whichever comes first. Updated the placeholder to show the colon form. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(mcp-headers): preserve real secrets when [REDACTED] sent on edit When a user opens the MCP server edit dialog, header values come back as [REDACTED] from the API. If they save without changing those values the client sends { Authorization: '[REDACTED]' }, which was being written literally into the bundle YAML — overwriting the real token. _apply_headers now treats a value equal to the '[REDACTED]' sentinel for an existing key as 'preserve the stored value', restoring it from the existing bundle entry instead of writing the placeholder. Also reverts unrelated package-lock.json churn and adds a round-trip integration test covering the edit-with-existing-headers scenario. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * chore: regenerate openapi.json for MCP headers fields Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(mcp-headers): send {} to clear headers when all rows removed When editing a server and removing all header rows, the frontend was sending null (preserve) instead of {} (clear), so stale auth tokens were silently kept in the bundle. null now only means 'preserve' for new servers (no originalName). Editing an existing server with zero rows sends {} to explicitly clear. Adds integration test covering the clear-all path. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> --------- Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
…ai#2999) When deleting a conversation with N descendants, each FTS row was deleted in a separate DELETE statement. Replace the per-ID loop with a single DELETE ... WHERE conversation_id IN (...) via the new delete_fts_by_conversation_ids helper. The single-ID function is kept intact for other callers. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
…nigent-ai#2985) Runners were reported to be "randomly dying" with no explanation in the runner log — an uncaught exception left only a bare traceback on stderr, and orderly shutdowns (signal, idle timeout, tunnel drop, parent death) logged nothing at all. Attribute the exit on each hookable path so the runner log always says why it stopped: - uncaught exceptions via sys.excepthook (with traceback) — the silent-crash case - SIGTERM/SIGINT, recording the specific signal - idle timeout, websocket tunnel close, and the parent-death hard-exit backstop (logged at the os._exit call site, which skips atexit hooks) - fatal server rejection keeps its concise stderr message SIGKILL and os._exit remain uncatchable in-process; the absence of an exit line is itself the signal that the runner was killed uncatchably. Co-authored-by: Isaac
…mnigent-ai#3022) Per-parent child-title uniqueness was enforced by a UNIQUE index on (workspace_id, parent_conversation_id, title_hash), where title_hash was a 16-byte sha256(title)[:16] mirror of title maintained solely to key that index. Reads never used it (the runner's find-or-create pre-check filters title, whose 3rd index column was title_hash), so it was pure write amplification. Move the check into create_conversation: a per-parent (parent, title) existence SELECT served by idx_conversations_parent, raising NameAlreadyExistsError on a hit. Only children are scoped; top-level (NULL parent) sessions may reuse titles freely, as before. Drop the index, the title_hash column, the two hash helpers, the _CKSUM16 alias, the ORM default and the two rename-path recomputes, and the store's IntegrityError->title translation (the id-PK branch stays). Trade-off: the DB index was the atomic backstop for concurrent same-name spawns (tool calls dispatch concurrently within a turn). The app check is best-effort, so a rare concurrent duplicate spawn now yields a stranded duplicate child + a wasted runner instead of a clean error. Bounded, not corruption; the common repeat-send path is unaffected (served by the runner pre-check). Migration 72e6dceae14f. SQLite drops/recreates idx_conversations_parent by hand around the batch rebuild so its DESC ordering survives; MySQL/Postgres use native DROP COLUMN. Downgrade re-adds title_hash, back-fills it in Python, and restores the unique index. Co-authored-by: Isaac Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* 🐛 fix(codex): Surface native subagents in UI Register subAgentActivity starts before child events hit the stale-thread guard. Cover bridge routing plus real native-spawn and Agents-rail end-to-end journeys. Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com> * ✅ test(codex): Verify subagent completion * 📝 docs(codex): Add native subagent demo --------- Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com> Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
) _COMPACT_LOCKS existed but was never acquired, so concurrent compact events could both observe idle and run at once. Hold a WeakValueDictionary lock per session, recheck status after acquire, and cover the race with a deterministic concurrency test. Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
…ifier (omnigent-ai#2586) Descriptions told the model to cancel with task_id while dispatch already returned handle_id. Align schemas/messages on handle_id and keep task_id as an identical compatibility alias scheduled for removal in 0.8.0. Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
…nt-ai#2587) Malformed JSON previously fell through to {}, which could run a default/no-argument system tool. Require a JSON object and return the canonical structured error before dispatch. Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
…mnigent-ai#2588) Idle shutdown was terminating runners while sys_call_async results were still in flight because has_active_work only checked foreground/harness turns. Keep the runner alive for live async tasks, timers, and parked approvals without pinning on completed or housekeeping work. Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
…nigent-ai#2093) * feat(server): streaming dictation endpoint (local speech-to-text) Adds WS /v1/dictation/stream + GET /v1/dictation availability probe, backed by a lazily-loaded sherpa-onnx streaming transducer (new optional extra: omnigent[dictation]) with optional online re-punctuation. Fills the gap documented in web/electron/README.md: dictation where the browser Web Speech API has no backend, with audio never leaving the operator's infrastructure. A deterministic fake engine (OMNIGENT_DICTATION_ENGINE=fake) keeps CI hermetic and will drive the Playwright e2e test. See designs/server-dictation.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): stream server dictation into the composer mic button When the browser has no Web Speech backend (Electron, Firefox, Chromium), the mic button now falls back to the server recognizer: GET /v1/info advertises dictation_available, an AudioWorklet downsamples the mic to 16 kHz PCM over WS /v1/dictation/stream, and partial transcripts form live in the composer via a replaceable interim region (useDictationInsert) shared by ChatPage and NewChatDialog. Web Speech behavior is unchanged where it works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e-ui): dictation loop against the fake engine Fake mic (Chromium fake media device) -> AudioWorklet -> dictation WS -> OMNIGENT_DICTATION_ENGINE=fake -> transcript lands in the composer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: ruff format + regenerated openapi.json for dictation routes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: prettier formatting Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e-ui): honor plugin context args in the dictation test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: drop the caller-less GET /v1/dictation probe ponytail review: the web UI only reads dictation_available from GET /v1/info, so the dedicated probe endpoint had no caller. Also simplify the engine singleton (config never changes mid-process; tests inject engine_provider) — a failed load still caches nothing, so gaining models doesn't require a restart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: hardware sizing table for dictation models Measured on Apple M-series and an Intel N95 mini-PC: the default Nemotron 0.6B is too slow for N95-class servers (0.6-0.7x realtime); the mid-size streaming zipformer decodes 1.4-2.3x realtime there in ~190 MB and held accuracy in spot checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(server): remote dictation worker relay with local fallback OMNIGENT_DICTATION_REMOTE_URL relays takes to a dictation worker on a beefier LAN box over the existing wire protocol; local models (when installed) serve as a lazy fallback when the worker is down. Ships a standalone single-route worker entrypoint (python -m omnigent.server.dictation_worker). Motivated by real hardware: an N95 main server decodes the default 0.6B model at only 0.6x realtime, but a workstation on the same LAN runs it at 9x. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(deps): pin sherpa-onnx-core + numpy explicitly in the dictation extra sherpa-onnx's wheel metadata declares its native payload package (sherpa-onnx-core, which carries libonnxruntime) inconsistently across platforms, so it was missing from uv.lock — failing the hashed OSV audit in CI and breaking aarch64 installs. Pinning it explicitly fixes both and removes the fetch script's aarch64 fixup. numpy is imported directly by the engine, so declare it instead of riding transitives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: harden dictation take lifecycle (adversarial review findings) Server: the route now closes the engine stream handle on every exit path — an abandoned take (browser vanished mid-dictation) previously leaked the remote relay's worker WebSocket and reader thread, holding a worker capacity slot forever and eventually starving dictation for everyone. Web client, all confirmed by review: - useDictationInsert strips the interim region only when the draft still ends with the exact text it inserted, so dictation can never delete user-typed text; ref bookkeeping moved out of the setState updater (StrictMode double-invokes updaters). - The worklet flushes its partial chunk before stop() tears the graph down — trailing speech under the 100 ms boundary was being clipped from every take. - Client ready/stop budgets now exceed the server's cold-load and worker-flush budgets (40 s / 15 s), so slow first takes and slow tail flushes no longer fail or drop text spuriously. - The 1013 at-capacity close surfaces as "busy — try again" instead of "unavailable", and engine-init error frames surface their message. - A socket close during audio-graph setup now fails the start instead of resolving a dead session that silently drops all audio. - Web Speech network-error fallback is per take, not sticky: a transient blip in real Chrome no longer permanently downgrades the page to the server model, and stale events from the dead recognizer can no longer clobber the live server take's state (which could leave the mic recording while the button showed idle). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: dictation model choices for other languages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(web): format dictation files * fix(server): close dictation takes even when the task is cancelled An ASGI server cancels the websocket handler task on shutdown. The cleanup awaited asyncio.to_thread(handle.close) inside finally, so the CancelledError could arrive before the worker thread ran close() -- about half the time, measured. contextlib.suppress(Exception) never caught it: CancelledError is a BaseException. Create the close task before the first await point and shield it, so it runs to completion while cancellation propagates. Hold a strong ref (asyncio keeps only a weak one) and retrieve the result so a failing close logs instead of warning. Also corrects the comments: an abandoned take is reaped by the ASGI server's ping timeout (~20s), not held forever. Verified against a live worker with OMNIGENT_DICTATION_MAX_STREAMS=1. * refactor(dictation): split out remote, add engine registry, fold beautify Keep this PR focused on local dictation and make future model swaps cheap: - Defer the remote worker (RemoteDictationEngine, dictation_worker.py, and the close-on-cancel machinery that existed to release a worker slot) to a follow-up PR. Remote only helps a narrow deployment; local sherpa runs at many-times realtime on any normal machine, so this does not block testing. - Select engines by name from a registry (register_engine); get_engine and engine_availability resolve from it instead of an if/elif ladder. Adding an engine is one call with a factory + availability probe. - Fold punctuation into the sherpa engine and drop beautify from the DictationStreamHandle protocol. Emitted text is display-ready, so the seam is PCM-in -> text-out -> close; models that punctuate themselves (Whisper, Parakeet) implement nothing extra. Co-authored-by: Isaac * chore: re-trigger CI checks Empty commit to re-run the security scan and CI on this PR. Co-authored-by: Isaac * build(deps): minimize dictation lock diff to sherpa-only, public index The merge re-lock rewrote every uv.lock URL to the Databricks internal index proxy and would fail the public-registry lint. Restore public pypi.org / files.pythonhosted.org URLs so the lockfile diff versus main is only the two dictation packages (sherpa-onnx, sherpa-onnx-core), with no unrelated churn. Co-authored-by: Isaac * fix(web): sync ServerInfo test fixtures with merged capability fields The main merge made single_user/sharing_mode/public_sharing_enabled required on ServerInfo while dictation_available became required from this PR, but four test fixtures each construct a ServerInfo literal missing the other side's fields, failing tsc (and the web build via Docker/E2E-UI). Add the missing fields so every fixture is a complete ServerInfo. Co-authored-by: Isaac --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Ubuntu <kerry.chang@your.hostname.com>
…mnigent-ai#2584) * fix(sessions): avoid GeneratorExit on SSE stream disconnect cleanup Yielding [DONE] from _stream_live_events finally raised RuntimeError on client aclose; keep finally cleanup-only and aclose the subscribe slot. Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com> * chore(openapi): regenerate session stream description Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com> --------- Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
…omnigent-ai#3014) * feat(scheduled tasks): track run completion + expose run history The fire path records a scheduled_task_runs row as `running` and never revisits it, so runs stayed `running` with finished_at=NULL forever even after the agent turn completed (the FU-1 gap confirmed in prior E2E). list_runs also existed in the store but was exposed by no REST route. Add a periodic reconciliation backstop + run-history endpoint: - Store `update_run` (conditional WHERE status=running, idempotent — an already-terminal run is never clobbered and concurrent sweeps can't double-transition) and `list_runs_by_status_all_workspaces` (the sweep source). ScheduledTaskRun entity now carries workspace_id so the sweep can re-enter each run's workspace_scope. - `run_reconciler.py`: a 60s asyncio loop (own module, off the ScheduledTaskScheduler) that reads each running run's conversation and transitions it — completed transcript -> succeeded; a failure label / missing conversation -> failed(code); live_status running/waiting is a cheap pre-filter. A run past a 6h max-age with no terminal state is force-failed (error_code=incomplete) so every run eventually terminates. Wired into the server lifespan next to the scheduler. - `GET /v1/scheduled-tasks/{id}/runs`: owner-scoped run history (404 if not owned), API-stable field naming. No schema/migration change — status codec already had succeeded/failed and the columns (finished_at/error/error_code) already exist. FU-3 + omnigent-ai#2978 semantics intact (owner via user_id; API-stable owner_user_id JSON key). Tests: update_run transitions + idempotency; reconciler classification matrix (completed->succeeded, errored/cancelled->failed, in-flight and young runs left alone, stale->failed(incomplete)); GET runs 200/empty/404. Full targeted suite green (155). E2E on a live server + connected host: a real timer fire's run flipped running->succeeded with finished_at set (the exact thing that stayed running before), readable via the runs endpoint; honest-fail still records failed(no_online_host) and the sweep leaves terminal runs untouched. Co-authored-by: Isaac Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com> * feat(scheduled tasks): make run completion event-driven (replaces poll) Replaces the 60s all-workspaces reconciliation poll from the previous commit with an event-driven completion hook + a poll-free orphan backstop, matching how the sibling scheduled-task systems reconcile (at a lifecycle boundary, not on a timer). Primary mechanism: a completion hook (``session_live_state.persist_scheduled_run_completion``) fired from ``_publish_status`` the instant a fired conversation's turn reaches a terminal edge (idle -> succeeded, failed -> failed+error_code). It rides the same long-lived SSE relay that already persists ``live_status`` for a browserless scheduled fire, routed through the same ordered/contextvar-copying executor so the run's ``workspace_scope`` reaches the write thread. A reverse lookup (``get_running_run_by_conversation``, backed by a new ``(workspace_id, conversation_id)`` index) finds the run; the idempotent conditional ``update_run`` (WHERE status=running) transitions it and never clobbers an already-terminal row. For the common (non-scheduled) conversation the lookup returns None and the hook is a cheap no-op. Orphan backstop (no periodic poll): the ``ScheduledRunReconciler`` becomes a ONE-SHOT startup sweep (reconciles runs left ``running`` by a restart mid-fire), and a lazy-on-read pass at ``GET /v1/scheduled-tasks/{id}/runs`` force-fails a task's runs past the 6h max age (``incomplete``). Together they keep the invariant "every run eventually reaches a terminal state" without a recurring background sweep. One migration: the ``conversation_id`` index. FU-3 / omnigent-ai#2978 owner semantics, the ``GET /runs`` response shape, and the fire-time ``_record_run`` writes are unchanged. Co-authored-by: Isaac Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com> * refactor(scheduled tasks): drop startup sweep, lazy-on-read is sole backstop Simplifies the orphan backstop per review. The event hook already transitions every normal run the instant its turn ends; the boot-time startup sweep is removed entirely (fewer moving parts). A run orphaned by a mid-fire restart that nobody ever opens staying `running` in the DB is harmless until read, and reading it fixes it. Changes: - Remove `run_startup_sweep`, the `ScheduledRunReconciler` class, and its lifespan wiring in app.py. `run_reconciler.py` reduces to the stale-run policy: the constants + a shared `force_fail_stale_runs` helper (pure age-based, no conversation I/O). - Run the lazy force-fail-stale reconcile on BOTH read endpoints: - `GET /v1/scheduled-tasks/{id}/runs` (detail, already there). - `GET /v1/scheduled-tasks` (list, ADDED) — force-fail the owner's tasks' runs still `running` past 6h so a Tasks-list badge never shows a stale orphan as `running`. Owner-scoped indexed query (`list_running_runs_for_tasks`), conditional `update_run`, no per-run conversation read. - Drop the now-unused `list_runs_by_status_all_workspaces` store method. Net mechanism: (a) event hook = primary, instant terminal transition; (b) lazy-on-read force-fail-stale on list + detail = the only orphan backstop. No startup sweep, no periodic poll of any kind. Keeps the 6h STALE_RUN_MAX_AGE_SECONDS invariant "every run eventually terminal". Co-authored-by: Isaac Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com> * refactor(scheduled tasks): drop dead ScheduledTaskRun.workspace_id field The ``ScheduledTaskRun`` entity carried a ``workspace_id`` field solely so the cross-workspace reconciler sweep could re-enter each run's ``workspace_scope`` before acting on it. That sweep is gone — completion is event-driven and the lazy-on-read backstop both run inside a single ambient ``workspace_scope`` — so the field has no reader. Its only consumer was the deleted ``_reconcile_run``. Remove the field from the entity dataclass and drop the ``workspace_id=`` line in ``_run_to_entity``. The DB column ``scheduled_task_runs.workspace_id`` (the real tenant partition key) and its index are unchanged; the store still filters every query on ``current_workspace_id()``. Co-authored-by: Isaac Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com> * refactor(scheduled tasks): PR polish — comment fix, fired_at age basis, hook wiring test Addresses three review findings on the FU-1 run-completion PR: - Fix a stale finally-block comment in app.py: it still said the run reconciler is "a one-shot startup sweep (no periodic task to cancel)", but the startup sweep was removed — completion is event-driven + lazy-on-read, so there is no reconciler task at all. Comment now says only the per-job scheduler needs stopping. The scheduled_task_scheduler.stop() logic is unchanged. - Measure the lazy-on-read stale window from fired_at (falling back to scheduled_at when a run never recorded a fire time), not scheduled_at. A run that fired late no longer gets a shortened effective window — the 6h clock starts when dispatch actually began. Locked by two unit tests: a run fired >6h ago is force-failed; a run scheduled >6h ago but fired recently is left alone. - Add integration coverage for the primary completion mechanism at the _publish_status seam: drive the real _publish_status(conversation_id, "idle") / "failed" edge (the way the SSE relay does) and assert the scheduled_task_run transitions running -> succeeded / failed(+error_code) with finished_at set, through the hook + shared session_live_state executor (workspace_scope contract exercised, not bypassed). This locks the wiring so a future _publish_status refactor can't silently break scheduled-run completion. Co-authored-by: Isaac Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com> --------- Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
…mnigent-ai#3025) Reintroduce the remote path split out of the initial dictation PR, now as a registered engine rather than a special-cased branch. - Register a `remote` engine (OMNIGENT_DICTATION_ENGINE=remote) that relays each take to a dictation worker over the same wire protocol the browser speaks. Selected purely by env var — OMNIGENT_DICTATION_REMOTE_URL points at the worker; no CLI integration, keeping the surface small for a niche deployment (weak main server + a beefier LAN box). - Ship the standalone worker (python -m omnigent.server.dictation_worker): create_dictation_router served on its own, unauthenticated, LAN-only. - Per-take fallback to the local sherpa engine (lazy) when the worker is unreachable and models are installed. - Widen the web client's ready/stop timeouts to outlast the worker's cold-load budget. websockets is already a core dependency, so no new package. The engine slots into the registry with no changes to the route, protocol, or selection logic. Co-authored-by: Isaac Signed-off-by: kerry.chang <kerry.chang@your.hostname.com> Co-authored-by: Ubuntu <kerry.chang@your.hostname.com>
…-ai#2589) Skipping unresolvable function policies left an empty gate that allowed every tool call. Install a deny sentinel instead so a misconfigured policy cannot disappear silently. Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
4e110e2 to
6fc409d
Compare
…copy-at-spawn) (omnigent-ai#1041) * feat(spawn): add file_ids to sys_session_send schema (omnigent-ai#900) Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(server): add lineage-scoped file copy endpoint for subagent file passing (omnigent-ai#900) Add POST /v1/sessions/{session_id}/resources/files:copy. The destination (child) session copies parent-owned files authorized by spawn lineage: the source must be the destination itself or an ancestor up the parent_conversation_id chain. Each file is re-stored as a new child-scoped row so the child reads its OWN copy — no cross-session read grant is created, preserving the session-scoping invariant. Co-authored-by: Isaac Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * feat(runner): forward file_ids from parent to subagent via copy-at-spawn (omnigent-ai#900) Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * test(e2e): file passing from parent agent to subagent (omnigent-ai#900) Co-authored-by: Isaac Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(omnigent-ai#900): harden file copy — strict-ancestor source, rollback partial copies, delete phantom child Address codex review findings: - Reject self as copy source; require a strict parent_conversation_id ancestor. - Prefetch blobs during validation + roll back created rows/blobs on mid-batch storage failure, restoring true all-or-nothing semantics. - Delete the freshly-created server child session when copy-at-spawn fails, so a failed spawn cannot leave a phantom child that poisons a same-(agent,title) retry. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * test(omnigent-ai#900): update sys_session_send schema assertions for new file_ids field Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(omnigent-ai#900): regenerate openapi.json for copy endpoint schema Docstring reformatting (rst -> markdown) and the sessions -> session_resources tag move drifted the committed spec from the generator output, failing the openapi-drift gate. Regenerate to match. Co-authored-by: Isaac Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(omnigent-ai#900): tear down child + defer resource events on copy-at-spawn failure Two partial-failure bugs surfaced by cross-model (codex) review of the copy-at-spawn path: P1 (tool_dispatch): a named send that copied files successfully but then failed to POST the child message only unregistered runner-local state — it did not delete the freshly-created child like the copy-failure branch does. That left a phantom child (poisoning a same-(agent,title) retry) and orphaned the already-copied child-scoped file rows. Extract the teardown into `_teardown_failed_child` and call it on every post-copy failure path so they undo identically. P2 (sessions copy endpoint): `files:copy` published and persisted `session.resource.created` inside the per-file loop, before the batch was known to succeed. A later write failure rolled back the file rows/blobs but not those events, so clients saw phantom files. Defer all resource events to a second loop that runs only after every write lands. Tests: send-failure-after-copy deletes the child; mid-batch write failure persists zero resource events and no orphan rows. Co-authored-by: Isaac Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(omnigent-ai#900): bound copy-at-spawn — cap files/bytes + stream one at a time Address PattaraS's blocking review finding on PR omnigent-ai#1041: copy_session_files prefetched every source blob into memory before writing, so a send with many or large file_ids was an unbounded memory spike on a shared server. - Cap file count and summed StoredFile.bytes during metadata validation, BEFORE any blob is read, rejecting an over-limit request with 400 so a rejected request never buffers a blob. - Limits are parameterized config knobs (copy_max_files / copy_max_total_bytes in server_config, defaulting to MAX_COPY_FILES=20 / MAX_COPY_TOTAL_BYTES=256 MiB in content_resolver), overridable per deployment via the YAML config. - Copy one file at a time (get -> create -> put) so peak memory is a single blob, not the whole batch; the existing rollback still gives all-or-nothing. - Tighten the CopyFilesRequest/endpoint docstring to state the source must be a strict ancestor (self rejected). Tests: over-count and over-total-bytes rejections assert 400 with ZERO blob reads (artifact_store.get never called) and nothing copied; at-limit boundary succeeds. Existing lineage/rollback/self-rejected coverage stays green. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(omnigent-ai#900): enrich copy response + CopyResult dataclass (PR omnigent-ai#1041 nits) Two non-blocking nits from PattaraS's review of PR omnigent-ai#1041: nit #1 — the copy response returned only an id mapping, so the runner dispatch path did an extra metadata GET per file and guessed content-type from the filename, even though the true content_type is preserved at copy time. CopyFilesResponse.mapping now carries {new_id, filename, content_type} per file (new CopiedFile model); _build_subagent_message_content reads the type straight from the response — dropping N round-trips — and only falls back to a filename guess when the source row had no recorded type. nit #3 — _build_subagent_message_content returned a clunky tuple[list, None] | tuple[None, str] (value, error) union. Replace it with a small frozen CopyResult(content, error) dataclass; the single dispatch call site branches on result.error. Also regenerated openapi.json for the tightened CopyFilesRequest/endpoint docstrings (strict-ancestor wording). Tests: dispatch asserts the content type comes from the copy response with ZERO per-file metadata GETs, plus a no-content_type→filename-fallback case; endpoint tests assert the enriched {new_id, filename, content_type} mapping. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(omnigent-ai#900): probe artifact_store.exists during copy validation Codex review of the cap-and-stream change flagged a regression: moving to metadata-only validation dropped the original "missing source blob surfaces before any child row is created" guarantee. A blob that failed mid-stream (dangling row: metadata present, blob gone) would only surface after earlier files were already written, leaning on best-effort rollback. artifact_store.exists() is a cheap metadata probe (S3 HEAD / local stat / DB row) — NOT a blob read — so calling it in the validation pass restores the fail-before-any-write guarantee without reintroducing the batch prefetch or spiking memory. Test: a source whose blob was deleted (row intact) → 404 with nothing copied. Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> * fix(files): address review feedback --------- Signed-off-by: praneeth_paikray-data <praneeth.paikray@databricks.com> Co-authored-by: praneeth_paikray-data <praneeth.paikray@databricks.com>
…ent-ai#2998) * perf(store): batch FTS inserts in append and fork_conversation Each call to insert_fts issued a separate raw SQL INSERT into the conversation_items_fts table, causing N+1 queries when appending or forking conversations with many items. Add insert_fts_bulk(session, rows) in omnigent/db/utils.py that issues a single multi-row INSERT for any number of rows. Replace the per-item insert_fts calls in append and fork_conversation with a single insert_fts_bulk call after the loop. Keep insert_fts intact for single-item callers. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> * fix(db): chunk insert_fts_bulk to avoid SQLite variable limit Split rows into chunks of 300 (3 params × 300 = 900 binds) so a single INSERT never exceeds SQLite's SQLITE_MAX_VARIABLE_NUMBER (999 on pre-3.32 builds). Without chunking, fork_conversation on a large conversation raises OperationalError: too many SQL variables. Also add the list[tuple[str, str, str]] annotation to fts_rows in fork_conversation to match the append call site. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com> --------- Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
…omnigent-ai#2994) Replace M individual session.get() PK lookups + M individual UPDATEs with a single IN-clause query to fetch existing to_user grants, then one bulk DELETE for duplicates and one bulk UPDATE for reassigns. For M grants this reduces the query count from 1 + M + up to M = 1+2M down to 3 queries regardless of M. Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
…mnigent-ai#2974) * fix(web): don't queue messages while only background work is running A session with a running background job (background shell / still-running sub-agent) settles into the `waiting` status: the turn already ended and the server's turn gate is free to accept a new turn, but the frontend treated `waiting` as busy and queued every new message client-side until full idle. Two independent gates forced this: - `shouldQueueSend` / `maybeFlushQueuedHead` treated `sessionStatus === "waiting"` as busy, so sends queued and the queue wouldn't drain. - The `session_status` handler grouped a `waiting` edge carrying a `response_id` (which the claude/cursor-native Stop hook always posts) with `running`, forcing local `status = "streaming"`, which never cleared while background work ran. The composer's "(queued)" placeholder and the send gate both key off local `status`, so this alone kept messages queued on native sessions. Treat `waiting` as a turn-end edge everywhere it gates sends: drop it from the busy checks and finalize the local send lifecycle like `idle`, while keeping `sessionStatus = "waiting"` and `backgroundTaskCount` so the "Working…" spinner and sidebar dot still reflect the background activity. A new message now starts a fresh turn immediately, matching what the server already accepts. This only affects sessions with background work running — a turn that ends with no background work still settles on `idle` and behaves exactly as before. Co-authored-by: Isaac Signed-off-by: Serena Ruan <serena.rxy@gmail.com> * fix(web): treat waiting as turn-end on reconnect; add e2e coverage Address the Polly review notes on the message-queueing fix and add the e2e_ui coverage the required gate asks for. - `reconnectStatusPatch`: a `waiting` snapshot is a turn-end edge, so it now finalizes the local send lifecycle like `idle` instead of reopening a streaming response. The server keeps `active_response_id` populated across `waiting` (it only pops on idle/failed), so grouping `waiting` with `running` re-opened "streaming" on a reload/reconnect and re-queued sends — the exact behavior the fix removes. Now covered for the reloaded-tab path, not just live SSE. - The live-SSE mismatched-id `waiting` branch now finalizes a still-streaming bubble to `completed`, matching the matching-id path, so a stale bubble doesn't linger spinning with no edge left to close it. - Add tests/e2e_ui/chat/test_send_while_background_task.py: publishes the native Stop-hook `waiting`+response_id edge live, then asserts the composer sends directly (idle placeholder, user bubble renders, no queued strip) instead of queueing behind the background task. Co-authored-by: Isaac Signed-off-by: Serena Ruan <serena.rxy@gmail.com> --------- Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
6fc409d to
e7914e1
Compare
…e UI (backend, flag-gated) (omnigent-ai#2912) * feat(host): add install-harness tunnel frame pair + registry plumbing Adds the HostInstallHarnessFrame / HostInstallHarnessResultFrame pair to the host tunnel protocol, mirroring the existing HostCreateDirFrame request/result pattern, plus the pending_installs future map on HostConnection. This is the vocabulary the server and a connected host use to negotiate a UI-driven harness install (later PRs add the host handler, the route, and the frontend button). Additive only: no frame is sent or received yet, so behavior is unchanged. The result frame carries a freshly-recomputed readiness map (configured_harnesses, reusing _optional_str_availability_map) so the UI can flip the harness badge without waiting for a reconnect. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * refactor(onboarding): surface install failure reason from install_harness_cli Extracts install_harness_cli_with_reason(key) -> tuple[bool, str | None] alongside the existing install_harness_cli(key) -> bool, which becomes a thin wrapper that discards the reason. Single implementation, no caller churn: the four setup-wizard call sites keep their boolean contract unchanged. The reason is derived from the existing failure branches (manual-only spec, missing installer, timeout, OS error, non-zero exit, post-install binary-not-found) without capturing installer output — so omni setup's live npm output UX is preserved. A later PR's UI-driven install returns this reason to the user instead of a bare failure. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * feat(host): install harness on request + resolve the install result Adds the host daemon side of UI-driven install: - _handle_install_harness in host/connect.py runs install_harness_cli_with_reason off the event loop, recomputes configured_harness_map(), and returns a HostInstallHarnessResultFrame carrying either the fresh readiness map or a failure reason. - host_tunnel.py's receive loop resolves the pending_installs future. - A shared allowlist/resolver (ui_installable_harnesses / ui_install_key) in onboarding/harness_install.py is the single source of truth for which harnesses are UI-installable (claude, codex, pi, opencode, qwen) and their install-spec keys. Defence in depth: the handler re-checks ui_install_key, so a stray or spoofed frame can never drive the installer for a non-allowlisted harness (e.g. hermes, whose installer is a curl | bash). Inert until PR4 wires a sender: nothing emits HostInstallHarnessFrame yet. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * feat(server): add UI harness-install route behind a default-off flag Adds POST /v1/hosts/{host_id}/harnesses/{harness}/install: the server endpoint the web UI's Install action calls. It validates in order — feature flag (404 when off) -> allowlist (400) -> auth/require_user -> owner (403) -> liveness (409) — then forwards a HostInstallHarnessFrame over the tunnel via _proxy_install_harness and returns the host's refreshed configured_harnesses map. - Reuses the _proxy_create_dir request/future/wait_for template; the install timeout (330s) sits above install_harness_cli's 300s subprocess ceiling so the result is received before the server gives up. - Concurrent installs of the same (host, harness) coalesce onto one in-flight task (conn.inflight_installs) so a double-click can't fire two non-race-safe global npm installs. - Gated by OMNIGENT_HARNESS_INSTALL_ENABLED, surfaced to the SPA via GET /v1/info (harness_install_enabled), mirroring smart_routing_enabled. Allowlist ordering (400 before 403) avoids leaking host ownership through error codes. Ships dark: with the flag off the route is 404, so merging this changes nothing in production. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * fix(host): make UI install idempotent + widen the server wait End-to-end testing against a real host surfaced two issues the stubbed unit tests masked: - The host ran `npm install -g` even when the harness CLI was already on PATH; npm re-resolves over the network and took >60s for an already-present binary, so a repeat Install click hung. _handle_install_harness now short-circuits on harness_cli_installed(key) and just returns fresh readiness (reusing the existing check) — sub-second on the happy path. - The server's per-call wait (330s) sat only 30s above install_harness_cli's own 300s subprocess cap, so a genuine cold npm install could finish right as the server gave up — a "504 but actually installed" outcome. Widened to 420s (300s + 2min headroom for readiness recompute + tunnel latency). Verified end-to-end: happy path 200 in 0.8s (already-installed fast-path), a real cold opencode install completes route->tunnel->daemon->npm->readiness, hermes rejected 400, codex reports needs-auth post-install. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * chore(openapi): regenerate spec for the harness-install route CI's openapi-drift guard flagged openapi.json as out of sync after the new POST /v1/hosts/{host_id}/harnesses/{harness}/install route. Regenerated via scripts/dump_openapi.py so the committed spec matches the app. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * refactor(server): share the harness-install flag env-var name Extract OMNIGENT_HARNESS_INSTALL_ENABLED into a single HARNESS_INSTALL_ENABLED_ENV constant in hosts.py, read by both the install route and the /v1/info flag in app.py, so the flag the UI sees and the flag the route enforces can never drift on a typo. Also switch the install-task scheduling from asyncio.ensure_future to the more idiomatic asyncio.create_task. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * feat(server): describe per-harness setup steps for the UI setup flow Extends the harness-install backend so the web UI can render a "set up this agent" checklist that mirrors omnigent setup, instead of a single Install button. - /v1/harnesses now carries an ordered setup_steps list per harness (install, then auth), derived from the existing HarnessInstallSpec so it can't drift from the real install/login commands. Claude/Codex/Pi/OpenCode/Qwen get a first-class two-step flow; other harnesses get a generic "run omnigent setup" step. - The host readiness map now reports a two-step signal (binary-missing / needs-auth) for Claude and OpenCode too, matching Codex, so the UI can show install-done vs sign-in-done. Pi/Qwen stay binary-only (their credential isn't locally determinable). - The launch gate (harness_is_configured) is unchanged and stays binary-only, so a not-signed-in harness is never blocked from launching. - /v1/info advertises installable_harnesses (bare + native spellings) so the UI offers setup only where the install route will accept it. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * feat(server): key harness setup steps by every spelling for the UI The setup dialog looks up steps by the harness a session declares — often a native wrapper (codex-native) or an installable id that isn't a picker row (opencode/qwen), none of which appear in the harness catalog. Add harness_setup_steps_by_spelling() and return it from GET /v1/harnesses as a top-level setup_steps map so the dialog can resolve steps for whatever id it holds, without adding non-pickable rows to the catalog. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * fix(server): use host.user_id in the install route's owner check The install route still compared host.owner, but the Host model's owner field was renamed to user_id (identity-columns unification on main). An authenticated install therefore 500'd with AttributeError. Switch to host.user_id (matching every other host route) and add an owner-mismatch test that exercises the ownership branch with a real user_id — the existing tests run unauthenticated, so the comparison was never hit. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * docs(server): correct the setup-step "can't drift" comment The auth-step commands (codex login, etc.) are display-only literals, not derived from HarnessInstallSpec.login_args — only the install step's label is derived. Reword the comment/docstring so they don't overstate the guarantee. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> * Address review: family-keyed install coalescing + clearer naming - Coalesce concurrent UI installs on the resolved install *family* key (ui_install_key) rather than the raw spelling, so codex + codex-native (both the openai npm package) share one in-flight install. Cleanup is tied to task completion via add_done_callback and every caller awaits under asyncio.shield, so a cancelled request can't clear the map out from under a follow-up and start a second concurrent `npm install -g`. - Add an integration test that fires two overlapping same-family installs and asserts exactly one frame reaches the host. - Rename install_harness_cli_with_reason -> try_install_harness_cli and return a HarnessInstallResult NamedTuple instead of a bare tuple. - Trim the over-long install-handler docstring and UI-installable map comment to the essentials. Co-authored-by: Isaac Signed-off-by: xq-yin <xiaoqian.yin@databricks.com> --------- Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* feat: import Qwen Kiro Pi and Kimi chats Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com> * 🐛 fix(import): Harden JSONL adapter contracts - Expose stable Kiro and Kimi parser APIs for import reuse - Hash overlong source IDs and bound Qwen locators safely Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com> --------- Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com> Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
On Windows, `omnigent setup` could crash as soon as it reached the interactive harness picker because the TTY menu path imported the POSIX-only termios/tty modules. The user-visible failure was `ModuleNotFoundError: No module named 'termios'`, after the setup banner and preflight warning had already printed. Route Windows setup menus through the existing numbered fallback instead of the raw termios path, including the legacy wizard helpers and their back-navigation behavior. Also remove the remaining POSIX os.getuid() assumptions from native bridge temp-root setup so Windows installs do not fail while importing those bridge modules. Tested with the focused Windows startup regressions: python -m pytest tests/onboarding/test_interactive.py tests/onboarding/test_wizard.py tests/test_claude_native_bridge.py::test_ensure_secure_dir_succeeds_without_getuid tests/test_qwen_native_bridge.py -q -k "not rejects_symlinked_ancestor" Signed-off-by: scwf <wangfei_hello@126.com>
e7914e1 to
ba9c7dd
Compare


Summary
stable_user_id()helper for qwen/kiro/hermes/kimi native bridge temp roots so imports do not require POSIXos.getuid().termiosonboarding menus to numbered prompts on Windows while preserving wizard back navigation.Testing
python -m pytest tests/onboarding/test_interactive.py tests/onboarding/test_wizard.py tests/test_claude_native_bridge.py::test_ensure_secure_dir_succeeds_without_getuid tests/test_qwen_native_bridge.py -q -k "not rejects_symlinked_ancestor"Also verified locally on Windows with the installed
omnigenttool:omnigent server start; omnigent server status; omnigent server stop@('q') | omnigent setup