Opencode-native has the same process-leak shape codex-native did (fixed in
#3925): each session runs a runner-owned `opencode serve` subprocess tracked
in _AUTO_OPENCODE_SERVERS plus the opencode TUI pane. Only DELETE /v1/sessions
cancelled the forwarder (whose finally closes the server); the other ways the
TUI pane goes away left the server orphaned for the runner's lifetime:
- the idle pane reaper closed the tmux pane but never touched
_AUTO_OPENCODE_SERVERS,
- an unexpected TUI exit (crash / OOM / host recycle) evicted the pane
without cancelling the forwarder, and
- a graceful host/runner stop tore the runner down without a per-session
DELETE, so _stop_pm never closed the servers.
Mirror the codex fix: add teardown_opencode_native_server (cancel the
forwarder, close any leftover registered server; no-op when none is
registered) and teardown_all_opencode_native_servers (shutdown sweep). Wire
them into the idle-reaper reap, the terminal-exit publisher, and _stop_pm
alongside the codex calls.
No boot-time reconcile: opencode has no crash-safe process registry and
`opencode serve` is a plain Popen (not start_new_session=True), so it shares
the runner's process group and dies with a hard runner death — the
graceful-stop + reaper + exit paths cover the observed leak.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
omnigent-telemetry#15 introduces a CloudFront default config
(omnigent_version: "default") served for any version that lacks an
explicit config file. Without this change, the version check on line 190
always rejects the default payload and silently disables telemetry.
Accept "default" as an equivalent of the current VERSION so the default
config is honoured.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The historical-replay redaction (_redact_inline_base64) only matched
whole-string "data:*;base64,..." URIs — the resolver form under
image_url / file_data. But Claude Code's Read tool returns an image file
as an Anthropic content block {"type":"image","source":{"type":"base64",
"data":"..."}} — raw base64 with no data: prefix — carried in a
function_call_output. That shape slipped past redaction, so if it reached
the "Conversation so far:" text prefix json.dumps flattened the full
base64 into prompt text (the same class of overrun that wedges resume on
the native path).
Extend _redact_inline_base64 to also rewrite image/document base64
"source" blocks to a compact "[image/attachment: <media>, <N> base64
chars]" placeholder. Verified: image and document source blocks now
redact (base64 absent), data-URI and plain-text paths unchanged.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The per-turn idle watchdog fails a turn that emits no non-heartbeat
events for the window. Context compaction's summarizing LLM call runs
as a single long await that emits nothing until it returns, so on a
near-full context it can exceed the 240s default and trip the watchdog.
That wedges the session in a "Prompt is too long" -> compaction ->
240s-timeout loop, since every retry re-triggers the same slow compaction.
Raise the default from 240s to 600s so a healthy long compaction has
room to finish. The HARNESS_TURN_TIMEOUT_S env knob and the absolute
ceiling are unchanged.
Co-authored-by: Isaac
* fix(web): persist open shell tabs per session
Shell tabs lived only in transient component state and the
conversation-switch effect cleared them on every navigation, so opening
a shell, switching sessions, and returning lost the tab. The PTYs
themselves live on the server and are re-fetched by useTerminals — only
the tab strip was being discarded.
Persist openTerminals/selectedTerminalKey per session in
sessionWorkspaceState (mirroring the open file tabs), seed and restore
them on mount/switch, and gate the dead-tab prune effect on the
terminals list's loading state so a restored tab isn't wiped by the
transient empty list before the session's terminals load.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e): cover shell-tab persistence; skip prune on errored terminal fetch
Add an e2e_ui test that opens a real shell in one session, switches to
another via the sidebar (client-side nav), and returns — asserting the
shell tab and its live PTY are restored. This exercises the
conversation-switch effect that regressed, which a full page reload
wouldn't.
Also address review feedback: the dead-tab prune effect ran whenever the
terminals query wasn't loading, but an errored fetch also yields an empty
list — a non-authoritative one. Pruning against it would wipe restored
tabs whose PTYs we simply couldn't reach. Gate the effect on
terminalsError as well, with a component test for the errored-read case.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Claude Code >= v2.1.197 writes `status: "shell"` to its per-session status
file when a turn ends but a background shell is still alive. The status-file
poller's map didn't know that literal, so `read_session_status` returned
`None`, the poller fired no edge and stayed stuck on its last `running` (while
also suppressing the PTY watcher's `idle`). The session never reported idle
while a background shell ran, so `sessionStatus` stayed `running`,
`shouldQueueSend` returned true, and every new message queued client-side —
regressing the "don't queue while only background work runs" behavior.
Map `shell` to `idle`: the agent loop is idle, and the Stop hook separately
relabels its own `idle` to `waiting` with the shell tally, which is what keeps
the "N background tasks still running" spinner lit.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
This reverts commit 617293d3d9.
Painting a cached transcript before revalidation meant the contents
moved under the reader: the window appeared instantly, then shifted as
newer commits were gap-bridged onto it. A hydrate spinner that resolves
into a settled transcript reads better than a fast paint that jumps, so
go back to the cold-load spinner on every conversation switch.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* ci: mirror linked issue priority onto closing PRs
Add a workflow that copies an issue's priority label (P0-P3) onto the
PR that closes it. Only closing links (closes/fixes/resolves #n) count;
a plain "related to #n" mention is ignored. When a PR closes several
issues the highest priority wins, and stale priority labels are dropped.
Runs on PR events and re-syncs when an issue's priority label changes;
the issue-label trigger is gated to priority labels only so other label
edits don't spin up the job.
Co-authored-by: Isaac
* ci: address review feedback on priority sync
- Tolerate null GraphQL nodes (unknown PR number, data: null) instead of
crashing on AttributeError; cover the parsing with tests.
- Add a 30s urlopen timeout so a stalled connection fails fast.
- Validate PR_NUMBER is an integer with a clear message.
- Surface a warning when the issue->PR GraphQL lookup fails rather than
silently succeeding.
- Pass the resolved PR list through an env var instead of interpolating
it into the run block.
Co-authored-by: Isaac
Rename the `enhancement` label to `Feature` and `documentation` to `Docs`
across the issue-triage system. The triage agent's `type` value is applied
verbatim as an issue label, so update the validator allow-list, the agent
schema and classification rule, the feature-request template's auto-label,
and the design proposal doc to keep them coherent.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* perf(host): cache auth headers and parallelize status payloads
Two follow-on speedups for omni host status:
1. Cache _remote_headers() per base_url within a process.
Databricks SDK credential resolution (~3s) ran on every
_host_http_json call. Since tokens are valid for the lifetime
of a CLI invocation, resolving once and reusing is safe.
A threading.Lock serialises concurrent first-time resolution
for the same URL.
2. Build daemon status payloads in parallel with ThreadPoolExecutor.
With the dead-process skip from the previous commit, only live
daemons make HTTP calls. Parallelising them lets independent
servers be queried concurrently instead of sequentially.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* chore: restore uv.lock to main
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: move header cache resolution inside try/except in _host_http_json
_remote_headers() does file I/O and Databricks SDK calls that can raise
OSError. The cache-populating call was outside the try block, so such a
failure propagated unhandled. Under ThreadPoolExecutor (added in this
PR) that aborted the entire omni host status listing.
Move the resolution inside the existing try/except so auth/file errors
remain recoverable and produce a status_code=0 result per daemon,
matching the pre-change behaviour.
Also adds test_host_http_json_handles_remote_headers_oserror to pin
this contract.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* chore: fix import order (ruff)
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* ci(triage): re-triage issues when needs-info is cleared
Add a hybrid needs-info lifecycle. When the issue author comments on an
issue that still carries needs-info, needs-info-response.yml removes the
label using the omnigent-ci App token (the default GITHUB_TOKEN would not
re-trigger downstream workflows). That removal fires issue-triage.yml's
new `unlabeled` trigger, which reads the reporter's follow-up comments,
reclassifies, and assigns an owner — re-adding needs-info only if the
issue is still too vague. Issues the reporter never clarifies are closed
by the existing stale.yml.
issue-triage.yml changes:
- trigger on issues [opened, unlabeled]; the unlabeled path fires only
for needs-info on an open issue, and allows a bot actor (the App)
- feed the author's follow-up comments into the triage prompt
- remove needs-info on re-triage when the LLM no longer flags it
- suppress the duplicate-of comment on the re-triage path
- add a per-issue concurrency group
Co-authored-by: Isaac
* ci(triage): address review — idempotent label removal, dormant-App notice
- needs-info-response.yml: re-check live labels before `gh --remove-label`
so a stale event payload / race can't fail the step (gh errors on a
missing label); emit a ::notice:: when the omnigent-ci App is
unconfigured so a dormant feature is distinguishable from a broken one.
- issue-triage.yml: also suppress the `duplicate` label on the re-triage
path (not just the comment), keeping the label and its explanation
consistent; hoist `import os` to the top of the block.
Co-authored-by: Isaac
* feat(webui): capture raw SSE events and show in execution logs panel
- sseEventLog.ts: module-level ring buffer (max 500 events/session)
with subscribe/snapshot API for useSyncExternalStore
- useSseEventLog.ts: React hook that subscribes to the ring buffer
- chatStore.ts: tap tapSessionEvents to push each StreamEvent into the
ring buffer; clear on fresh stream bind (not reconnect)
- ExecutionLogsPanel.tsx: add Items/SSE toggle — SSE tab shows
timestamped raw events with expand-to-pretty-print, auto-scrolls
to bottom as events arrive
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* perf(webui): skip SSE ring buffer when debug mode is off
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* perf(webui): cache isDebugMode as module-level boolean
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(webui): return new array ref on push so useSyncExternalStore re-renders
Object.is on the same mutated array always returns true, causing React
to skip re-renders. Produce a fresh array on every push/trim so the
snapshot reference changes and the SSE list updates in real time.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(webui): support localStorage debug flag in addition to ?debug=1
Both useDebugMode and the SSE ring buffer guard now check
localStorage.getItem("debug") === "1" as a fallback, so debug mode
can be toggled once in the console without keeping ?debug=1 in every URL:
localStorage.setItem("debug", "1") // enable
localStorage.removeItem("debug") // disable
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(webui): stable snapshot ref and correct debug flag detection
- snapshotSseLog: return shared EMPTY constant instead of allocating a
new [] on every call; prevents useSyncExternalStore render-loop from
the unstable reference on sessions with no log yet
- isDebugMode: re-read window.location.search + localStorage on every
call instead of caching against popstate; React Router uses pushState/
replaceState which never fires popstate, so the cached value stayed
stale when navigating to ?debug=1 in-app
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): split the harness picker by support level
The landing composer's harness picker split its primary list and "More"
group by host readiness, so any configured harness led: Claude Code,
Codex, Cursor, and Pi all competed for the few primary slots, while "More"
held only harnesses that happened to need setup. Support level — what
actually distinguishes these integrations — wasn't represented at all.
Add a `fullySupported` flag to `NativeCodingAgentSpec` and set it on
Claude Code and Codex, the integrations we maintain and test end to end.
Only those lead; every other harness folds into "More" whether or not it
is configured on the host. The flag is opt-in, so the supported set is two
lines in one file rather than a marker on each of the nine others, and a
test asserts the set is exactly claude + codex so it can't drift silently.
Two behaviors are preserved: selecting a harness pins it inline via the
existing `effectiveAgentId` rule, so the active pick is never buried; and
the hide-unconfigured preference still outranks support level, dropping
harnesses that can't launch here (and the "More" trigger with them when
that empties the group).
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(web): promote previously-launched harnesses in the picker
Splitting the picker by support level left Pi and Cursor users a hover
away from their harness on every new session, even though the split is
right for a first-time user. Nothing recorded which harnesses someone
actually launches.
Add a localStorage-backed `useRecentHarnesses` (modeled on
`useRecentWorkspaces`, but not host-scoped — a preference for Pi follows
the person across machines) and record the canonical harness id on a
successful create. The picker then promotes any recorded harness into the
primary list alongside the fully supported ones, so a regular Pi user
gets one click instead of one hover, while a fresh install still leads
with Claude Code and Codex only.
Recording happens only after the create succeeds, so a harness the user
merely browsed past never earns a slot, and the hide-unconfigured
preference still outranks recency: promotion applies within what can
launch on the host, never resurrecting a harness that can't run there.
Stored ids fold through the reversed-alias map, so `native-pi` matches
the canonical `pi-native` spec.
Also fixes the two CI failures from the support-level split: the flow
test's `selectAgent` helper now drills into "More" only when the row
isn't already inline, and the harness-install e2e no longer drills for
Codex (fully supported, so it leads inline even while needing setup).
Adds tests/e2e_ui coverage for both behaviors, stubbing every harness as
configured so the split is provably driven by support level rather than
host readiness.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(webui): wire SessionRail into AppShell behind ?debug=1
SessionRail and ExecutionLogsPanel were implemented but never rendered.
Add SessionRail as a desktop-only column between the chat and workspace
panel, gated on debugMode so it only appears with ?debug=1. The column
hides automatically when a push panel (terminals or execution logs) is
open.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(webui): remove TerminalsCard from SessionRail debug rail
Terminals are already shown in WorkspacePanel. The debug rail should
only show the Execution logs card. Also removes the onExpandTerminals
prop and all terminal-related dead code from SessionRail.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(webui): fix execution logs card title overflow in debug rail
Widen the debug column from w-48 to w-56 and add truncate/min-w-0 to
the CardTitle so the text doesn't overflow into the action buttons.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(webui): add top padding to debug rail column
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): keep chat content clear of the TurnRail as the area narrows
PR #4085 replaced the transcript's md:pl-12 left inset with a symmetric
px-4 gutter, dropping the clearance that kept the centered chat column off
the left-edge TurnRail (the tick minimap). On a narrow conversation area
the prose crowded the ticks.
Restore the clearance as a continuous, width-driven clamp keyed on the
conversation area (@container/chat) rather than the viewport: the column
slides left with the area until its edge nears the rail, then the left
inset ramps up to hold a minimum gap and caps at 3rem so it stops moving
instead of snapping. Because it reads the area width, opening the sidebar
feeds it too.
Add a multi-turn visual-snapshot test that mounts the rail (it only renders
for >= 2 turns, so the one-turn baseline never covered it), rendered at a
narrower viewport so the inset is actually engaged in the capture.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): regenerate visual baselines
* fix(web): shrink rail gap to 24px and stop the pill leaking into snapshots
Reduce the restored TurnRail clearance cap from 3rem to 1.5rem (24px) so the
column sits closer to the ticks while still clearing them.
Park the pointer out of the transcript's top hover band before capture in both
chat snapshot tests. Playwright's virtual mouse starts at (0,0), inside the band
that reveals the "Jump to top" pill (and, on the rail test, over a tick), so a
load-timing race could flash that transient chrome into the resting-state
baseline. Moving the pointer low pins it hidden.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): regenerate visual baselines
* test(e2e-ui): hide the Jump-to-top pill from chat snapshots deterministically
The pill is transient chrome: the initial layout settle (LatestTurnSpacer +
StickToBottom pinning to the bottom) fires a scroll that reveals it for ~2s, so
whether it lands in a capture is a race — which is why a regenerated baseline
picked it up. Force it hidden via an injected style, the same way the shared
settle kills the blinking caret, so the resting-state baseline is deterministic
regardless of when the scroll settles.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): regenerate visual baselines
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
- PiExecutor._resolve_model: strip trailing [1m]-style bracket suffixes before
passing model IDs to the Databricks AI Gateway. The direct Anthropic API
accepts e.g. system.ai.claude-opus-5[1m] but the gateway endpoint does not
(returns 404).
- CodexExecutor.run_turn: when model_provider_override is set (cli-config path)
pass model=None to thread/create so the codex binary uses its own configured
model rather than forwarding an unresolvable alias (e.g. gpt-5.6) to the UC
API.
- credential_label: cli-config providers now label from the entry name
(provider_display_name) rather than the display_name field, for consistency
with other provider kinds.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* Root cause fix — omnigent/policies/builtins/_shell.py
sudo/env/command/time/exec moved out of CMD_WRAPPERS (skip-one-word) into _FLAG_WRAPPERS with their value-consuming flags. CMD_WRAPPERS is now just {"nohup"}, which genuinely takes no options. -- needs no entry — it's consumed as a valueless flag.
While verifying, I found the same hole one level down, which also affects the original GHSA-fixed wrappers: _skip_flag_wrapper_args matched value flags by whole-token equality, so bundled short options bypassed too — sudo -nu root git push, env -iu FOO git push, and (pre-existing) nice -qn 10 git push. It now scans the bundle's characters and consumes a separate value only when the value-taking option is the bundle's last character, so -n 10/-o L still consume while -n10/-oL stay attached. This mirrors orchestration.py:236-245, which already got this right for blast_radius.
Fail-safe backstop — new is_unresolved_invocation(), wired into both consumers
The wrapper tables are an enumeration, so I didn't want the next unmodelled wrapper to be another silent ALLOW. A head still starting with - now routes through each policy's existing "can't parse this" path rather than abstaining — ASK in github.py, the configured action in working_dir.py. Reachable today via nohup -- git push …. Detection is shared; the response stays per-policy, per the module's stated contract.
* 1. env -S / --split-string (the blocker). Reviewer was right: modelling -S as a value flag swallowed the command into the flag's value, leaving zero tokens — which is_unresolved_invocation([]) can't see. Fix takes the reviewer's option (b): env -S is a command interpreter like sh -c, so it's unwrapped and re-parsed on the path that already exists for bash -c / eval.
- _skip_flag_wrapper_args gained a capture_flags set and now returns (index, captured) — reusing the existing flag walk (which already handles --flag=v, -S v, -Sv, bundles like -iS v) instead of writing a second scanner.
- real_invocation_tokens stops at env when a split-string is captured; unwrap_shell_command returns it → recursion gates the inner command.
env -S 'git push <evil> main' → DENY. env -S 'npm test' → still abstains.
2. /usr/bin/sudo -u root git push — same fail-open, not flagged in either review. Wrapper lookup matched the bare word only, so a path token became the apparent command and the segment abstained → ALLOW. Wrappers now match on basename (unwrap_shell_command already did).
* fix(policies): add BSD sudo -a/--auth-type and -c/--login-class to value-flag set
These two options were missing from _FLAG_WRAPPERS["sudo"], leaving a
residual silent-ALLOW bypass: sudo -a foo git push ... left "foo" as
the apparent command head, which does not start with "-" so is_unresolved_invocation
could not catch it. Add both flags and tests for each form.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com>
test_scheduled_task_create_edit_modal_and_time_picker flaked ~30% of runs,
always timing out on `_pick_minute`'s `name_input.click()` with
"dialog-overlay intercepts pointer events". While the time-picker Popover is
open, the Radix Dialog owns pointer hit-testing over the modal, so a normal
actionability-gated click at the input's coordinates resolves to the overlay
and blocks the full 30s under load.
Force every dismiss click on the name input (`click(force=True)`) — the same
technique the picker's open click already uses. A forced click still
dispatches a real pointerdown on the input, which Radix registers as the
interaction-outside that closes the popover, without waiting on overlay
actionability. Covers all three dismiss sites: the retry path and final
dismiss in `_pick_minute`, plus the two post-typed-time blurs in the test body
(focusing the time input reopens the picker via onFocus).
Verified: reproduced the flake (multiple failures across batches of 5-8 runs),
then 12/12 green after the fix; the full file's 9 tests pass.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* refactor(web): decouple typography from interface geometry
Make the desktop font preference drive semantic text tokens while keeping icons, controls, and spacing fixed.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* refactor(web): migrate interface body text to text-ui
Use the settings-controlled semantic body token across shared components and application pages for consistent sizing.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* style(web): refine sidebar typography and empty states
Align sidebar hierarchy with settings-controlled tokens and make empty projects easier to scan.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* style(web): tighten sidebar density and theme polish
Unify sidebar row geometry, refine theme-specific colors and canvas treatments, and standardize compact controls.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* test(e2e-ui): align font size checks with typography tokens
Update browser assertions for the discrete desktop font token and its current bounds.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* test(ui-snapshot): update typography visual baselines
Adopt the CI-rendered snapshots for the intentional settings-driven typography changes.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* fix(web): preserve dark active sidebar hover
Keep selected row colors stable when hovering in dark mode across both sidebars.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* style(web): polish sidebar actions and overlays
Align sidebar controls, dropdowns, and tooltips with shared density, typography, and interaction tokens for a more consistent visual hierarchy.
* style(web): normalize mobile sidebar scale
Keep mobile sidebar typography and icon geometry predictable without changing the desktop presentation.
* style(web): refine responsive sidebar and chat density
Use responsive sidebar spacing and settings-driven chat typography so mobile and desktop retain clear, consistent reading rhythm.
* test(web): align CI expectations with sidebar polish
Update E2E assertions and reviewed visual baselines to reflect the intentional typography, navigation, and density changes.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
---------
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* dev/repro-agent: pin the verdict handoff to a single JSON block
The output contract only said "a single structured verdict block" without
pinning a format, so the agent rendered YAML on some runs and JSON on others,
and the shape drifted (missing facets, prose bullets instead of objects). That
makes the `verdict` field — which the caller parses to label the issue —
unreliable to extract.
Pin it: exactly one fenced ```json block as the final message, JSON only, every
key always present, and `verdict` restricted to the four lowercase literals so
it matches verbatim. `facets` becomes an array of {symptom, verdict, evidence}
objects instead of free-form bullets. README step 4 updated to match.
Co-authored-by: Isaac
* dev/repro-agent: require the JSON block be the last chunk, allow prose above
Some runs split the artifacts into separate markdown sections (a small
"Reproduction Verdict" block, then prose "Journey"/"Facets" headers) with no
single consolidated handoff, so there was no reliable last block to parse.
Clarify the contract: comprehensive prose above the block is fine, but the
```json block must be the LAST chunk of the final message (nothing after its
closing fence) and must carry the complete self-contained handoff. Explicitly
forbid splitting the artifacts across separate sections/headers. There is no
output-schema enforcement for the claude-sdk agentic loop (AgentSpec.output_type
is inert), so this is enforced by instruction plus last-```json-fence parsing on
the caller side.
Co-authored-by: Isaac
The "new session in project" pencil navigates to /?project=<name> while
the landing screen stays mounted. The project-prefill state machine only
restarted when the ?project= param changed, so re-clicking the SAME
project's pencil after editing its default settings kept the stale seeds
— the fix only showed up after clicking another project (or Home) and
back, which flipped the param away and back.
Track a signature of the config the machine last settled from and restart
the prefill when that content changes for the same project, mirroring the
project-switch reset. The saved config is already fresh in the react-query
cache; this makes the machine re-read it.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* Add deep-research example (single agent over an MCP search server)
A single-agent example that answers a question with a cited, cross-checked
report: it plans sub-queries, searches the live web and reads full pages
through an MCP search server, and verifies claims across independent sources.
It is the repo's first example that wires an MCP server via tools/mcp/*.yaml
(auto-discovered), so it also documents the MCP extension path. One agent plus
one MCP server, no sub-agents — the simplest example to copy from. Runs
zero-config against a public, keyless endpoint.
Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>
* test: add e2e coverage for the deep-research example agent
The examples-coverage-sync drift guard (test_every_agent_has_a_dedicated_test_file)
requires every example agent to have a dedicated e2e test. The deep-research
example shipped without one, failing E2E Tests (shard 0/4).
Add a structural test via validate_agent_def_structure (infra-free: the agent's
tools come from the hosted Keenable MCP server and it runs on the claude-sdk
harness, so it can't run end-to-end in CI). Because the agent name 'deep-research'
has a hyphen (not a valid Python test-module name), the test lives in
test_deep_research_example.py and the guard is told via a 'deep-research' entry
in _ALT_COVERED, mirroring the existing 'openai-coder' handling.
Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>
* docs: show deep research search provider options
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* feat(qwen,goose): record delegated fs I/O and gate it with result-phase policy
Omnigent's OSEnvironment but left two layers as documented follow-ups: the
delegated I/O was invisible in history and no content policy ran on it.
Wire both onto the existing _handle_fs_read / _handle_fs_write handlers:
- emit a paired ToolCallRequest + ToolCallComplete per op so the I/O shows in
history (the adapter renders them as observed function_call items)
- run PHASE_TOOL_RESULT content policy on the bytes; an explicit deny refuses
the op (a write is gated before it happens), failing open otherwise
Content-only: the harness policy round-trip carries no request_data, so the
payload is {"result": content}. Closes the file-I/O recording / content policy
item in docs/QWEN_FOLLOWUPS.md.
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(qwen,goose): gate delegated fs at the call phase and audit stale ops
Addresses the review on the delegated-fs recording/policy work.
1. Phase semantics. A delegated write was gated by a result-phase policy eval
before the write, which is content-only and fails open, so a policy timeout
would let the write through. Gate writes (and reads) at PHASE_TOOL_CALL with
the tool name, path, and content, failing closed on an eval error or an ASK
verdict (delegated fs has no elicitation path). Reads keep the result-phase
content check that decides whether the read bytes reach the model.
2. Audit records. Stale prior-turn server fs requests were answered at turn
start, running real I/O, and then had their ToolCall events cleared before
they reached history. Drain those events into history instead of dropping
them, so the I/O they performed is recorded.
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(qwen,goose): evaluate result-phase policy after a delegated write
The write handlers gated at PHASE_TOOL_CALL and then wrote, but never ran a
result-phase evaluation, so the value env.write() returned was never policy
checked and the audit record dropped it. Reads already did both phases.
Run PHASE_TOOL_RESULT after the write carrying the actual result. A denial
records BLOCKED and refuses the response; it cannot undo the write, since it
runs after the operation. The success record now carries the real result too,
matching the read path.
_fs_content_policy_denies was read-specific, so it is now
_fs_result_policy_denies and takes any result. Read behavior is unchanged.
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
PR #3105 removed the `server start` subcommand in favor of
`server --background` and updated the Electron shell-out in the same
commit. The desktop app ships on its own electron-updater channel, so a
client built before v0.7.0 is a normal steady state against a v0.7.0
CLI — and it still runs `omni server start`, which now dies with
"No such command 'start'". "Start locally" is broken for those users.
Restore the subcommand as a hidden alias that routes to the same helper
as the flag, so the two spellings cannot drift. The deprecation notice
goes to stderr; the desktop parses the URL off stdout, which is
unchanged.
Signed-off-by: Yi Lyu <isabellalyu1130@gmail.com>
Co-authored-by: Serena Ruan <82044803+serena-ruan@users.noreply.github.com>
Remove the cron schedule triggers from both discord-watch-rotation
workflows so they no longer fire automatically. workflow_dispatch is
kept for manual runs, and the original crons are left commented out so
the schedules can be restored later.
Co-authored-by: Isaac
Switching from a Codex session to a Claude Code session briefly painted
the Codex model (e.g. gpt-5.5) in the Claude session's composer before
correcting itself.
`switchTo` clears the session-scoped model fields but deliberately keeps
`selectedModel`, the cross-session sticky pick, so a CLI-created new chat
inherits the user's last choice. The native picker kind flips to Claude
immediately (the session query and sidebar row are already cached), so
for the whole snapshot round trip the composer resolved the sticky and
read the outgoing session's model.
Only surface the sticky once the session's own catalog vouches for it.
Pre-bind the catalog is empty, so the label waits instead of advertising
a model this session would reject; post-bind it is a no-op, since the
store only ever leaves a catalog-compatible sticky (or the override) in
`selectedModel`.
Co-authored-by: Isaac
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(server): file forked sessions into the source's project
Forking a session filed in a first-class project left the fork unfiled:
fork_conversation built the fork's metadata row without project_id. The
fork route now carries the source's project onto the fork, gated on the
forker owning that project (projects are owner-private, so a fork of a
shared session filed in someone else's project stays unfiled).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): refresh the project folder when a session is forked
A fork inherits the source's project, but the dialog only invalidated the
flat session list — each folder renders its own ["project-sessions", name]
query, which has no poll and converges only on an explicit invalidation.
The push stream can't cover it either: it skips the active session, and the
fork becomes active on navigate. So the clone stayed missing from its folder
until a reload or a re-navigation.
Adds an e2e regression test. It seeds the committed turn the fork action
anchors on straight into the store (new seed_committed_turn helper) instead
of driving a model turn, so it neither waits on nor inherits the flakiness
of the mock-LLM harness.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): stabilize reasoning indicators during active turns
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
A dropped host rendered two different indicators depending on incidental
state. The badge read the host tunnel directly (name + red dot), while
ChatPage passed a separate `hostOffline` prop derived from
`liveness.kind === "host_offline"` that replaced the name with generic
"Host is offline — click to reconnect" copy.
`host_offline` is far narrower than "the host tunnel is down": it also
requires the runner to be down (a live runner short-circuits to `online`),
the startup grace to have lapsed, and the host to be non-resumable. So the
same event — the host dropping — showed a passive, unclickable name when
the runner outlived the host, and a nameless reconnect prompt when it
didn't. The name is what tells the user which machine to go restart.
The badge now owns the decision: one shape (name + status dot) that turns
into a button opening the reconnect instructions whenever its bound host is
offline and reconnectable. A dormant resumable managed host stays passive —
the next message wakes it, so `omnigent host` would be wrong advice.
The reconnect dialog's state now comes from the session's host binding
rather than liveness, so a session whose runner outlived its host gets the
`omnigent host` command instead of the local `omnigent run --resume` one.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
Closes#866
The `omnigent` Homebrew formula has not built since 0.7.0, and the tap reported
green anyway, so 0.7.0, 0.8.0 and 0.8.1 all merged with no bottle — every user
compiled from source, and CEL policies silently did not work.
- **Root cause was the CEL migration.** #2970 swapped `cel-expr-python` for
`cel-python` on the premise that it is pure Python. It is not: `cel-python`
hard-depends on `google-re2`, whose sdist runs `bazel build` whenever
`GITHUB_ACTIONS` is set. The bazel dependency moved rather than disappeared.
- **Pin compiled extensions to upstream wheels.** `generate_formula.py` gains
`WHEEL_REQUIRED` / `PREFER_WHEEL` / `PURE_WHEEL` with abi3 and universal2
handling, so grpcio (by far the most expensive build), protobuf, regex,
uvloop, httptools, argon2-cffi-bindings, markupsafe, pyyaml, zstandard and
google-re2 stop being compiled. Native wheels rank above pure-Python ones, so
protobuf keeps its upb build instead of the slow fallback.
- **jiter, tiktoken and watchfiles keep building from source.** Their maturin
wheels carry no Mach-O install-name padding, so Homebrew relocation fails with
"Failed changing dylib ID" (#866). `pendulum` can go neither way — its wheel
cannot be relocated and its sdist does not link on 3.14 (pyo3 leaves
`_Py_NoneStruct` undefined) — so it takes the pure-Python wheel, which ships
no extension module at all.
- **A dropped dependency is now an error, not a warning.** A missing sdist used
to be skipped silently, yielding a formula whose venv lacked an import;
`--allow-no-sdist` is the explicit waiver. The formula test also asserts
`import re2, celpy`, since omnigent imports celpy behind `try/except
ImportError` and would otherwise disable policies silently.
- **Delete `update-homebrew.yml`.** It raced `homebrew-tap-pr.yml` on the same
`release: published` event and asserted on hand-maintained stanzas the
template no longer emits, so it failed on every run. Its one worthwhile part
moves into `homebrew-tap-pr.yml`: an admin/maintain gate on manual dispatch
(it writes to another repo with an App token), plus
`persist-credentials: false`. Its nightly `schedule` is deliberately NOT
carried over -- that cron only existed because `brew
update-python-resources` resolves through pip's `--uploaded-prior-to=P1D`
window and so could never see a same-day release. The generator runs `uv pip
compile --no-config` straight against PyPI, so the blindness it worked around
no longer exists, and a nightly regeneration would just burn a runner to
print "nothing to do".
Verified by building the generated formula in the tap, not by inspection.
- `omnigent-ai/homebrew-tap#18` contains **verbatim output of this
`generate_formula.py`** and bottled successfully on macos-15 and macos-26
(run 30944428771, `bottles_macos-15` / `bottles_macos-26` ≈ 37 MB each). This
is the check that matters: it proves the generator — not a hand-edit —
produces a buildable formula, so the next release regenerates something that
works.
- `omnigent-ai/homebrew-tap#17` carries the same fix for the shipped 0.8.1
formula and is green on all three runners, with `brew test` running
`import re2, celpy`. Inspected the bottle: `celpy/__init__.py`,
`re2/_re2.cpython-314-darwin.so`, and a relocated
`jiter/jiter.cpython-314-darwin.so`.
- Audited every pinned wheel by replaying Homebrew's own operation,
`install_name_tool -id <Cellar path>` against each extracted `.so`, so the
wheel/source split is evidence-based rather than guessed.
- `python3.12 -m py_compile`, `ruff check`, `ruff format --check`, `brew style`
(no offenses), `ruby -c`, plus stubbed-PyPI unit checks of the new failure
paths (missing sdist is fatal, `--allow-no-sdist` waives it, abi3 accepted,
free-threaded `cp314t` rejected).
- Confirmed generator output matches the green formula: same 100 resources,
identical sdist/wheel split, no non-comment differences.
N/A — release tooling, no user-visible UI.
- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
The generator has no test suite in this repo, and its real contract — "the
emitted formula builds under Homebrew on macOS" — cannot be asserted here. It is
covered instead by building the generated formula on the tap's `brew test-bot`
matrix (homebrew-tap#18, bottles produced on macos-15 and macos-26). The two new
generator failure paths were exercised locally against stubbed PyPI metadata,
and every wheel pin was verified relocatable with `install_name_tool`.
`brew install omnigent` works again, and installs prebuilt wheels instead of
compiling grpcio and friends from source.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
The boxlite SDK's BoxOptions already supports disk_size_gb, but the
omnigent wrapper never threaded it through — every box got the SDK's
own default disk size with no way to override it. Add
sandbox.boxlite.disk_size_gb to the server config, alongside the
existing image/env knobs.
Signed-off-by: Avri Chen-Roth <11185446+the-mentor@users.noreply.github.com>
Co-authored-by: Avri Chen-Roth <11185446+the-mentor@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(web): decouple typography from interface geometry
Make the desktop font preference drive semantic text tokens while keeping icons, controls, and spacing fixed.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* refactor(web): migrate interface body text to text-ui
Use the settings-controlled semantic body token across shared components and application pages for consistent sizing.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* style(web): refine sidebar typography and empty states
Align sidebar hierarchy with settings-controlled tokens and make empty projects easier to scan.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* test(e2e-ui): align font size checks with typography tokens
Update browser assertions for the discrete desktop font token and its current bounds.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
* test(ui-snapshot): update typography visual baselines
Adopt the CI-rendered snapshots for the intentional settings-driven typography changes.
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
---------
Signed-off-by: Ajay Alfred <ajayalfred07@gmail.com>
`HostRegistry.deregister` was a bare `dict.pop`, and the host tunnel's receive
loop refreshed `conn.last_frame_at` without checking whether its connection was
still the registered one. The runner side already guards both (
`TunnelRegistry.deregister` takes a session guard and `mark_frame_seen` rejects a
superseded session); the host side did not, so a host could be left in a state
its own route handler never noticed.
Dropping a host registration from outside the route handler did not close the
socket or cancel its tasks. The ping loop kept writing `host_store.heartbeat`, so
the durable row stayed **online** while every `host_registry.get` reported the
host offline. Anything that resolves liveness from that row then waits for a
reconnect the host was never told to make, because from the host's side nothing
happened. `register` already poisons a replaced connection's outbound queue for
exactly this reason; `deregister` now does the same.
Three changes:
- `deregister` queues the `None` sentinel so the sender loop exits and the
socket tears down, letting the host redial.
- `deregister` takes an optional `conn` generation guard and returns whether it
removed an entry. The tunnel route gates its `set_offline` write on that
return, so a superseded handler reaching cleanup after a reconnect replaced it
can no longer evict the live connection or mark a live host offline.
- `mark_frame_seen` mirrors `TunnelRegistry.mark_frame_seen`: a frame only
refreshes liveness while its connection is current, and the receive loop stops
when it is not.
Six tests added to `tests/server/test_host_registry.py`; five of them fail
against the previous behavior.
Co-authored-by: Isaac
Signed-off-by: Mark Tai <mark.tai@databricks.com>
Co-authored-by: Mark Tai <mark.tai@databricks.com>
* feat(web): fold settled turns behind a 'Worked for Xs' row
Once a turn completes, the chat view collapses its whole process trace
(interstitial narration, tool-run folds, resolved approval cards,
reasoning) behind one muted 'Worked for Xs' expander with a hairline
rule, leaving only the final answer visible - mirroring the Codex
desktop treatment so it's obvious where reading starts instead of a
wall of uniform prose. Expanding the row replays the trace inline.
- Live turns keep their trace expanded; liveness comes from the
bubble's own lifecycle, not session status, so a completed turn
folds even while a later turn streams (and vice versa).
- partitionTurn splits a settled turn into foldable process, exempt
always-visible cards (pending elicitations, persistent
dispatch/routing cards, in-progress spinners), and the trailing
final answer; a turn with no trailing answer (interrupted / failed
/ tool-only) never folds. Resolved approval cards fold with the
trace in document order. Codex's trailing turn_diff bookkeeping
folds as process instead of masquerading as the answer.
- The 'Worked for Xs' duration spans the live stream clock while
streaming, or the items' server created_at stamps on reload;
ConversationItem.to_api_dict() now exposes created_at (additive)
to make the reload path possible.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(stores): expect created_at in the item API-shape round-trip
to_api_dict() now serializes created_at, so the exact-shape assertion
gains the store-assigned stamp.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* docs(web): demo screenshots + cross-clock note for the turn fold
Adds the collapsed/expanded 'Worked for Xs' screenshots referenced by
the PR description, documents that turnWorkedForS's first block picks
the clock branch, and pins the reverse mixed-clock direction
(live-first, epoch-last) as undefined.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): settle the turn lifecycle on bare terminal status edges
The 'Worked for Xs' fold (and the Fork action) only appeared after
navigating away and back: the turn lifecycle finalized live ONLY on a
session.status edge carrying a matching response id, but most idle
publishes carry none (the PTY-activity relay, orchestration teardown).
So a native turn ending on a bare idle cleared 'Working…' while the
bubble stayed 'streaming' forever — settled state was only re-derived
from the snapshot on reload.
- session_status: any terminal edge (idle/failed/waiting) now
finalizes a still-streaming turn, id-matched or not; cancelled is
preserved. The stray running->idle pair the policy-deny
short-circuit publishes mid-turn is healed by
reviveStrayCompletedResponse: live deltas for the turn flip it back
to streaming, so the misread is a brief flicker, not a mid-turn
fold.
- Mid-turn first open: the initial session bind now reopens the
streaming lifecycle from the snapshot's activeResponseId (mirroring
reconnectStatusPatch), so a running session's live turn renders
expanded instead of prematurely folded.
- e2e: test_bare_idle_finalizes_turn_and_folds drives the exact event
sequence (running+id -> items -> bare idle) against a real server
and asserts the fold forms in place, no reload.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): fold turns split by a sub-agent await, and ease the collapse
Two gaps in the 'Worked for Xs' fold, both visible on a turn that
dispatches sub-agents.
Fold never formed. Dispatching sub-agents ENDS the parent turn — it
must yield to await their results — and the inbox wake starts a new
turn under a new response id carrying the answer. That splits one
logical turn across bubbles: the first holds narration + tool calls
and no answer, the second holds the answer and no work. The fold
required both halves in ONE bubble, so neither qualified and the
narration stayed spread out unfolded. buildBubbles now flags a bubble
whose turn continues in a later assistant bubble (scanning past the
runtime [System: ...] wake markers, stopping at a real user turn),
and such a bubble folds its whole trace despite carrying no answer.
The flag participates in bubblesEqual so the memoized bubble actually
re-renders when its continuation lands.
Collapse was abrupt. The settled render swapped a tall expanded trace
for a one-line row in a single frame, which read as a partial page
reload. The fold now MOUNTS OPEN when the turn settles on screen and
closes on the next frame, so the steps visibly fold into the summary
row; settled history still mounts closed (nothing to animate away).
The height animation lives in index.css because it needs Radix's
measured --radix-collapsible-content-height, and is disabled under
prefers-reduced-motion.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): remove the jolt at the start of the turn-fold collapse
The collapse read as two motions. Measuring the bubble height every
frame through a settle showed why: inserting the summary row and the
fold's own padding/border grew the bubble ~43px TALLER in one frame,
and only then did the 200ms collapse run — a jolt up, then a ramp
down.
- The summary row now grows in (grid-template-rows 0fr -> 1fr) over
the same beat instead of appearing at full height, so row expanding
and trace shrinking net one monotonic shrink.
- The animated element carries no padding or border of its own: any
chrome there is height that lands before the collapse starts, which
is exactly the jolt. Expanded spacing comes from the row's hairline
above and the message column's gap below.
- The fold also animates when it appears on an already-mounted bubble,
not only when the turn itself settles — a turn split by a sub-agent
await folds when its continuation lands, and that case was snapping
shut with no animation at all.
Measured on a live server, same turn shape both times: leading jolt
43px -> 10px, and both the plain and sub-agent-split cases now show a
single animated ramp instead of a jump followed by one.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): stop the turn fold oscillating on codex sessions
On codex the fold flipped collapsed/expanded repeatedly as a turn
streamed. Instrumenting a live turn showed why: the server recorded
that turn as ONE response, but the client showed five to seven
bubbles. A streamed narration renders as its own transient 'live:'
preview bubble until its authoritative item replaces it, and reasoning
bursts group separately, so bubbles appear and merge away on every
delta. Each appearance gave an earlier bubble 'a later assistant
bubble' and marked it continued, folding a fragment; the merge
unmarked it and unfolded it again. Two fragments folded mid-turn as
'Worked for 1s' / 'Worked' rows carrying only a reasoning burst.
- markContinuedTurns only runs between turns: while a response is
streaming the transcript is mid-restructure, so nothing is marked.
Marks are sticky, so a bubble that has folded never reopens when the
next turn starts streaming.
- A continued bubble must also have RUN something (a tool call in its
process) to fold. That is the shape the flag exists for — narration
plus tool calls, then a yield to await sub-agents — and it keeps a
narration- or reasoning-only fragment from folding into a lone
'Worked' row with nothing behind it.
Measured on live codex turns, same prompt shape: fragments folding
mid-turn 2 -> 0, and the only remaining fold is the real one at turn
end.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): never fold a bubble made only of streaming artifacts
Residual codex flicker: the fold still appeared and vanished mid-turn,
just less often. This path never involved the continued flag, which is
why the previous guards only reduced the frequency.
Codex splits an in-flight turn into fragment bubbles — a reasoning
burst (ctx.itemId is null until its item is finalized) plus a 'live:'
narration preview. Their synthetic response id never matches
activeResponse, so walkBubbles labels them 'completed', and a fragment
holding reasoning + text satisfied the ordinary process-plus-answer
rule and folded. When the authoritative item replaced the preview the
fragment merged away and its fold went with it.
A genuine turn always carries at least one server-assigned item id, so
a bubble whose items are ALL null-id or 'live:'-prefixed is a fragment
of the turn still arriving and never folds. LIVE_ITEM_PREFIX moves to
lib/blocks.ts so the renderer and the store share one definition.
Verified by assertion: before this change a reasoning + live-preview
bubble rendered a fold; now it renders expanded. Two frame-exact
recordings of the reported prompt (63k frames, with approvals) showed
no fold disappearing, so this was found by construction rather than by
reproducing it live.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): render a native turn as one bubble live, so it folds like it does on reload
Root cause of the codex fold flicker, found by comparing the same
conversation live vs reloaded: 4 assistant bubbles and NO fold live, 1
bubble folded after a reload. A native turn was being split into
fragment bubbles while streaming and merged back into one on reload,
so the two views disagreed. walkBubbles groups by response id, and
three kinds of block carried the wrong one:
- Live text previews were stamped with a synthetic 'live:<id>' as
their response id, so each streamed narration broke the run. They
now adopt the live turn's id (falling back to the synthetic id when
no turn is tracked, so a preview can't join an unrelated bubble).
- A native harness emits no response.created, so the reducer never
learned the turn id and stamped its own blocks (reasoning, streamed
text) with a stale or empty one. A 'running' status edge carrying a
turn id IS the native turn-start signal, so the reducer adopts it --
without sealing an already-open section, since codex opens reasoning
~2s BEFORE that edge lands and closing would split one thought in
two.
- Blocks emitted in that ~2s window still carry no id, so the store
attributes the trailing unattributed run to the turn when the edge
names it.
With one bubble per turn, the fold condition stops oscillating: it was
flipping because the fragment boundaries moved as previews appeared
and merged, so whichever fragment momentarily had the
process-plus-answer shape folded and then unfolded.
Also: a trailing reasoning item no longer blocks the fold. Codex opens
a reasoning section as the turn ends, landing it after the final
message; reasoning is process, never the answer, so it peels into the
trace like the turn_diff wrap-up already did.
Measured on the reported prompt (with approvals), same shape each
time: bubbles 4 -> 1, and fold transitions went from 'never appears
live' to exactly one 0->1 the instant the turn ends, with zero
decreases (no flicker).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): make bubble grouping and the turn fold robust to a mid-turn connect
The codex flicker survived the response-id stamping fixes because their
premise was fragile: they depend on the client CATCHING the one
'running' status edge that names the turn. A tab that connects (or
reconnects) mid-turn never sees it — SSE replays no status edges — so
reasoning blocks carry rid "" and live previews fall back to their
synthetic ids. Attaching a fresh client to the reporting user's live
session reproduced it exactly: the persisted turn was ONE response, but
the page rendered up to ELEVEN bubbles, five of which folded mid-turn,
including one fold flip back open.
Two structural fixes, replacing edge-dependence with invariants:
- walkBubbles no longer splits a bubble on ANONYMOUS response ids
("" or live:*): such blocks only ever come from the live stream of
the turn around them, so they join it, and a group that OPENED on
anonymous blocks adopts the first real id that arrives. One turn is
now one bubble regardless of which edges the client happened to see.
Bubbles also stop keying off transient live: preview ids, so the
authoritative-item swap no longer remounts the bubble.
- The LAST assistant bubble never folds while the session is running,
even when its lifecycle reads settled — a mid-turn connect misreads
the live turn as 'completed', and folding it collapsed and reopened
the trace as its tail alternated between text and tools. The
session's terminal status edge folds it, which is the natural moment
anyway. Earlier bubbles still fold as usual while a later turn runs.
Verified by attaching mid-turn to a live codex run of the reported
prompt (with approvals): before, 8+ bubbles with 5 mid-turn folds and
a fold flip; after, one bubble, expanded throughout, folding exactly
once when the turn ends.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): one bubble — and one 'Worked for' fold — per user turn
The reported flow (a codex step-wise/goal turn) rendered SEVEN
'Worked for Xs' folds under one user message: codex publishes a
distinct response id per STEP on its status edges while the items all
carry the thread id, so each step opened a new bubble, and every
settled fragment folded separately once the turn ended. The server had
persisted the whole thing as ONE response.
- walkBubbles now groups ONE bubble per user turn: a response-id
change between two assistant blocks with no user message between
them is a continuation (step-wise sub-turns, retries, pre-edge
blocks), not a new turn. The group tracks the LATEST real id so
lifecycle follows the live edge. Blocks stamped a distinct id ON
PURPOSE — deny/failure sentinels and REQUEST-phase elicitations —
still open their own bubble, in both directions.
- Fold appearance is debounced (500ms of held eligibility): a
step-wise turn's between-step idle edge, or a stray idle before its
revive, reads settled for a moment and would otherwise fold and
reopen the trace. Losing eligibility hides the fold immediately, and
settled history still mounts folded with no delay.
Tests that pinned per-response grouping modeled adjacent turns with no
user message between them; real streams separate turns with one (the
inbox wake marker in the sub-agent flow), so they now include it. The
reducer-driven reused-callId test keeps its no-cross-pollination
assertions within the merged bubble.
Verified live: a simulated 5-step turn (distinct per-step edge ids,
one thread id) renders one bubble with zero mid-run folds and exactly
one fold at the end, and a real codex approval run folds once, 0.5s
after the turn ends.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): don't fold a live turn's partial work on a mid-turn refresh
Refreshing while a turn was parked on an approval collapsed the
partial trace into a premature 'Worked for' row. Two holes let the
last-bubble fold suppression miss the live turn on reload:
- The parked elicitation forms its own trailing assistant bubble whose
card ChatPage floats to the page bottom, leaving the bubble
item-less (it renders null) — and that phantom was counted as the
'last assistant' bubble, handing the actual trace to the fold.
lastRenderableAssistantIndex now skips item-less bubbles.
- On a step-wise codex turn the snapshot's active_response_id names
the STEP id while the items carry the thread id, so on reload the
trace's lifecycle reads 'completed' even though the turn is parked.
A pending elicitation now suppresses the last bubble's fold
directly: a card awaiting the user proves the turn is in flight
regardless of what the lifecycle or session status read.
Verified live: reloading a session parked on a codex command approval
keeps the trace expanded with the card visible, and a simulated
mid-turn reload with the step/thread id mismatch stays expanded until
the terminal idle edge, then folds once.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): no 'Worked for' flash when a reload lands between turn steps
Approving an elicitation and refreshing flashed the fold: a step-wise
codex turn publishes an idle edge when each step completes, and a
reload landing in the between-step gap reads fully settled — status
idle, no pending card, trace ending in narration text — so the fold
mounted instantly (the settled-history fast path), then the next
step's running edge cancelled it. Reproduced deterministically: fold
at 0.27s, gone at 1.66s.
Nothing in that snapshot can distinguish the gap from a real turn end,
but the trace's AGE can say how ambiguous it is: items carry server
created_at stamps, so the bubble now records its newest item's time.
The last assistant bubble mounted over a JUST-active trace (newest
item < 15s old) holds its fold for 3s instead of showing it instantly
— long enough for the next step's running edge to cancel it, so the
gap reload never folds at all. A reload after a genuine turn end folds
once the hold elapses, and old history still mounts folded with no
delay.
Verified live against the simulated gap: reload-in-gap shows no fold
ever (was flash-then-hide), reload-after-real-end folds at ~3s, and
stale-history mounts fold instantly (unit-tested).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): don't pop a settled turn's fold open while the next turn spins up
Once a real user message follows the last assistant bubble, a running
status belongs to the reply-in-flight for that newer input, so the
settled bubble's 'Worked for' fold must not be suppressed. Closes the
opencode dip where the prior fold opened for seconds until the new
turn's first item mirrored through the TUI.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* feat(web): scroll the expanded 'Worked for' trace into view
Clicking the fold expands the trace above the reading position, and the
browser's scroll anchoring keeps the answer below it stationary — the
work opens off the top of the viewport and the click looks like a no-op.
On a user-initiated expand whose row+trace don't fit the scroller, snap
the fold row to the top (before paint) so the trace reads from its
beginning. Fits-on-screen expands and the programmatic mount-collapse
animation don't scroll.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(e2e): cover the 'Worked for' fold across native wire shapes
Four deterministic events-API tests: step-wise per-step status edges
fold once with no mid-run flicker; items that switch response id
mid-turn still yield one fold per user message; a mid-turn reload keeps
partial work expanded until the terminal edge; and a settled turn's
fold holds through a follow-up send's item-less gap.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): always snap the fold row on user expand
The fits-on-screen fast path never held in practice: on the last turn
the stick-to-bottom scroller treats the 200ms expand animation as
appended content and re-pins the bottom, and elsewhere native scroll
anchoring pins the answer below — either way the growing trace glides
the row off the top and the click looks like a no-op. Snap the row to
the scroller top on every user expand (the upward scroll also unpins
stick-to-bottom) and park overflow-anchor for the animation.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): make the fold's expand snap win against the bottom-lock
Clicking 'Worked for' while the view is pinned at the bottom (the
resting position on the last turn) did nothing: the expand animation
opens at height 0, so the snap clamps against a scroller with no room,
and stick-to-bottom's resize handler then rides the growth to the
bottom — programmatic scrolls never unpin it. User expands now open at
full height in one frame (no height animation), release the bottom-lock
via a null-safe ConversationScrollLockContext (same recipe as
JumpToTopButton), and then snap the row to the top.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): land the fold's snap below the chat top fade
The snap parked the row 8px below the scroller edge — inside
chat-scroll-fade's transparent band (opaque only from 80px), so the
'Worked for' label sat scrolled-to-top yet invisible. The row's
scroll-margin-top now lives next to the fade definition (88px, plus the
iOS inset variant) so the two can't desync.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): restore the session busy signal when a live delta revives a turn
A stray idle edge clears sessionStatus before the revive flips the
turn back to streaming, so shouldQueueSend saw an idle session and let
a mid-turn send bypass the queue (and the Working indicator stayed dark
until the next running edge). The delta that triggers the revive proves
the session is mid-turn — restore sessionStatus: 'running' with it.
Local send status stays untouched: cross-client and TUI-typed turns
have no local send in flight.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* docs(web): drop the turn-fold demo screenshots from the repo
The PR description references them by pinned commit SHA, so the binary
assets don't need to live in the tree.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
## Related issue
N/A
## Summary
- The Databricks AI gateway only serves Claude requests in coding-agent mode when the `x-databricks-use-coding-agent-mode: true` request header is present; omnigent's Claude launches to the gateway did not send it.
- `ClaudeSDKExecutor`'s Databricks gateway env (`_resolve_gateway_env`) and native-claude's ucode launch config now pass `ANTHROPIC_CUSTOM_HEADERS=x-databricks-use-coding-agent-mode: true`, which Claude Code forwards verbatim as request headers (this survives the thinking-display gateway shim, which forwards all request headers).
- Generic-provider gateway envs (non-Databricks `key`/`gateway` providers) deliberately do not receive the header.
## Test Plan
- `uv run pytest tests/test_claude_native.py tests/inner/test_claude_sdk_executor.py -q` — 283 passed.
- Updated the ucode env exact-equality assertion and gateway-env tests to assert the header; added `test_generic_provider_gateway_omits_databricks_header` to pin the Databricks-only scoping.
- `uv run ruff check` and `uv run ruff format --check` on the touched files — clean.
## 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
N/A
## Changelog
Claude sessions routed through the Databricks AI gateway now send the `x-databricks-use-coding-agent-mode` header the gateway requires
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Moves aravind-segu from `owners` to `owners_paused` in the 12 areas they
owned, so PR reviewer assignment and issue triage stop routing to them.
Readers use only `owners`; the 2+ owner check counts paused owners, so no
backfill was needed and no area is left without an active owner.
`policies` is now down to a single active owner (TomeHirata).
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(codex-native): clear the MCP startup band once the model starts working
The web chat showed 'Starting MCP servers (3/4): <name>' underneath an
agent that was visibly already working, sometimes for minutes.
Codex delivers per-server startup edges only to the connection that owns
the thread, so the forwarder synthesizes the round and settles it when
the thread goes idle after a turn, or when a config-derived window
elapses. Both are late: a server that never reaches a terminal state
(e.g. a misconfigured command that never handshakes) keeps the band
pinned for the whole first turn, and the window stretches to the slowest
configured startup_timeout_sec plus grace (135s for a 120s budget).
Settle on the first model-produced turn item as well. Codex defers turn
EXECUTION until the startup round ends, so assistant-side output proves
the round is over while the turn is still running - the same invariant
the idle-edge settle already relies on, observed at the earliest point
it can be. The band now covers only the genuine pre-turn wait.
The turn's userMessage item is excluded, and only parent-thread events
count: a turn is ACCEPTED (thread flips active, user message
materializes) mid-startup, and a collab child's turn says nothing about
the parent's round.
Two adjacent fixes fall out: a mid-turn reload no longer re-shows the
stale band from the session snapshot, and hitting Stop during a first
turn no longer reports 'cancelled' for servers whose startup had in fact
finished. The failed-turn diagnostic that names still-pending servers is
unaffected - a failed turn/start produces no model output, so no settle
precedes it.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(codex-native): settle the MCP round once, and add before/after visuals
Addresses review feedback on the settle-on-model-output change:
- Settle at most once per forwarder connection. The round is seeded
once per connection and never on thread rotation, so once model
output settles it the outcome cannot change; without a guard every
later item in the session re-read the bridge file to reach the same
idempotent no-op. A state flag short-circuits them, and the new test
re-populates the map behind the flag so dropping the guard fails
rather than passing on idempotency alone.
- Add the before/after chat captures the review asked for, taken at the
same point in the turn (agent running 'sleep 40') against servers
built from the same web UI, differing only in this fix.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* chore(codex-native): drop the committed demo screenshots
The before/after captures don't need to live in the repo; the same
evidence is in the PR description as the sampled A/B table and the
runner-log timeline.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(cursor-native): merge Omnigent MCP bridge into existing mcp.json instead of overwriting
write_mcp_config() previously called build_mcp_config() which returned a
dict with only the Omnigent bridge MCP server, then wrote it wholesale to
.cursor/mcp.json. This destroyed any user-configured MCP servers.
Now read the existing mcp.json, merge the Omnigent entry into mcpServers
leaving other keys intact, and write back the merged config.
Fixes#3083
Signed-off-by: lg320531124 <155300404+lg320531124@users.noreply.github.com>
* fix(cursor-native): guard malformed mcp.json and cover the merge path
A hand-edited .cursor/mcp.json can hold any JSON shape. The merge read it
and indexed straight into it, so a list/null root or a non-dict mcpServers
raised AttributeError/TypeError and took down the session launch, where the
old overwrite-always code could not.
Discard non-dict shapes before merging, swap the try/except/pass for
contextlib.suppress (SIM105), and add tests for the merge path (user server
plus a sibling top-level key survive) and the malformed shapes.
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(cursor-native): type the mcp.json merge against JsonObject
main moved this module off typing.Any onto JsonObject (dict[str, object]),
so the merge's `dict[str, Any]` annotation broke ruff F821 and pyrefly
once rebased, and indexing the object-valued mcpServers failed bad-index.
Narrow the loaded JSON with isinstance into a local `servers` dict (the
pattern opencode_native_provider already uses) and bind it back into
`existing`, so the write lands through the alias and stays typed.
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: lg320531124 <155300404+lg320531124@users.noreply.github.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
The example Postgres URLs in these docstrings are placeholders, but gitleaks'
`postgres-connection-string` rule matches the `scheme://name:secret@host` shape
and can't tell a placeholder from a live DSN. That makes them permanent false
positives: they show up in GitGuardian digests, and the Databricks pre-push hook
re-flags them on every new-branch push, since pushing a new branch re-scans
commits already on main. Working around that means reaching for
SKIP_SECRET_SCAN, which is a habit worth not having.
Switching the examples to angle-bracket placeholders sidesteps the rule (`<` and
`>` fall outside its username/password character classes), and reads more
clearly as a placeholder besides.
Docstrings and comments only: with docstrings stripped, the AST of every touched
file is byte-identical to before. Test files are deliberately left alone: their
URLs are live inputs and expected values, and one case exists specifically to
prove percent-encoded credentials survive the prefix rewrite, so rewriting it
would defeat the test. Those remaining findings are best marked as false
positives in the scanner instead.
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
The private Databricks secure-release repo was named in 9 places: three
workflow header comments, the `release.yml` run summary, a design-doc table
row, and four direct links into the private repo's file tree from
`editors/vscode/PUBLISHING.md`. None of it resolves for anyone outside
Databricks.
`release.yml` printed the name into its run summary on every release. Public
run summaries are world-readable, so a repo variable would keep leaking it.
The summary now prints the full command with `<secure-release-repo>` as the
only placeholder, so a release manager still gets something to paste and fill
in, and points at the runbook for the value.
The rest is a straight substitution to "a Databricks-internal secure-release
repo". `PUBLISHING.md` keeps the build half and defers the repo name and
workflow paths to the runbook.
No behaviour change: no trigger, input, permission, or step logic is touched.
The only executable change is the summary `echo` block, verified by extracting
it from the YAML and running it.
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
`RELEASING.md` documents the whole release pipeline, including the private
Databricks secure-release repo, its workflow filenames, and its dispatch
inputs. A public reader can't act on any of that, so per the thread with Corey
and Rice it moves to `omnigent-internal` (`RELEASING.md`).
This deletes the file here and repoints the six inbound "see RELEASING.md"
pointers (4 workflows, the changelog script) at "the maintainer release
runbook", so nothing links to a path that no longer exists.
Scrubbing the private repo name from the workflow comments and
`editors/vscode/PUBLISHING.md` is a separate follow-up.
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(sessions): expose persisted activity heartbeat
Signed-off-by: Solaris-star <820622658@qq.com>
* docs: broaden updated_at wording to cover session metadata edits
Per review feedback: updated_at also advances on title renames
(including auto-titling), agent switches, and archive toggles — not
just conversation item appends. An orchestrator treating it as a pure
item-append heartbeat should know a mid-stall rename resets the clock.
Broadened the docstring in SessionResponse and the SDK Session class,
and re-ran scripts/dump_openapi.py so the OpenAPI description matches.
---------
Signed-off-by: Solaris-star <820622658@qq.com>
Two bugs in the Kimi Code (kimi) harness integration:
Bug 1 - Omnigent could never detect a completed kimi login. The KIMI_KEY
install spec had no file-based login detector and the setup overview row was
hardcoded to "Not configured"/warn whenever the CLI was installed, so a
successful `kimi login` always showed as not signed in.
Fix: add a subprocess-free detector `kimi_auth.kimi_login_detected()` that
returns True when `~/.kimi-code/credentials/kimi-code.json` exists and is
non-empty (the file `kimi login` writes; verified against kimi CLI v0.29.1),
mirroring the Gemini `gemini_login_detected()` pattern. Wire it into
`harness_readiness._FAMILY_CREDENTIAL_CHECK` (binary + credential gating, like
agy) and make the setup overview row render green "Signed in" when detected.
Bug 2 - Sign-out was broken. The spec declared `logout_args=("logout",)` but
kimi has no `logout` subcommand (`kimi logout` errors "unknown command" on
v0.29.1). Set `logout_args=None` so `harness_logout` is a no-op for kimi (same
as Qwen / agy) and remove the "Sign out (kimi logout)" row and its branch from
the Kimi drill-in. Docstrings/comments claiming kimi ships `kimi logout` are
corrected.
Tests: add tests/onboarding/test_kimi_auth.py (present/absent/empty credential
via tmp paths), update the harness_install/harness_readiness onboarding tests
for the new logout_args=None and binary+credential readiness, and update the
CLI drill-in / setup-overview tests (no sign-out row; signed-in vs
not-configured overview row).
Signed-off-by: evangoh122 <evangohsg@gmail.com>
Signed-off-by: Evan Goh <authoremail@example.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Approving a plan from the web UI did nothing: the card showed as
approved but the plan never ran, and answering in the terminal view was
the only way through. Claude Code ignores a PermissionRequest hook's
`allow` for ExitPlanMode (that dialog only accepts a TUI answer), so the
`setMode` decision the server builds never took effect. As a result
Claude's `auto` mode was unreachable from the web UI, since the plan
card is the only surface that offers it.
Key the verdict into the pane instead, the way a local user would:
option 1 for accept-with-auto-mode, 2 for accept, Escape for reject.
The bridge only presses a key when the plan dialog is actually on
screen, which keeps a non-plan verdict (or one already answered in the
terminal) a no-op. Rides the approval event the server already forwards
to the runner, so no new event type or server plumbing.
Co-authored-by: Isaac
Signed-off-by: Pranav Setlur <psetlur@gmail.com>
The Databricks Apps entrypoint built every other store but never the
project store, and create_app mounts the projects router only when a
project store is wired — so first-class Projects were non-functional
on every Databricks Apps deployment while the bundled web UI still
offered project creation. The CLI server and Docker entrypoint paths
already wire it.
Construct SqlAlchemyProjectStore from the Lakebase DB URI and pass it
to create_app, mirroring the other stores.
Co-authored-by: Isaac
Claude-Session: https://claude.ai/code/session_01P9dr2dYHrwMvnXvJsjLDKk
Signed-off-by: Randy 🌞 <randypitcherii@gmail.com>
* Central CTA + background bugfix
Landing screen:
- Headline moves to Hanken Grotesk at 400 weight ("What should we build?"),
self-hosted via @fontsource-variable so no CDN is involved, exposed as the
`font-display-alt` token.
- The project variant swaps the bare folder glyph for a pink rounded tile,
using a new `tag-pink` token from the design's tag palette.
- The composer placeholder and its aria-label now name the selected project
("Start a new session in <project>") instead of always reading the generic
task prompt.
Bug fix — the mobile sidebar was see-through. Below md the sidebar is a
full-screen overlay on top of the chat, but the per-theme canvas rules paint
it with the `background` shorthand, which resets background-color and silently
overrode Sidebar.tsx's max-md:bg-card-solid; the dark stack is entirely
translucent, so the conversation showed straight through. Restores an opaque
fill under the gradients below md only, at matching specificity and after the
theme rules, so desktop keeps its intended translucency.
Adds regression tests for that contract, and updates the landing-screen tests
and visual-suite docs for the new headline.
Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
* test(e2e-ui): regenerate visual baselines
---------
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
* perf(runner): bound per-runner memory via glibc arenas + threadpool cap
Each session spawns its own runner process, and each grows to ~200MB in
prod, over-using host resources. Profiling shows ~123MB is the irreducible
import floor; the growth on top is runtime bloat from threaded Python on
glibc: the runner offloads heavily via asyncio.to_thread, the default
executor sizes to min(32, cpu+4) threads, and glibc opens up to 8*ncpu
malloc arenas that never return to the OS. Nothing tuned any of this.
Three low-risk, env-gated levers (all no-ops or benign off Linux):
- MALLOC_ARENA_MAX=2 + a 128 MiB trim threshold, injected into the runner
child env at both spawn sites via a shared _proc.malloc_tuning_env()
helper. Empty off Linux; OMNIGENT_RUNNER_MALLOC_ARENA_MAX=0 reverts.
- Cap the asyncio default executor at 8 workers (runner.threadpool_max_workers
config key, OMNIGENT_RUNNER_THREADPOOL_MAX_WORKERS env override), set before
any to_thread use so the 20-thread default pool is never created.
- gc.freeze() after app construction to drop the static import graph from
GC's tracked set.
This targets the runtime growth, not the import floor; collapsing the floor
itself (a copy-on-write zygote) is tracked separately.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): apply the glibc arena cap at the zygote exec
The zygote forkserver landed and is now the default runner spawn path, which
silently defeated this branch's MALLOC_ARENA_MAX injection. glibc reads that
variable once, when its allocator initializes at exec; a zygote-forked runner
never execs, it just replaces os.environ, so the value arrived far too late to
configure an allocator and the cap stopped applying to every runner.
Move the injection to the zygote's own Popen -- the single real exec on this
path -- so all forked runners and harnesses inherit an already-capped
allocator. Two tests pin the contract at that boundary, including that an
operator's explicit export still wins.
The other two levers on this branch (the 8-worker threadpool cap and
gc.freeze()) live inside _run_tunnel_from_env, which every runner reaches
regardless of how it was started, so they were unaffected. Note in
malloc_tuning_env why the arena cap is glibc-only: macOS libmalloc uses
per-CPU magazines with madvise reclaim and ignores MALLOC_ARENA_MAX, so macOS
hosts get their reduction from the threadpool cap (measured: 21 threads -> 9).
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
nimble-python is the optional `nimble` extra and the import is already
guarded by try/except ImportError. Pyrefly has no way to know it's
intentionally absent, so annotate with `# pyrefly: ignore[missing-import]`
to silence the false-positive without changing runtime behaviour.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(policies): add force-push protection to GitHub policy
Add a `deny_force_push` parameter (default `True`) to the GitHub
policy that blocks `git push` with force flags (`--force`, `-f`,
`--force-with-lease`, `--force-if-includes`) regardless of
repo/branch allowlists. This prevents agents from rewriting remote
history, which can destroy commits and break collaborators' clones.
The check fires before repo/branch gating so even a force push to
an undeterminable remote alias is denied rather than surfaced as ASK.
Set `deny_force_push=False` to let force pushes through normal
write gating.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(policies): merge startswith calls to satisfy ruff PIE810
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* style(policies): join force-push condition onto one line for ruff format
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* spike(runner): measure copy-on-write savings from a warm-fork zygote
Each session spawns its own runner process, and each pays a ~123MB import
floor for omnigent's own graph plus pydantic/fastapi/httpx. Runtime tuning
trims the growth on top but can't touch that floor; the only way to collapse
it is to import the graph once in a warm parent and os.fork() a child per
session, sharing the read-only import pages copy-on-write.
This standalone script measures whether that COW sharing actually
materializes before we commit to the full zygote architecture. It imports the
runner graph once, forks N idle children, and reports aggregate memory against
an N-process Popen baseline, optionally with gc.freeze().
Not wired into the daemon — this is a measurement gate, not a feature. On this
macOS box (N=8) the fork path showed ~82% lower aggregate footprint than the
Popen baseline, but macOS phys_footprint is only an indicative analog to Linux
Pss and the children idle (no COW erosion from refcount page-dirtying), so a
Linux-under-load measurement is still required before productionizing.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(runner): add copy-on-write zygote forkserver for runner processes
Every session spawns its own runner, and each pays the full ~120MB import
floor (omnigent's graph + pydantic/fastapi/httpx). On a host running N
sessions that floor is duplicated N times. This adds a zygote: a single
long-lived process that imports the runner graph once and os.fork()s a child
per session, so on Linux the read-only import pages are shared copy-on-write
and each extra runner costs only the pages it dirties.
Design (grounded in the daemon/runner lifecycle, not the naive sketch):
- omnigent/runner/_zygote.py — the forkserver. Single-threaded, no event loop
or network; imports the graph once, gc.freeze()s it, then blocks on an
AF_UNIX control socket forking a child per request. The child reopens its
log, replaces os.environ with the request env, and calls the unchanged
_entry.main() — so it behaves exactly like `python -m omnigent.runner._entry`.
It is Popen-exec'd by the daemon (never forked from it), so it inherits none
of the daemon's asyncio loop / websocket / worker threads — the classic
fork-in-multithreaded-async deadlock is avoided by construction.
- omnigent/host/runner_zygote.py — the daemon-side client. ZygoteManager owns
the control socket; ZygoteRunnerProc is a Popen-shaped shim so the existing
_RunnerHandle / _watch_runner / _handle_stop paths are unchanged. The daemon
is NOT the forked runner's parent, so poll()/returncode/wait() round-trip to
the zygote (the real parent) for exit status while terminate()/kill() signal
the pid directly.
- connect.py — _handle_launch forks via the zygote when enabled, else the
original Popen. RUNNER_PARENT_PID is set to the ZYGOTE's pid (not the
daemon's) because the runner's orphan watchdog compares os.getppid(); daemon
death -> control-socket EOF -> zygote exit -> runners reparent -> each tears
itself down, preserving today's parent-death semantics through one hop.
Gated behind OMNIGENT_RUNNER_ZYGOTE=1 and Linux-only; any zygote failure
disables it for the daemon's life and falls back to a direct Popen, so it is
never a hard dependency. Also removes the Phase-1 measurement spike script,
which this supersedes.
Verified on macOS: a real zygote subprocess forks children, reports pids and
exit codes, isolates per-fork env, reaps cleanly, and tears down on stop
(fork works on macOS even though the COW savings are Linux-only). The
production memory win and a full session-through-the-tunnel run are unverified
here — they need a Linux host under load, which this change is written to be
turned on for.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): address zygote review feedback
- connect.py: a failed fork no longer stops the running zygote. Stopping it
would kill healthy runners already forked from it (their orphan watchdog
sees the parent die), so one bad fork could take down unrelated live
sessions. Latch a `_zygote_disabled` flag for future launches instead and
retain the manager so the zygote is still reaped on daemon shutdown.
- runner_zygote.py: wait() after kill() in stop() so a zygote that ignored
SIGTERM is reaped rather than lingering as a zombie.
- _zygote.py: unify the _entry/app/native import to a single `from ... import`
(CodeQL flagged mixed import styles).
- test: build the fresh-interpreter probe via an explicit newline join instead
of implicit adjacent-string concatenation (CodeQL).
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): forward --log-to-stderr TTY fd through the zygote
The direct-Popen launch path forwards OMNIGENT_LOG_TTY_FD via
child_logging_popen_kwargs so a detached runner can still mirror logs to the
daemon's terminal. The zygote path dropped it, so --log-to-stderr mirroring
was lost for zygote-forked runners.
Forward it across both hops:
- daemon -> zygote: reuse child_logging_popen_kwargs to dup the TTY fd and add
it to the zygote's pass_fds (the helper also rewrites env[LOG_TTY_FD] to the
duped number).
- zygote -> forked runner: the valid fd number inside the child is the one the
zygote inherited, not the daemon-side number the payload carries, so the
child restores LOG_TTY_FD from the zygote's own value (and clears a stale
payload value when the zygote has no terminal mirror).
Adds a test asserting a bogus payload LOG_TTY_FD is cleared in the child.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): address second round of zygote review feedback
- _zygote.py: create the forked child's log file 0o600, not 0o644. Runner
logs can carry secrets (tokens, prompts); matches create_process_log_path.
- _zygote.py: the child guard now preserves SystemExit's code instead of
flattening it to a traceback + exit 1, so a zygote-forked runner exits with
the same code as `python -m omnigent.runner._entry` (main() raises
SystemExit on a tunnel rejection). New test covers it via a raise seam.
- runner_zygote.py: stop the partially-started zygote if the initial ping
raises (timeout / EOF), so a failed start never leaks a process + socket.
- runner_zygote.py: signal via signal.SIGTERM / signal.SIGKILL instead of the
raw 15 / 9.
- test: mark the suite posix_only (it uses os.fork / pass_fds) so cross-
platform sweeps skip it on Windows.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(runner): enable the zygote on all POSIX hosts, not just Linux
The host daemon runs on the user's own machine — most often macOS — so a
Linux-only gate denied the copy-on-write import-floor savings to the majority
of hosts. Gate on IS_POSIX instead (the zygote needs os.fork + AF_UNIX
fd-passing, both POSIX; Windows still takes the direct Popen path).
macOS is the platform where fork-without-exec is riskiest (CoreFoundation/GCD
abort a forked child that touches them), so this was verified rather than
assumed. The abort is triggered by forking from a MULTI-threaded process, which
the zygote already designs against: it forks from a single-threaded parent
(asserted active_count()==1) and does create_app + all network work in the
child. Evidence on this macOS box:
- A faithful fork probe (fork from the single-threaded import state, child runs
create_app + getaddrinfo + TLS ctx + asyncio + httpx) survived 5/5. The same
work forked from a multi-threaded parent SIGSEGV'd 2/3 — confirming the
single-threaded fork is what makes it safe.
- test_host_launch_runner_and_session_round_trip passes with
OMNIGENT_RUNNER_ZYGOTE=1: a real host daemon forks a runner through the
zygote, the runner connects its tunnel, and a full mock-LLM session round-trip
completes. The daemon log confirms the zygote path (distinct zygote/runner
pids), not a Popen fallback.
Also adds an info log on the successful zygote-fork path so operators can see
the zygote is active and which pids are involved.
Still opt-in behind OMNIGENT_RUNNER_ZYGOTE=1 with the full Popen fallback; the
steady-state Pss win under load remains best measured on a Linux host.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(runner): fork harness subprocesses from the runner zygote
The harness subprocess (`python -m omnigent.runtime.harnesses._runner`) is a
separate exec per conversation, so it re-pays its import floor — and that floor
is ~54MB of the same common graph (fastapi/pydantic/omnigent-core) the runner
zygote already holds resident. This extends the zygote to fork harness children
too, sharing that graph copy-on-write instead of exec'ing a fresh interpreter.
- _zygote.py: the serve loop becomes a single-threaded `selectors` multiplexer
over the daemon socket PLUS one inherited control socket per forked runner.
A new `fork_harness` command forks a child that reproduces `_runner.main(argv)`
in-process. The runner-fork request/response bytes are unchanged; the new
multiplexer wraps them rather than rewriting them. A forked child closes every
inherited zygote-side socket (it never speaks the fork protocol).
- _harness_zygote_client.py (new): the runner-side client. `HarnessZygoteClient`
reads the inherited control-socket fd from OMNIGENT_RUNNER_ZYGOTE_HARNESS_FD;
`ZygoteHarnessProc` is an asyncio.subprocess.Process-shaped shim (pid /
returncode / wait / send_signal / kill) with a background poll task keeping
returncode fresh for _wait_for_bind's synchronous reads.
- process_manager.py: `_spawn_harness_process` forks via the zygote when the
runner was itself zygote-forked, else the original create_subprocess_exec;
disabled on first failure so it falls back for the process's life.
- _runner.py: a zygote-forked harness has the zygote (not the runner) as OS
parent, so its watchdog probes the runner pid explicitly instead of trusting
os.getppid(), and skips PR_SET_PDEATHSIG (which would bind death to the
zygote). Gated by OMNIGENT_HARNESS_ZYGOTE_FORKED.
Present only when the runner itself was zygote-forked; any failure falls back to
a direct exec, so the harness fork is never a hard dependency. The win is
bounded to the ~54MB Python wrapper (the external claude/codex CLI is a separate
exec no Python zygote can share) and materializes under multi-conversation
fan-out. Verified on macOS: fork_harness forks, reports pid + exit code,
round-trips argv, reaps, and leaves the daemon socket serving; existing
process_manager tests unchanged. Linux Pss savings still unverified.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): clear pyrefly type errors in the zygote
- ZygoteRunnerProc.wait: narrow on `timeout` (not just `deadline`) so
TimeoutExpired(timeout=...) gets a `float`, not `float | None`.
- _spawn_zygote_process: pass stdin/stdout/stderr explicitly with a typed
`BinaryIO | None` log handle instead of a `dict[str, object]` splat that
matched no Popen overload.
- _ZygoteServer.serve: cast selector key.fileobj (HasFileno | int) to socket
— only sockets are ever registered.
- _ZygoteServer._on_readable: wrap the bytearray partition result in bytes()
before dispatch, which expects bytes.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): harden zygote failure paths (crash recovery, exit-code leak)
Review flagged three correctness bugs in the unhappy lifecycle paths; none are
security issues but each is reachable in prod.
1. Unexpected zygote crash stranded the daemon's view of every child. The
daemon isn't the runner's OS parent, so once the zygote died it had no
channel to learn a runner exited — ZygoteManager.poll returned None
("still live") forever, so _watch_runner looped, _handle_runner_status
reported gone sessions as alive, and _handle_stop's final wait() could hang.
Now poll() probes the runner pid directly when the zygote is gone: a dead
pid surfaces a non-zero sentinel (254) so the runner reads as dead-and-
failed, not eternal alive. _handle_stop's post-kill wait() is now bounded.
2. _exit_codes leaked for a dropped runner's harness children. Exit codes were
only popped via poll, but a dropped runner's harnesses have no remaining
client to poll them — the entries accumulated (unbounded map growth +
pid-reuse misattribution). _drop_runner now discards those descendants'
codes and marks still-live ones orphaned: _reap waitpid's them (no zombies)
but discards the code instead of storing it.
3. ZygoteHarnessProc.wait() masked a crashed harness as exit 0. If the zygote
went away, wait() returned 0, so a harness that crashed on boot (bind
failure, import error) read as a clean exit and the process manager could
hang waiting for a bind that never comes. Now probes the harness pid and
returns a non-zero sentinel when the code is unrecoverable.
Also: tighten "Linux-only" docstrings to "POSIX; COW savings on Linux" (the
gate is IS_POSIX and the path runs on macOS), and add a sleep test-seam so the
new failure-path tests can hold a child genuinely alive.
Tests: kill the zygote under a live runner and assert the daemon eventually
sees it dead (not hanging); a dropped runner's harness code is not retained; a
crashed harness with an unrecoverable code surfaces as failure, not 0.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): keep zygote poll/wait off the daemon event loop
Review flagged a liveness regression on the enabled path: for a zygote-forked
runner, poll()/wait() are blocking control-socket round-trips (with lock
contention against a booting zygote that holds the lock across its ~120MB
import), not the lock-free waitpid the direct-Popen path used. Calling them on
the loop thread could freeze the whole daemon — all sessions, websocket
traffic, heartbeats — until the import finishes or the 30s control timeout
elapses.
- _watch_runner: poll() now runs via asyncio.to_thread.
- _handle_stop: now async; the poll/terminate/wait sequence runs off-loop in a
_stop_runner_proc helper. Its dispatch site and three tests updated to await.
- _tracked_runner_pids: include the zygote pid so the orphan reaper never
waitpid's the zygote out from under ZygoteManager._proc on an unexpected
crash (which would confuse is_running()/stop()).
Also updates test_poll_after_stop to use a live child, since the crash-recovery
sentinel (254) now correctly fires for an already-exited pid after stop().
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): status query off-loop + enable zygote by default
- _handle_runner_status did its poll() on the event loop, the one place the
PR hadn't moved off it. For a zygote-forked runner poll() is a blocking
control-socket round-trip (bounded only by the 30s control timeout, and
contended against a booting zygote), so a slow zygote could stall the whole
daemon for a single status query. Made it async and run the poll via
asyncio.to_thread, matching _watch_runner / _handle_stop. Dispatch site and
the three status tests updated to await.
- Enable the zygote by default: OMNIGENT_RUNNER_ZYGOTE is now opt-OUT
(=0/false/no/off), not opt-in. The host daemon runs on the user's own
machine (most often macOS), so defaulting on lets most users share the
~120MB import floor. Still POSIX-gated with a full Popen fallback, so an
unsupported platform or any zygote failure is transparent.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: prevent mid-spawn launch leaks and harden zygote request handling
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(web): set the text size steps from the design
Body and chat-thread text are both 13px/18px in the design; the shared
`text-13` step was on a 20px line, so tighten it to 18px. Adds the 12px/16px
caption step used by sidebar section subtitles (Projects, Sessions).
Defines the steps only — switching each surface onto them is follow-up work.
Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
* feat(web): put chat and sidebar text on the design's type scale
The chat thread hard-coded its own 15px/24px with negative tracking, and
sidebar rows set a size but no line height, so neither matched the design.
- Chat bubbles (user and assistant share the wrapper): 13px/18px, and the
-0.01em tracking is dropped — the design specifies 0.
- Sidebar body rows: pin the line height to 18/13 of the font size, which was
previously left to inherit.
Both stay in rem/unitless so the mobile root-font bump and the Appearance
font-size setting keep scaling them. Sidebar section captions were already
12px/16px and are unchanged.
Co-authored-by: Isaac
* test(e2e-ui): regenerate visual baselines
* refactor(web): express the sidebar line height in rem
1.3846 was the 18/13 ratio written as a unitless number — unreadable, and it
took arithmetic to confirm it meant 18px. 1.125rem is 18px directly and
scales the same way, matching how the chat wrapper states it.
Co-authored-by: Isaac
---------
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
OMNIGENT_RUNNER_ENV_PASSTHROUGH lets an operator name extra env vars for the
host to forward on to spawned runners (provider gateway wiring, config env: refs,
etc.). It worked locally but was a silent no-op in --server mode: the remote
daemon env is allowlisted by a prefix set of DATABRICKS_ + LC_/MLFLOW_/OTEL_/
OMNIGENT_OTEL_ — NOT plain OMNIGENT_ — so the control var itself was stripped at
the CLI->daemon hop, and _build_runner_env never saw the names it listed. Any var
forwarded through the passthrough (e.g. a Linear API key for the repro-agent)
reached the runner locally but never remotely.
Add OMNIGENT_RUNNER_ENV_PASSTHROUGH to _RUNNER_ENV_ALLOWLIST so it survives both
hops. It carries only env var NAMES, not secrets, so allowlisting it leaks
nothing on its own — each named var must still independently reach the daemon
(here via the DATABRICKS_ prefix).
Tests: a daemon-hop test (remote env keeps the control var) and an end-to-end
two-hop test (a named var survives CLI->daemon->runner, an unnamed one doesn't).
Both fail without the one-line allowlist change.
Co-authored-by: Isaac
* dev/repro-agent: give Linear tickets a real fetch path + follow linked GitHub issues
The local repro-agent pointed Linear tickets at nonexistent "Linear tools",
so Linear runs had no way to read the ticket body and fell back to guessing
from the URL slug — noticeably worse reproductions than GitHub issues, which
have a working `gh issue view` path.
Wire Linear to the same GraphQL path the internal issue-sync agent uses
(api.linear.app/graphql, `Authorization: $LINEAR_API_KEY`, no Bearer), pulling
description/comments/attachments via sys_os_shell. When the key is absent or
auth fails, stop with needs_more_info naming the missing key instead of
guessing. Also: when a Linear ticket links a GitHub issue, always fetch that
issue too and treat it as authoritative for the technical journey — that
richer thread is why GitHub-first runs reproduced better.
Co-authored-by: Isaac
* dev/repro: forward the Linear key through the --server env strip
Reading a Linear ticket needs the key in the agent's shell, but under --server
the CLI->daemon->runner hops strip everything not allowlisted. The DATABRICKS_
prefix survives only the first hop; the daemon->runner hop has no DATABRICKS_
prefix. So dev/repro.py now names DATABRICKS_LINEAR_API_KEY in
OMNIGENT_RUNNER_ENV_PASSTHROUGH (itself allowlisted) when a Linear URL is passed
and the key is set, which forwards it the rest of the way. AGENTS.md reads
whichever name is present (LINEAR_API_KEY locally, DATABRICKS_LINEAR_API_KEY
under --server). Warns rather than fails when the key is missing.
Companion change (omnigent-internal): the repro-agent CI workflow must set
DATABRICKS_LINEAR_API_KEY from secrets.LINEAR_API_KEY in the run step, mirroring
how it already sets DATABRICKS_BEARER for the LLM key.
Co-authored-by: Isaac
* dev/repro: mirror LINEAR_API_KEY into the DATABRICKS_ name
Maintainers typically export the plain LINEAR_API_KEY locally, so copy it into
DATABRICKS_LINEAR_API_KEY when only the plain name is set — then the same
passthrough forwarding carries it past the --server env strip. Warn only when
neither is set.
Co-authored-by: Isaac
* feat(web): make the rails flush boxes and move the canvas gradient
The sidebar and workspace rails were floating cards (margin, rounded
corners, border, shadow) on a gradient canvas. The design has them flush to
the window edges, reading as part of the canvas.
- Left sidebar and right workspace rail sit flush: no outer margin, no
rounding, no drop shadow. The workspace rail keeps a left divider.
- Light canvas is flat white; the brand gradient moves onto the left
sidebar, joined by the mock's dot-grid and pink corner glow.
- Dark canvas carries the mock's purple gradient; the dark sidebar gets the
same dot-grid plus a purple bottom wash and the diagonal sheen.
- Both rails are excluded from the dark glass rule instead of overriding it,
so they no longer pick up its blur, sheen, fill, or border. The workspace
rail's panel contents are transparent too.
- Dark surface tokens (--card, --card-solid, --tray, --muted, --background)
move off their purple tint onto neutral slate.
Consolidates the canvas/rail CSS so each surface owns its full background in
one rule, and drops the now-redundant ::before dot overlay.
Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
* test(e2e-ui): regenerate visual baselines
---------
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
Broaden issue-triage auto-assignment from P0/P1-only to every triaged
issue except needs_info ones. The gate now keys off needs_info alone, so
any bug/enhancement/doc issue with enough info to triage gets a
load-balanced area owner (least open assigned issues first, LLM rank as
tiebreaker) instead of only high-priority ones. Drops the now-unused
priority/type branch from the shell gate.
Also refresh .github/areas.json ownership:
- remove SabhyaC26 from all areas
- add PattaraS to harness-antigravity (keeps it at the 2-owner minimum)
- reactivate dbczumar (owners_paused -> owners) across their areas
Co-authored-by: Isaac
* fix(web): remount terminal view when switching same-vendor sessions
Two sessions of the same shape share a fixed agent-terminal id (e.g. every
claude-native session's `terminal_claude_main`, every SDK session's
`terminal_tui_main`). ChatPage stays mounted across a session switch and only
feeds MainTerminalView / TerminalsPanel a new conversationId, so keying the
xterm wrapper on the terminal id alone let React reuse the existing mount —
the pane kept the previous session's 20k-line scrollback until the new
WebSocket reconnected and tmux repainted. The stale history cleared only on a
manual refresh.
Scope the wrapper key to `${conversationId}:${terminalId}` in both surfaces so
a session switch forces a clean remount (fresh xterm + WebSocket, no stale
buffer). Add regression tests that switching conversationId with the same
terminal id remounts the TerminalView.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(web): assign terminal mount id once per mount
Copilot review flagged that useRef(++terminalMountSeq) evaluates the
increment on every render (useRef ignores the arg after first render),
so the module counter advanced on re-renders — contradicting the
comment. The read value (instance.current) was still stable, so the
assertion held, but assign the id conditionally so the counter tracks
real mounts.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
The Chat/Terminal switcher moved into the header as a Radix DropdownMenu
whose trigger toggles on pointer-down and carries a controlled hover
tooltip on the same node (ViewModeToggle.tsx). On a busy page — a live
terminal stream plus that tooltip re-rendering during the click — a lone
`.click()` occasionally nets the menu back to closed, so the follow-up
`expect(menuitemradio).to_be_visible()` times out. That is the observed
flake in test_codex_goal_mode and the native render-parity suites: the
failure snapshot shows `tooltip "Terminal view"` (rendered only while the
menu is closed) with no menu items.
Add a shared `_select_view_mode(page, option)` helper that reopens the
menu in a retry loop until the target radio item is actually visible, then
selects it, instead of trusting a single toggle click. Route
`_ensure_chat_view` and every native-parity `_open_terminal_view`
(codex, claude, goose, hermes, cursor, kiro) through it.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Repoint the gray text and border tokens onto the shadcn Zinc scale so the
UI's neutrals match the design system:
- Primary text (--foreground, --card-foreground, --secondary-foreground,
--sidebar-foreground) -> Zinc 800 #27272a
- Secondary text (--muted-foreground) -> Zinc 500 #71717a
- Default border (--border, --input, --sidebar-border) -> Zinc 200 #e4e4e7
- Strong border (--border-strong) -> Zinc 400 #a1a1aa
Also adds the two tokens the palette needs but the app lacked:
--border-weak (Zinc 150) and --foreground-tertiary (Zinc 400), exposed as
Tailwind utilities.
Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
The codex goal-mode + native-parity targets need the Codex-parity Rust
sidecar. flake-stress-ui.yml relied on the fixture's inline `cargo build`
at test time, capped by --timeout=300. On a cold Rust cache every parallel
attempt independently compiles the ~1100-crate tree and overruns the
per-test timeout, so all attempts die at fixture setup before the test
body ever runs — masquerading as a 100% failure rate unrelated to the
target under test.
Mirror e2e-ui.yml / ci.yml: add a dedicated build-sidecar job that
compiles the sidecar once (same main-scoped cache key so it usually
restores), uploads the ~10MB binary, and has each attempt download it and
set CODEX_PARITY_SIDECAR_BIN. build_sidecar_bin() then returns the prebuilt
path and skips cargo entirely. Drops the per-attempt Rust toolchain + target
-dir cache that never made the inline build fit the timeout.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
`dev/repro.py --public` sets `public: true` in the agent's input contract, and
the agent shares the session read-only (`sys_session_share __public__`) at the
start of its run so it is browsable live — useful when watching a run or
reproducing against a shared --server. Off by default (a local session is
already yours to browse).
- dev/repro.py: add --public; include `"public": true` in the payload when set.
- config.yaml: re-add `agent_session_sharing: public` to grant the __public__
capability (opt-in via the flag).
- AGENTS.md: document the `public` input; make sharing the first preflight step.
- README.md: document the --public flag.
Co-authored-by: Isaac
* dev/repro: add worktree-isolating driver script; clarify browser context
Add dev/repro.py — a maintainer-only wrapper around `omnigent run
dev/repro-agent`. It prompts for the bug URL (or takes it as an argument /
bare id like OMNI-1234 / 3987), creates an isolated git worktree off the
current checkout's HEAD (branch repro/<slug>, auto-suffixed on collision),
and runs the agent FROM that worktree so the authored e2e test lands on its
own branch without dirtying your checkout. The worktree is always kept; the
script prints its path + branch + cleanup command at the end.
It lives under dev/ (not shipped in the wheel) rather than as an `omni`
subcommand because it depends on a source checkout — the repro-agent authors
into tests/e2e_ui/ / tests/e2e/, which only exist here.
Also, from PR review:
- AGENTS.md: note that UI-journey reproduction drives the desktop app's
embedded browser, so it expects a desktop / embedded-browser context (fall
back to the backend path / needs_more_info when there's no browser pane).
- README.md: document the dev/repro.py driver.
Co-authored-by: Isaac
* dev/repro-agent: handle compound / multi-symptom bug reports
Ported from the internal repro-agent (omnigent-internal#24). A single bug
report often bundles several distinct symptoms (e.g. "picker unavailable AND
catalog defaults lag"), and they can have different truth on the running
build — one already fixed, the other still live. Averaging them into one
verdict hides the part that's still broken.
AGENTS.md now instructs the agent to:
- enumerate each claimed sub-symptom in Step 1 (don't collapse a compound
report into one journey),
- reproduce and judge each independently in Step 2, and
- roll up to an overall verdict where ANY live sub-symptom ⇒ reproduced
(already_fixed only when every facet is fixed), emitting a per-facet
breakdown (`facets`) in the output so a partial fix stays visible.
Wording adapted to the local variant (running build / local session; no
deployed-app or public-share references).
Co-authored-by: Isaac
* dev/repro: drop the `ref` input — always reproduce against the running build
`ref` never controlled what was validated: the agent always reproduces against
the app it is connected to (the running build / latest main), and `ref` was
only informational — and redundant, since the reported version is already in
the bug report the agent reads. Simplify the input contract to just `bug_url`.
- dev/repro.py: remove the --ref option; the payload is {"bug_url": ...}.
- config.yaml / AGENTS.md / README.md: drop the ref bullet/examples; keep the
guidance that reproduction is always against the running build (so an
old-version report can still land already_fixed).
Co-authored-by: Isaac
A developer-facing repro agent under dev/. Given just a bug (a bug_url — GitHub
issue or Linear ticket — plus an optional ref), it reconstructs the user journey
from the linked report, drives the running Omnigent app it is connected to (the
server `omnigent run` spins up, or one passed with --server) through that journey
until the failure happens live, and authors a durable e2e test (Playwright under
tests/e2e_ui/ for UI bugs, or tests/e2e/ for backend) as the regression artifact.
It reproduces against whatever app it is connected to and authors the test into
the current checkout, so a developer can run it against their own local server:
omnigent run dev/repro-agent -p '{"bug_url":"https://github.com/omnigent-ai/omnigent/issues/1234"}'
It does not fix the bug, merge, or push — it produces a live-confirmed
reproduction plus the test and hands off (the fix half owns the before/after
fail->pass proof).
Files:
- config.yaml — claude-sdk brain, os_env shell/file access, blast-radius guard.
- AGENTS.md — the operating procedure (confirm workspace -> reconstruct journey
-> reproduce live -> author the e2e test -> structured verdict).
- README.md — prerequisites, usage, and what it produces.
Co-authored-by: Isaac
omni host status was slow because _add_daemon_host_status made a
GET /v1/hosts/{id} request for every daemon record, including the many
stale records accumulated over dev sessions (39 in one measured case).
Dead processes can't have an online tunnel, so the correct answer is
host_status=offline with no network round-trip.
Skip the HTTP call when process=offline and set host_status directly.
This cut omni host status from ~14s to ~5s on a workstation with many
stale daemon records.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
When a session's workspace directory no longer exists on the host
(e.g. a worktree was deleted), the host was returning a generic
failed status with no error_code, causing the server to silently
wait out the full connect timeout and then surface a generic
'runner_failed_to_start' banner.
Changes:
- Add WORKSPACE_MISSING_ERROR_CODE ('workspace_missing') to host/frames.py
- Host returns this code when workspace.is_dir() fails, alongside the
existing descriptive error message
- Server (routes_events.py post_event) handles workspace_missing the same
way as harness_not_configured: immediately consumes the user message and
persists an actionable runner_failed_to_start error item with the host's
'workspace path does not exist: ...' message instead of timing out into
a generic RUNNER_UNAVAILABLE
- orchestration.py _ensure_runner_relay_ready skips the connect-timeout
wait for workspace_missing (same as harness_not_configured), and records
the refusal in runner_exit_reports so snapshot-based renders also show
the actionable cause
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): stop rendering shell-style env vars in prose as LaTeX math
Error messages like "Unresolved environment variable '$LLM_API_KEY' … Set
$LLM_API_KEY or $OMNIGENT_LLM_API_KEY" render through the assistant markdown
renderer, which has single-dollar math enabled. The paired `$` tokens collapsed
into a garbled inline formula.
normalizeExplicitMathDelimiters already escaped a lone `$` before a digit
(currency); extend that heuristic to also escape shell-style variable
references ($VAR_NAME and ${VAR_NAME}, SCREAMING_CASE) so they stay literal text
instead of flipping the math span.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* docs(web): clarify SHELL_VAR_RE handles single-char braced refs
Address Copilot review: the comment said "2+ chars" but the braced
alternative uses `*`, so `${A}` matches. That's intended — braces
disambiguate a variable reference, so one char is enough there, while the
bare form still requires 2+ so `$X …` reads as inline math. Fix the comment
and add a test for both cases.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): require full-token boundary for bare shell-var match
Address Copilot review: SHELL_VAR_RE's bare branch matched a SCREAMING_CASE
prefix of a mixed-case token (e.g. `$FOOBar$`), escaping the opening `$` while
leaving the closing `$` as a delimiter — an unbalanced span that breaks
genuine inline math. Add a `(?![A-Za-z0-9_])` boundary so only full
SCREAMING_CASE tokens match, and greedy backtracking can't settle on a prefix.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
When the VPN drops, a corporate proxy answers the host tunnel's
WebSocket upgrade with 401/403 before the request reaches the Omnigent
server. `_classify_http_status` treated those as permanently fatal, so a
live, already-registered host exited with code 1 and the user had to
re-run `omnigent host` after reconnecting.
A host that already completed an upgrade proved its credentials and
authorization are valid, so a later 401/403 is almost always a transient
network-path artifact. For a connected host, 401/403 now retries forever
via the normal reconnect path (mirroring the existing login-redirect
design), with a once-per-outage stderr notice so a foreground
`omnigent host` isn't silent. A fresh, never-connected host still fails
loud on the first 401/403.
Fixes OMNI-2367.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Two fixes to _ManagedMintTokenFactory and _InitialAuthTokenFactory:
1. Only latch declined=True on 400/404 if the factory has never successfully
minted a token. A 400 mid-session (e.g. during an IP ACL flip) is
transient — the server already proved it mints for this runner, so treat
it like any other transient failure instead of bricking the factory.
2. Add a declined property to _InitialAuthTokenFactory that proxies the
inner fallback factory. Without this, auth_flow sees declined=False on
the outer wrapper and raises 'no token' instead of falling back to bare
requests, causing infinite retry loops in PATCH external_session_id and
other callbacks after the inner factory latches declined.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
A managed-host wake (resume_managed_host: resuming a dormant resumable
sandbox on the next message) never forwarded launch-pipeline stages to the
caller, unlike the fresh-launch path (_arm_and_start_host), which threads
on_stage through. As a result _run_managed_wake left the session on the
single "provisioning" band that _kick_managed_wake seeded for the entire
resume — even while the host was already re-execing and dialing back — so
the UI showed a frozen "Provisioning sandbox" band for the whole wake.
Thread on_stage through resume_managed_host into _start_sandbox_host (which
already accepts it), and have _run_managed_wake pass a _publish_sandbox_status
closure. The wake now advances to "starting" (emitted by base start_host)
before "connecting"/"ready", matching a fresh launch.
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
Connecting a host to a Databricks-App-deployed omnigent server as a service
principal failed: `omni run --server <app>` resolves credentials through the
Databricks SDK's default chain, which reads only the DEFAULT ~/.databrickscfg
profile. When DEFAULT points at a different workspace than the one fronting the
app, the minted token is for the wrong workspace and the Apps proxy bounces the
request to interactive OIDC (302) instead of admitting it.
Add a `--profile NAME` option to `omni run` that sets DATABRICKS_CONFIG_PROFILE
for the CLI process, so every remote-auth path (_remote_headers, _server_auth,
_DatabricksTokenAuth) resolves the named service-principal profile. This enables
headless M2M access to a deployed app without a prior interactive `omnigent
login`. An explicit --profile wins over an ambient DATABRICKS_CONFIG_PROFILE;
omitting it leaves any preset untouched.
Prereq (Databricks-side, not code): the service principal must have CAN USE on
the app.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(runner): treat HTTP 403 as refreshable on tunnel reconnect
A runner whose auth token expires while the machine is offline can
receive HTTP 403 (not 401) when DNS resolves again and the server
rejects the stale credential. Previously 403 was in
_FATAL_SERVER_HTTP_STATUSES and caused the runner to exit immediately
with no retry, killing any active session.
Move 403 into _REFRESHABLE_HTTP_STATUSES alongside 401. The existing
_handle_refreshable_auth_failure path already handles this correctly:
it attempts one token refresh, and if the factory is invalidatable
(or returns None) the second 403 raises a fatal RuntimeError instead
of looping forever. A runner with no factory still exits fatally on
the first 403.
Add three tests covering the new behaviour:
- 403 with factory → refresh → retry → success
- 403 with invalidatable factory → refresh → persistent 403 → fatal
- 403 without factory → fatal immediately
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): guard 403/401 refresh against transient factory errors
- Drop the inline to_thread(factory) call in the refreshable-status
handler; rely on the loop-top _refresh_auth_token instead, which
already wraps factory calls in try/except for OSError/ValueError.
This prevents a transient IdP error on wake-from-sleep from crashing
serve_tunnel rather than falling back and retrying.
- Also removes the redundant double-refresh-per-cycle that the inline
call introduced.
- Update _handle_refreshable_auth_failure docstring: 401/403 now go
through the streak path, not this function; only 302 redirects
reach it.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(server): import _spawn_archive_stop in routes_core
Missing import introduced in 2ce9c60b.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* perf(sessions): stop and archive in parallel so archiving isn't gated on stop timeouts
Archiving a live session took 5-10s: the sidebar serialized stop -> archive,
and the PATCH handler awaited its own best-effort stop (5s runner / 10s host
teardown ceilings per running session) before flipping the flag — even though
the archive proceeds regardless of the stop's outcome. Fire the client legs
in parallel and detach the server-side stop into a retained background task;
the stop still runs to completion, it just no longer holds the response.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(sessions): let the server own the archive stop so it can't race the client's
Review follow-ups on the parallel-archive change:
- The client no longer sends its own stop_session alongside the archive
PATCH. Two concurrent stops raced the same runner, and because the
runner's stop handlers are not idempotent (kill_session raises once
the pane is gone -> 503), the loser's failure aborted the client stop
before it reached the host-runner teardown -- orphaning a host-spawned
session's dedicated runner. Archive now sends one PATCH.
- The server's detached stop carries the host-runner teardown that only
the client stop used to do, so archiving still drops the runner's
tunnel and flips runner_online. Bulk archive gains this too; it never
sent a client stop.
- The stop is spawned only after the archived flag commits. It ran
ahead of later validations, so a PATCH rejected after that point
(reserved label, runner_id permission) could stop a session it did
not archive.
Adds an e2e_ui browser test for the archive flow plus server coverage
for the teardown and the rejected-PATCH case.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(server): file forked sessions into the source's project
Forking a session filed in a first-class project left the fork unfiled:
fork_conversation built the fork's metadata row without project_id. The
fork route now carries the source's project onto the fork, gated on the
forker owning that project (projects are owner-private, so a fork of a
shared session filed in someone else's project stays unfiled).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): refresh the project folder when a session is forked
A fork inherits the source's project, but the dialog only invalidated the
flat session list — each folder renders its own ["project-sessions", name]
query, which has no poll and converges only on an explicit invalidation.
The push stream can't cover it either: it skips the active session, and the
fork becomes active on navigate. So the clone stayed missing from its folder
until a reload or a re-navigation.
Adds an e2e regression test. It seeds the committed turn the fork action
anchors on straight into the store (new seed_committed_turn helper) instead
of driving a model turn, so it neither waits on nor inherits the flakiness
of the mock-LLM harness.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
web/electron/package.json was deliberately excluded from
update_versions.py because lockstep versions are not valid semver, so
it rotted: v0.7.0 and v0.8.0 shipped a desktop app still calling
itself 0.6.0, and 0.8.1's desktop bump had to be pushed by hand onto
the release branch (and still reads 0.8.0 at the v0.8.1 tag).
Stamp it with the semver translation of the lockstep version instead
(0.6.0rc1 -> 0.6.0-rc.1, 0.7.0.dev0 -> 0.7.0-dev.0, finals
unchanged) — semver orders these the way PEP 440 does (dev < rc <
final), so desktop auto-update comparisons stay correct. check() now
gates the translation, so a drifted desktop version fails the version
lockstep lint. Aligns main's desktop version to 0.9.0-dev.0.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(ci): normalize uv.lock after /regen resolutions
The regen workflow was the one lock-writing CI path left out when the
normalize-then-verify step was added to release/bump/nightly: its
uv lock --upgrade-package runs re-add the size fields the canonical
form forbids, ballooning a /regen'd PR's lockfile diff by ~3k lines of
formatting noise and failing the pre-commit lint on the PR.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(ci): /regen upgrade touches only uv.lock
A targeted Python package upgrade was also deleting and re-resolving
pnpm-lock.yaml from scratch, burying a ~100-line dependency fix under
thousands of lines of npm churn. Plain /regen keeps refreshing both
lockfiles.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* Bump version to 0.9.0.dev0
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore(deps): drop the stale gitpython cooldown exemption
The per-package cutoff (2026-07-24) was added to make 3.1.55 resolvable
while it was inside the P7D window; it aged out, and the frozen cutoff
now excludes 3.1.56/3.1.57, which fix GHSA-p538-c434-8v24 and
GHSA-3f7w-8rr8-f37f — so the OSV audit fails on any PR touching the
lock. The global P7D cooldown admits 3.1.57 on its own now. Lockfile
regen follows via /regen upgrade gitpython.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* chore(oss): regenerate public lockfiles against public PyPI/npm
* chore(oss): regenerate public lockfiles against public PyPI/npm
* chore(deps): normalize the lockfile back to canonical form
The /regen runs regenerate uv.lock without the normalize step the
other lock-writing workflows gained, re-adding the size fields the
canonical form forbids. Text-only cleanup; the resolved versions
(gitpython 3.1.57, aiohttp 3.14.2) are unchanged.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* chore(deps): restore main's pnpm-lock.yaml
The /regen runs regenerate the npm lockfile from scratch even for a
Python-only package upgrade; this PR changes no JS dependency, so
main's lockfile is exactly right for it.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
The auto-generated entry said no user-facing changes: the release's one
change is a cherry-picked revert whose PR is still open against main,
which the changelog curation (merged-PRs-in-range) cannot see.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
The nightly cut is fully unattended, so a broken run blocks nobody and
consumers silently stop getting new builds. Add Nightly Release to the
failure monitor's watch list: its two-consecutive-failures rule and
close-on-green behavior apply unchanged, and skipped quiet nights
conclude success so they close any open tracking issue.
RELEASING.md gains a Nightly builds section: what the workflow does,
how consumers install and update from tags (no PyPI), and that a bad
nightly needs no recovery beyond fixing main.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* feat(harness): add Grok Build (xAI) as a first-class ACP harness
Grok Build (`grok`) had no first-class harness — only usable as a custom `acp:`
agent or as `xai/grok-*` behind openai-agents. Add `harness: grok` (alias
`grok-build`) driving `grok agent stdio` over ACP via the generic AcpExecutor,
the reuse path the issue suggests (like qwen).
- inner/grok_harness.py: thin create_app wrapping AcpExecutor with a fixed
`grok agent stdio` command; auth is Grok's own (grok login / XAI_API_KEY),
Omnigent stores no credential.
- Registry: valid_harnesses / harness_modules / alias grok-build / capabilities
(ACP profile: own-auth, cold resume, SSE permission, interrupt) / label
"Grok Build" / HARNESS_GROK_MODEL.
- Install spec (curl x.ai/cli/install.sh, grok login --device-auth) and
binary-gated readiness, matching the other own-auth CLI harnesses.
Closes#2881
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>
* test(onboarding): include grok spellings in configured-harness-map test
The grok harness added `grok` + `grok-build` to the configured-harness map;
test_configured_harness_map_covers_all_spellings pinned an expected_keys set
that omitted them, so it failed with both as extra items. Add them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>
* test(e2e): exclude grok from the live no-agent harness matrix
Registering grok as a coding harness added it to the matrix's expected set,
but grok is a headless ACP harness driven over stdio: it authenticates from
the grok CLI's own xAI login rather than the shared gateway/profile probe
wiring, so there is nothing for this binary-less no-agent matrix to probe.
Exclude it alongside goose, which is excluded for the same reason, and name
tests/inner/test_grok_harness.py as its coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>
* fix(harness): drop the grok model-override claim nothing implements
The harness registered HARNESS_GROK_MODEL in model_env_keys, but the executor
never read it, so a spec model or /model pick was silently dropped rather than
applied — and the docstring pointed at a session/set_model path this harness
doesn't implement.
Remove the registry entry and the claim. Grok selects its model in its own CLI;
an Omnigent-driven override for ACP-backed harnesses is a separate concern and
should land with the mechanism that actually applies it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: apeltekci <andrew@peltekci.com>
* refactor(harness): declarative catalog for builtin ACP CLI harnesses
Promoting an ACP-speaking vendor CLI to a first-class harness has meant
touching 6+ registration points (capabilities, valid set, module map,
aliases, labels, install spec, readiness, setup steps, a per-harness
spawn-env builder, the live e2e matrix exclusion) plus a near-identical
thin inner module. Recent PRs each re-derived this by hand and one shipped
without its spawn-env builder, silently dropping the session cwd and the
spec sandbox.
Add omnigent/acp_cli_harnesses.py: one AcpCliHarness row per vendor CLI
(label, binary, ACP argv, aliases, install and login metadata). Every
registration derives from the row:
- harness_plugins: validity, module routing (all rows run the shared
omnigent/inner/acp_harness.py wrap), aliases, labels, capabilities
(the generic acp profile), install specs and install keys
- onboarding: one-click install allowlist (npm rows) and vendor-login
setup steps derive; readiness rides the existing install-key gate
- runtime/workflow: one shared _build_acp_cli_spawn_env forwarding the
session cwd and serialized os_env, shell-quoting the resolved binary
- runner dispatch: one membership check covers every current and future
row
- tests: readiness spelling lists and the live-matrix exclusion extend
from the catalog; tests/test_acp_cli_harnesses.py drives a fake row
through the builder and dispatch and asserts full registration per
real row
The catalog ships empty; the first rows land with the Grok Build (#3075)
and Qoder (#3560) PRs, each reduced to one dict entry plus docs.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* refactor(grok): ride the ACP CLI harness catalog, one row instead of hand wiring
Rebase the Grok Build harness onto the declarative catalog from
feat/acp-cli-catalog: the thin inner module, the per-registry entries,
the install/readiness edits, and the manual e2e-matrix exclusion all
collapse into one ACP_CLI_HARNESSES row carrying the same label, alias,
command, install hint, and login metadata.
Riding the shared builder also fixes two gaps the hand wiring had: the
session working folder and the spec os_env/sandbox now reach the grok
subprocess (grok_harness.py read HARNESS_GROK_CWD / HARNESS_GROK_OS_ENV
but nothing ever set them), and a resolved binary path containing spaces
survives the shlex-split command string.
Registration, spawn env, readiness gating, setup steps, and the live
matrix exclusion are asserted per row by tests/test_acp_cli_harnesses.py,
replacing tests/inner/test_grok_harness.py.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: apeltekci <andrew@peltekci.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* feat: add nimble_research builtin backed by Nimble Agent API v2
Add a nimble_research built-in tool that delegates a research task to a
Nimble Web Search Agent through the asynchronous Agent API v2: start a
run (POST /v2/agents/{agent_id}/runs), poll it to a terminal status on
a monotonic deadline, then fetch the cited result. The tool returns a
bounded JSON envelope - run id, output (text or structured JSON), and
trust metadata (confidence, sources, per-claim citations) - capped so a
large result cannot blow the model context.
The builtin registers like web_search: a registry factory plus
runner-local dispatch, so a non-OpenAI model's nimble_research call
resolves to the backend. api_key and agent_id come from spec config
(the tool never creates agents; one-time bootstrap is documented in the
module); errors are returned as strings and always carry the run id,
including timeout, failure, cancellation, and unknown-status paths.
Polling honors Retry-After on 429 and retries transient failures within
a bounded budget; run creation is never retried.
Includes unit, dispatch, and e2e tests (respx transport mocks and a
fake-clock seam; the e2e drives the full lifecycle against a local
Agent API stub).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* feat: add nimble_extract builtin backed by Nimble Extract Templates
Add a nimble_extract built-in tool that runs one of the account's Nimble
extract templates (POST /v2/extract/templates/run) and returns the
template's structured, parsed results as JSON in one synchronous call.
The template is named in spec config; the LLM supplies the template's
params (each template declares its own input schema, discoverable via
GET /v2/extract/templates/{name}).
This is the migration target for the deprecated one-call /v1/agent
site-scraping path: same structured-entities output contract, now on
the current Extract Templates API. The predecessor tool name is retired
rather than aliased - the registry does not reserve it, and a test
locks that in - so the old name can never silently point at a
different API.
Wiring mirrors nimble_research: registry factory plus runner-local
dispatch. api_key and template come from spec config; errors are
returned as strings with the template named and the server's task id
preserved for supportability (parsing failures, template-not-found,
params rejection, and server error bodies are each mapped to clear
messages); output is capped to keep the model context bounded.
Includes unit, dispatch, and e2e tests (respx transport mocks; the e2e
drives the flow against a local Extract Templates stub), with
captured-request assertions that every request carries the
X-Client-Source header.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* fix(nimble_research): harden malformed-config and envelope bounds
Catch httpx.InvalidURL when building the run URL so a control character
in the configured agent_id returns the builtin's own clean error string
instead of escaping its documented never-raises contract (agent_id is
interpolated into the run URL path).
Cap each API-supplied trust string - reasoning, source and citation url
and title, and the output type - so a single oversized value cannot
inflate the returned envelope past its intended bound, matching the
list-length caps already applied to sources, claims, and citations.
Adds tests: a control-char agent_id returns an error with no request
made, and an oversized trust.reasoning is capped with the envelope
still valid JSON.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* fix(nimble_research): complete the never-raises and envelope bounds
Follow-up to the previous hardening pass, which covered only part of each
surface.
Never-raises: httpx.InvalidURL is not a subclass of RequestError, so it
also had to be handled on the poll and result hops. The run id comes from
the API and is only prefix-validated, so a control character after the
prefix could raise out of the tool. Polling treats it as permanent and
returns immediately rather than spending its transient-retry budget on an
error that cannot become valid.
Envelope bounds: cap the remaining API-supplied strings that reached the
envelope uncapped - trust confidence, per-claim path and confidence - and
drop a non-string source or citation url instead of passing the raw value
through. Also cap API-supplied text reflected into error strings, which
could otherwise be arbitrarily long.
Adds regression tests for both hops, for every capped field, for the
dropped non-string url, and for an oversized server error message.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* fix(nimble_research): bound run status, run id, and the trust section
The result body's run status was accepted as any string and interpolated
raw into the failure message, so a malformed status could turn into a
multi-megabyte error string. Only a known terminal status is trusted now,
and the message caps the values it reflects.
Bound the accepted run id at creation instead of echoing an arbitrary one
through later messages, and cap the trust section as a whole: the
per-field caps still multiplied across sources, claims and citations.
Includes regression tests for each bound.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* feat(nimble_research): adopt nimble-python 1.2 typed run fields
Create runs through the released nimble-python 1.2.0 client instead of
hand-rolled requests, and expose the typed per-run fields it added.
agent_id is now optional and selects the create route: when it is omitted
the run is created with agents.run() so Nimble provisions the agent, and
when it is set the run is created with agents.runs.create() against that
agent. Both routes forward input_data, output_schema, sources, agent_name,
skill, and use_case as typed arguments, so no extra_body escape hatch is
needed. The client is built with max_retries=0, because creating a run is
billable and not idempotent and the API exposes no idempotency key.
effort stays an optional override, so leaving it unset lets the selected
agent or template default apply. low, medium, high, and x-high are
selectable per run. max is a coming-soon custom-budget tier: it stops with
a pointer to the Nimble product team, and only degrades to x-high when a
spec opts in explicitly. The degradation is reported on every outcome, so
a run that was downgraded and then failed still says so.
The agent id returned by creation addresses the rest of the lifecycle,
since on the generated route it is the only one that exists, and a run
that comes back owned by a different agent is rejected rather than
retargeted. Identifiers are checked against an allowlist before they are
interpolated into a request path. Status polling defaults to ten seconds.
Includes unit tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* fix(nimble_research): warn against resubmitting an unresolved create
A create that fails in transport or times out may still have been received,
and a 408 or 5xx reached Nimble before the failure was reported, so the run
can be live and billed while the call reports an error. A 202 carrying an
unusable body is the settled version of the same problem: the run exists,
but the response cannot address it.
All of these now say so and tell the caller not to resubmit, since a
resubmission pays for the task a second time. The guidance names the run id
when one survived, and points at the account's recent run history when none
did. A clear rejection still carries no such warning: 401, 403, 404 and 422
create nothing, and attaching the warning to them would only teach the
reader to skip it.
Includes unit tests for the ambiguous and settled paths, and for the
rejections that must stay silent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* fix(deps): upgrade GitPython to 3.1.55 to clear advisories
The lockfile pinned GitPython 3.1.50, which carries eight advisories whose
fixes land across 3.1.51, 3.1.53, 3.1.54 and 3.1.55. The dependency audit
only runs when the lockfile changes, so the pin was invisible until it was
touched, and then it failed the scan.
3.1.55 is the first release that clears all eight. It sits one day past the
P7D resolution window, so it needs a per-package exception alongside the
existing ones; the cutoff is set to land on 3.1.55 rather than the latest
release, keeping the change to the smallest version that resolves the
advisories.
GitPython is a transitive dependency, so this is a lockfile-only change and
no declared requirement moves. No other package version changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* fix: align Nimble 1.2 run controls with released contract
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* fix(nimble_research): never invite a resubmit of a billed run
Once run creation succeeds the run exists and has been billed, but only the
create path said so. The 409 branch told the caller to "retry the task to
fetch it" — on a run already observed complete — which reads as an instruction
to call the tool again and pay for a second run to read the first one's
result. Timeout, polling and result-fetch failures said nothing either.
Every post-create failure now ends with the same guidance the create path
gives, keyed to the run id: do not resubmit, reconcile the run that already
exists. A create-time 429 stays a clear rejection, since a rate limiter
refuses the request before a run is started; that classification is now
documented and covered.
Also drops the notice channel left behind when the effort downgrade was
removed. _resolve_effort returned None for it at every exit, so the value was
always None and the code that consumed it was unreachable; a resolved effort
is now simply what the caller asked for. The tool schema's sources object is
tightened to match what the tool already enforces, so a schema-conformant call
is not rejected at runtime.
Includes unit tests for each post-create path and the rejection that must stay
silent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
* feat(onboarding): advertise the nimble builtins to the agent builder
list_builtin_tools.py is the onboarding assistant's sole source of truth
for recommendable builtins; without these entries the assistant can
never surface nimble_extract or nimble_research when building an agent.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* refactor(deps): move nimble-python behind a `nimble` extra
nimble-python was a baseline dependency, so every install pulled a
partner SDK that only the nimble_research builtin uses (nimble_extract
talks raw httpx). Follow the hindsight-client pattern: the SDK moves to
an optional `nimble` extra, nimble_research imports it lazily inside
_start_run and reports which extra to install (checked before anything
is sent, so nothing is billed), and the onboarding catalog advertises
the tool only when the SDK is importable. The client stays in the dev
set so the credential-free suites keep driving the real SDK, and mypy
gets the same ignore_missing_imports override as the other lazy-import
extras.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(nimble): address Polly review findings
Blocking items, all verified before fixing:
- Guard use_case with isinstance before the frozenset membership test; a
list/dict argument raised TypeError (unhashable) out of invoke(),
breaking the never-raises contract. Now a clear tool error, unbilled.
- Catch APIError (e.g. APIResponseValidationError, which subclasses
APIError, not APIStatusError/APIConnectionError) in the create path
and route it through the unresolved-create guidance: a 2xx whose body
fails SDK validation means the run may exist and be billed, which is
exactly the case the do-not-resubmit warning exists for.
- Clamp each HTTP call's timeout to the remaining deadline via
_request_timeout, so a single create/poll/result request can no longer
overrun the tool's documented timeout_seconds budget.
Also apply the research module's error-string caps to nimble_extract
(message, task id, parsing detail, status), closing the one reflected
uncapped path Polly's non-blocking notes and the maintainer review both
flagged. Regression tests for all four.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: Kobi Kadosh <kobi.kadosh@gmail.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
The release cut, main bump, and nightly cut all regenerate uv.lock in
CI. The runner's uv now writes size fields on file entries, which the
repo's canonical lockfile form (scripts/normalize_uv_lock_registry.py,
enforced by the pre-commit hook) forbids — so the v0.8.0 release
commit went red on the branch-push lint run, and the next cut from
that branch would fail the green-CI gate. Run the normalizer after
uv lock (fixer exits non-zero when it rewrites, so tolerate that),
then hard-verify with --check so a genuinely broken lockfile still
fails the step.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Promoting an ACP-speaking vendor CLI to a first-class harness has meant
touching 6+ registration points (capabilities, valid set, module map,
aliases, labels, install spec, readiness, setup steps, a per-harness
spawn-env builder, the live e2e matrix exclusion) plus a near-identical
thin inner module. Recent PRs each re-derived this by hand and one shipped
without its spawn-env builder, silently dropping the session cwd and the
spec sandbox.
Add omnigent/acp_cli_harnesses.py: one AcpCliHarness row per vendor CLI
(label, binary, ACP argv, aliases, install and login metadata). Every
registration derives from the row:
- harness_plugins: validity, module routing (all rows run the shared
omnigent/inner/acp_harness.py wrap), aliases, labels, capabilities
(the generic acp profile), install specs and install keys
- onboarding: one-click install allowlist (npm rows) and vendor-login
setup steps derive; readiness rides the existing install-key gate
- runtime/workflow: one shared _build_acp_cli_spawn_env forwarding the
session cwd and serialized os_env, shell-quoting the resolved binary
- runner dispatch: one membership check covers every current and future
row
- tests: readiness spelling lists and the live-matrix exclusion extend
from the catalog; tests/test_acp_cli_harnesses.py drives a fake row
through the builder and dispatch and asserts full registration per
real row
The catalog ships empty; the first rows land with the Grok Build (#3075)
and Qoder (#3560) PRs, each reduced to one dict entry plus docs.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* feat(ci): nightly prerelease builds — tag-only dev-datestamp cuts from main
Adds nightly-release.yml: every night at 04:30 UTC it walks main to the
newest commit with completed green CI, stamps the lockstep version to
X.Y.Z.devYYYYMMDD, commits the stamp detached on top of that base, and
pushes only the tag via the omnigent-ci App token. Quiet nights (no new
commits since the last nightly tag) and same-day reruns no-op.
Deliberately not the release.yml flow: no release branch, no main bump,
no benchmark gate. Downstream is already dev-quiet: no GitHub release,
no notes, no changelog, no homebrew; images publish the immutable
version tag; update-check ignores dev releases and omni upgrade --pre
opts in. The datestamp is fixed-width because PEP 440 compares the dev
segment as one integer — a wider stamp would sort above every narrower
one forever.
PyPI publishing follows separately via the secure release repo's
scheduled lane.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* feat(ci): nightly consumer update script; drop the stale PyPI hand-off note
scripts/update_nightly.sh resolves the newest vX.Y.Z.devYYYYMMDD tag
(version sorts before date, so the first nightly after a main version
bump outranks all older ones; the 8-digit date requirement screens out
legacy .dev0-style tags) and installs it with uv, pinning the lockstep
trio to that one tagged commit. Idempotent, so it is cron-safe: it
exits fast when the newest nightly is already installed instead of
redoing the web-UI build.
The workflow header no longer claims the secure release repo publishes
nightly tags to PyPI: that lane was dropped, nightlies are consumed
straight from the tag.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* feat(cli): omni upgrade --nightly moves onto the newest nightly tag
Nightlies are vX.Y.Z.devYYYYMMDD git tags that never reach the package
index, so the flag answers 'is there something newer' from the repo's
tags (git ls-remote + PEP 440 max, so the first nightly after a main
version bump outranks all older ones) and reinstalls with a git spec
pinned to that tag, per installer (uv/pipx/pip/poetry). It dispatches
before the VCS-vs-registry split: a registry install hops onto the
channel, and a VCS install pinned to an older nightly moves tags
instead of re-pulling its pinned ref. Same drain/stop, --check, and
probe-the-disk verification contracts as the release path.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
## Related issue
N/A
## Summary
- After the switch to corepack/pnpm, `pip install .` / `uv sync` could hang
indefinitely for users who have a corepack `pnpm` shim on PATH but have
never downloaded pnpm. Corepack prints `! Corepack is about to download
.../pnpm-11.15.1.tgz` and then blocks on `? Do you want to continue? [Y/n]`.
Build backends capture output, so the prompt is invisible and the install
just sits there until the 600s timeout.
- The trigger is the shim, not the `corepack pnpm` fallback: corepack's
`dist/pnpm.js` does `COREPACK_ENABLE_DOWNLOAD_PROMPT ??= '1'` while explicit
`dist/corepack.js` uses `'0'`. `shutil.which("pnpm")` finds the shim, so the
prompting path is the one that looked fine. CI is unaffected because corepack
skips the prompt when `$CI` is set.
- Run both pnpm commands in `setup.py` with
`COREPACK_ENABLE_DOWNLOAD_PROMPT=0` (download without asking) and
`stdin=DEVNULL` so nothing else in the toolchain can block on input we can
never deliver. Applied the same fix to `tests/e2e_ui/conftest.py`, which had
the identical latent hang under captured pytest output.
## Test Plan
Reproduced the hang and verified the fix against the pinned `pnpm@11.15.1`,
handing the child a real TTY via `pty.openpty()` and an empty `COREPACK_HOME`:
```
BEFORE (shim default prompt=1, TTY stdin): HUNG (timeout)
err='! Corepack is about to download .../pnpm-11.15.1.tgz\n? Do yo'
AFTER (prompt=0 + stdin=DEVNULL): proceeds straight to download
```
End-to-end check of the install path:
```bash
rm -rf ~/.cache/node/corepack "$COREPACK_HOME"
corepack enable # pnpm shim on PATH, pnpm not yet fetched
rm -rf omnigent/server/static/web-ui
pip install . # previously stalled with no output
```
`ruff check` / `ruff format --check` clean on both files.
## Demo
N/A
## Type of change
- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Verified manually: the failure only reproduces with a corepack `pnpm` shim, an
unpopulated `COREPACK_HOME`, and a TTY on stdin, so an automated test would
have to stand up a pty plus a registry fetch inside the build backend. Covered
instead by the pty-based before/after check in the Test Plan.
## Changelog
`pip install` / `uv sync` no longer hangs when pnpm is provided by a corepack
shim that has not downloaded pnpm yet.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
When omnigent-slack joined the lockstep, the cut job's hand-kept git
add list kept staging only the original five paths, so the release
commit shipped integrations/slack/pyproject.toml unstamped. At the
v0.7.0 tag the tree pins omnigent-slack==0.7.0 while the in-tree
package still says 0.7.0.dev0: uv sync --locked fails at the tag, a
source install with the slack extra cannot resolve, and lint went red
on both release/v0.7.0 pushes without blocking the tag. Stage with
git add -A like bump-version.yml so the staged set tracks whatever
update_versions.py stamps.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* Add WebSocket load test (dev/loadtest/) + run-load-test skill
Adds a Locust load test that opens N concurrent WebSocket connections to
WS /v1/sessions/updates and holds them open, measuring the server's
WebSocket fan-out (handshake, origin/auth gating, watch-set diffing,
heartbeat) under concurrency — no runner, LLM, or agent turns.
- dev/loadtest/ws_load_test.py: the locustfile (SessionUpdatesUser).
- dev/loadtest/run.py: runner taking server + host + load params, runs
locust headless, and writes a result set (summary.md, CSV, HTML, config).
- loadtest extra (locust + websocket-client) in pyproject.toml + uv.lock.
- .claude/skills/run-load-test: skill that gathers inputs, runs, and
explains the latency results.
Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>
* Launch locust via sys.executable -m locust in the load-test runner
run.py launched locust as a bare `locust` command, which resolves through
PATH and can pick up a stale/broken locust from a different Python (e.g. a
~/.local 3.10 install missing gevent's zope.event) even when run.py itself
runs under a venv — crashing the run with ModuleNotFoundError before locust
starts. Launch it as `sys.executable -m locust` so it always uses the same
interpreter + site-packages that run.py runs under. Preflight now checks
importlib.util.find_spec (the actual interpreter) instead of shutil.which
(PATH), and --web execs sys.executable too.
Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>
* Genericize --mount-prefix docs to reverse-proxy sub-paths
Replace deployment-specific mount-prefix details with a provider-neutral
"behind a reverse proxy at a sub-path" framing (neutral /omnigent example)
across the README, the run-load-test skill, and the run.py / ws_load_test.py
help + docstrings. The --mount-prefix flag itself is unchanged.
Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>
* Add runner-level turn load test (real multi-turn conversations, mocked LLM)
turn_load.py drives real agent turns through the runner — the full
POST .../events → server → runner → executor → LLM → stream → idle loop —
under concurrency, with the LLM mocked (zero latency) so the numbers isolate
Omnigent's own per-turn / history-handling overhead. Runs N concurrent
conversations of M sequential turns each on one durable session, so history
grows across the turns (a real long conversation, not N one-shots).
It boots the whole stack itself (server + zero-latency mock LLM + runner) by
reusing the benchmark harness's BenchEnvironment, using the in-process
openai-agents harness — no vendor CLI, no real API key — so it runs from a repo
checkout with no server to point at. Concurrency is asyncio (the runner stack
is async), not Locust. Writes the same summary.md / run_config.json result
format as the WS runner.
Documents both scenarios (WebSocket fan-out vs runner turns) in the README and
the run-load-test skill.
Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>
* Address review: fix socket leak, URL/timeout edge cases, double-count; add tests
Copilot review follow-ups on the load-test harness:
- ws_load_test: assign self.ws before the send/recv steps so a post-create
failure closes the socket instead of leaking it.
- ws_load_test: _ws_url now treats a schemeless host (localhost:8000, which
Locust accepts) as ws:// rather than emitting an invalid URL.
- ws_load_test: _read_until_snapshot caps each recv to the remaining deadline
so a late frame can't overrun by a full read timeout.
- ws_load_test: WS_READ_TIMEOUT falls back to the default on a non-numeric
value instead of raising in on_start.
- run.py: preflight websocket-client as well as locust; rename _fmt_ms ->
_fmt_num (it also formats Requests/s).
- run.py: _write_summary skips locust's Aggregated row when totaling, which was
double-counting the headline request/failure counts.
- docs: the scenario reads AUTH_TOKEN from the environment; drop the wrong
`-e AUTH_TOKEN` locust-flag examples (AUTH_TOKEN=... locust ...).
- tests: add tests/loadtest unit tests for the pure helpers (URL/env/argv
wiring, summary formatting, timeout parsing) — deterministic, no server boot.
Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>
* Redesign as one load test: each user is a real host driving real turns
Collapse the two scenarios (ws_load_test.py + turn_load.py) into a single
load test where each Locust user IS a real omnigent host. Each user spawns a
real `omnigent host` subprocess (unique identity + per-host $HOME so the
host-daemon singleton guard doesn't collide), registers it over the host
tunnel, then creates host-bound sessions and drives real multi-turn
conversations — every turn is a genuine post→idle loop through a runner the
host spawns, with the LLM mocked (zero latency). `-u N` scales the number of
hosts; Locust does the concurrency.
run.py boots the whole stack (server + mock LLM via BenchEnvironment),
registers one agent, sets the mock reply, then runs Locust against it — there
is no --server to pass, since mocking the LLM requires a stack we control. It
reuses the CSV→summary.md machinery (Aggregated-row dedupe kept).
Capacity-limited by design: N hosts × M sessions = N×M real runner processes on
the load box, so it drives genuine end-to-end turns rather than faking the
runner, but does not scale to hundreds on one machine (documented). Removes the
websocket-client dep (no longer used); needs [loadtest,dev,agents-sdk]. README,
skill, and tests updated for the single scenario.
Verified locally: 5 hosts × 3 turns → 133 turns, 0 failures.
Co-authored-by: Isaac
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>
---------
Signed-off-by: Shivam Mittal <shivam.mittal@databricks.com>
Co-authored-by: Shivam Mittal <shivam.mittal@databricks.com>
PR #3420 validated the OpenClaw Gateway ACP path end-to-end against a live
Gateway, but docs/openclaw.md still read as if streaming/final replies were
only protocol-matched and the integration provisional. Update the
compatibility section to state what live validation confirmed — streaming
assistant replies, native tool execution, ACP permission routing, and session
resume — and reframe the remaining Control-UI-sync gap as a known limitation
rather than an open question. Keep the note that CI cannot run OpenClaw.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(ap-web): harden math rendering
Load KaTeX runtime styles in every web entrypoint and normalize common TeX delimiters so streamed formulas, radicals, and display math render reliably across chat surfaces.
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: buyicoder <169354621+buyicoder@users.noreply.github.com>
* fix(ap-web): make math delimiter normalization region-aware
Address Polly review notes on the math-rendering hardening:
- Skip normalization inside existing $…$/$$…$$ spans and treat a
literal backslash-backslash as a verbatim escape, so a LaTeX line break
like \\[1em] inside an aligned display block is no longer mistaken for
a \[ opener and corrupted.
- Track backtick-run length so \(/\[ inside a multi-backtick inline-code
span is left verbatim.
- Correct the stale FILE_PATH_AWARE_COMPONENTS comment now that the memo
comparator is gone and MessageResponse shallow-compares props.
Co-authored-by: Isaac
* fix(ap-web): guard currency dollars and indented fences in math normalizer
Follow-up on Polly review notes:
- A single $ immediately before a digit reads as currency ($5), so it is
escaped and does not flip the math-span toggle. Prevents prose like
"it costs $5 or $10" from parsing as inline math now that
single-dollar math is enabled globally. An escaped \$ is copied verbatim.
- Fence detection now allows CommonMark's 0-3 leading spaces and matches the
full fence run, so an indented ```-fenced block containing \(...\) is not
normalized (and a 4-backtick run no longer leaks into inline-code tracking).
Co-authored-by: Isaac
* fix(ap-web): use String.match for fence detection to clear exfil scan
The security Exfil scan flags RegExp.prototype.exec() because its text-only
regex matches the substring 'exec(', which is meant to catch Python dynamic
code execution (exec/eval/__import__). This is a pure in-memory regex match
against local string data, so switch to the equivalent String.match(), which
returns the same match array for a non-global regex and avoids the token.
Co-authored-by: Isaac
* fix(ap-web): address Copilot review on math normalizer and styles
- Track the opening fence marker so a fenced code block closes only on a
matching fence char with a run at least as long (CommonMark). A stray
`~~~` line inside a ```-fenced block no longer flips the fence off and
lets math normalization run inside code.
- Drop the no-op `overflow-y: visible` on `.katex-display`; with a
non-visible overflow-x the browser computes overflow-y as auto anyway, so
it only risked stray vertical scrollbars.
- Resolve the entrypoint-style guard test's paths from import.meta.url
instead of process.cwd() so it doesn't depend on the runner's directory.
Co-authored-by: Isaac
---------
Signed-off-by: buyicoder <169354621+buyicoder@users.noreply.github.com>
Co-authored-by: zhanyongjie <zhanyongjie@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Serena Ruan <serena.rxy@gmail.com>
* fix(pi): surface credential resolution error when gateway provider's env var is unset
When a `kind: gateway` provider is configured as the pi harness default
via `default: pi` and its `api_key_ref: env:VAR` cannot resolve (because
VAR is not exported in the runner's environment), `_optional_provider_family`
previously caught the OmnigentError from `resolve_secret` and returned None
silently. The outer `_apply_provider_to_pi` then raised a generic
"no family whose credentials resolve — set the api_key env var for its
'anthropic' or 'openai' family" message with no mention of which specific
variable to export, making the error hard to act on.
Change `_optional_provider_family` to return the captured error alongside
None (as a tuple), and surface that error in the "no family resolves"
message so the user sees exactly which env var (e.g. `$MY_TOKEN` from
`api_key_ref: env:MY_TOKEN`) needs to be set.
The design intent of the silent catch is preserved: a family whose key is
unset is still treated as absent so pi can fall back to the other family
when only one key is exported. The only change is that the fallback-failure
error now carries the root cause.
Closes#3788
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* address review: fix return type annotation, correct keychain docstring, remove issue refs from tests
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): forward provider api_key_ref env vars into runner subprocess
_build_runner_env filters the host environment before spawning the runner
subprocess, passing only an allowlist of known credential vars
(ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.). A user who configures a gateway
provider with a custom env var via api_key_ref: env:MY_TOKEN would find that
MY_TOKEN is present in their shell and daemon process but stripped before
reaching the runner — resolve_secret then fails, _optional_provider_family
returns None for the family, and _apply_provider_to_pi raises the no-family-
resolves error.
Add provider_credential_env_vars(config) to provider_config.py, which scans
all inline-family providers for api_key_ref: env:VAR and api_key: $VAR
references and returns the set of env var names (plus OMNIGENT_-prefixed
aliases). Wire this into _build_runner_env so those vars are automatically
forwarded alongside the standard HARNESS_CREDENTIAL_ENV_VARS, without
requiring users to list them in OMNIGENT_RUNNER_ENV_PASSTHROUGH by hand.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): add authHeader to generic openai provider entries in models.json
Generic (non-Databricks) OpenAI-compatible gateways expect
Authorization: Bearer <token>. The 'databricks' and 'databricks-completions'
provider entries in the generated models.json were missing authHeader: True
on the generic provider path, so Pi used the Databricks-native auth scheme
instead — causing a 401 Missing Authentication header from the gateway.
Add authHeader: True to both entries when is_generic_provider is true,
matching the pattern already used by databricks-openai, databricks-anthropic,
and databricks-mlflow.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi-native): qualify namespaced model ids in --model arg to prevent builtin routing
When a gateway provider's model id contains a slash (e.g. an OpenRouter
namespaced id like 'moonshotai/kimi-k2.5'), Pi's arg parser treats
'provider/model' in --model as a provider override, routing to the builtin
'moonshotai' provider instead of our custom 'omnigent' provider. The builtin
has no API key, producing 'No API key for provider: openai-codex'.
Pass the fully-qualified 'provider/model' form (e.g.
'omnigent/moonshotai/kimi-k2.5') when the model id contains a slash, so
Pi's findExactModelReferenceMatch matches the canonical form under our
provider first.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(worktree-guard): switch to posixpath for path normalization to ensure consistent behavior across platforms
* Windows-only escape in worktree_guard, the sole write confinement for unsandboxed workers: it reasoned in POSIX but normalized with os.path, which is ntpath on Windows and rewrites / to \ — so startswith("/") never fired and /etc/passwd returned ALLOW.
Fixed by normalizing with posixpath explicitly, plus a drive-letter reject for C:/Windows/x, which posixpath reads as an ordinary relative dir named C:.
Two follow-ups from Copilot: the drive check ran on the raw path, so ./C:/… (and a/../C:/…) normalized past it — moved it after normalization; and isalpha() narrowed to ASCII, since Windows drives are [A-Za-z] and the Unicode form over-rejected.
109 passed on Windows, where four of those cases fail on main. Audited environment_filesystem.py:190 in the same pass — it pairs normpath with os.path.isabs, which holds on both platforms, so it needs no change.
* feat(web): move Chat/Terminal switcher into the header
Terminal-first sessions previously toggled between chat and terminal via
an in-page pill above the composer. Replace it with a MessagesSquare +
chevron icon button in the ChatHeader (next to the agent-info icon) that
opens a Chat/Terminal dropdown, freeing the composer area and keeping the
switcher with the other session controls.
The new ViewModeToggle reads the same TerminalFirstContext the pill did,
so behavior is unchanged: it self-gates for non-terminal-first sessions,
the iOS shell (native Liquid Glass bar), and rail-opened shell views, and
disables the Terminal option (with a spinner while starting up) until a
PTY is reachable. A tooltip names the current view. Removes the pill, its
dead CSS, and the now-redundant iOS keyboard guard.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): update e2e locators + a11y for header view toggle
The render-parity e2e helpers located the old in-page pill via
`role="group" name="View mode"` and clicked its inner Chat/Terminal
buttons. The header switcher is a dropdown, so point them at the
`view-mode-toggle` trigger and click the Chat/Terminal menuitemradio.
Also address review feedback on ViewModeToggle: import the shared
`TerminalFirstView` type instead of a duplicated union in the setView
cast, and only suppress dropdown close-refocus for pointer closes so
keyboard/AT users keep their place (mouse closes still avoid the stuck
ghost-button focus ring).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(codex-native): tear down app-server when TUI pane is reaped or exits
Each codex-native session (Polly dispatches every codex sub-agent this
way) runs two codex processes on the runner: the codex app-server backend
and the codex --remote TUI pane. Only DELETE /v1/sessions ran the full
cleanup that cancels the forwarder and closes the app-server. Two other
ways the TUI pane goes away left the app-server orphaned for the runner's
lifetime:
- the idle pane reaper closes the tmux pane after the idle window but
never touched _AUTO_CODEX_APP_SERVERS, and
- an unexpected TUI exit (crash / OOM / host recycle) evicted the pane
without cancelling the forwarder.
On a long-lived multi-session runner, every idle or crashed codex
sub-agent leaked a codex app-server process — the pile-up reported in
omnigents-qa.
Add teardown_codex_native_app_server(session_id): cancel the session's
forwarder (whose finally closes the app-server) and close any leftover
registered server. It's a no-op for a session with no registered codex
app-server, so it's safe to call from the shared pane-teardown paths for
every harness. Wire it into the reaper's reap and the terminal-exit
publisher.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(codex-native): reap codex app-server even if pane close raises
Move the codex app-server teardown in the idle-pane reaper into the
finally block. close_terminal() can propagate (TerminalInstance.close()
raises anything but TimeoutError), and in that partial-failure mode the
teardown line in the try body was skipped — leaving the exact orphaned
app-server this fix targets. The helper is idempotent and suppresses its
own errors, so running it in finally never masks the original exception.
Addresses Copilot review on #3925.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(codex-native): close app-servers on host/runner stop + boot reconcile
Host-spawned codex app-servers are spawned start_new_session=True, so
they survive their runner's death. Two gaps left them orphaned:
- On a graceful host/runner stop the host SIGTERMs the runner without a
per-session DELETE /v1/sessions, so per-session teardown never fired and
_stop_pm never closed _AUTO_CODEX_APP_SERVERS — every host-spawned codex
app-server leaked even on a clean stop. (The TUI panes were already
closed by the terminal registry's shutdown; only the app-server half
leaked.)
- On a hard death (SIGKILL / OOM / crash) nothing runs at all, and the
crash-safe registry was only reconciled when a NEW codex session
started — so orphans lingered until the next codex launch, if ever.
Add teardown_all_codex_native_app_servers() and call it from _stop_pm so a
graceful stop takes the app-servers down with the runner. Add a boot-time
reconcile_codex_native_process_registry() in _start_pm so a fresh runner
reaps orphans a dead predecessor left (owner-lock held => live sibling,
skipped). Reconcile runs in a thread since it does blocking file/PID work.
The --remote TUI self-exits when its app-server dies (observed: every
orphan seen in the field was an app-server, zero orphaned TUIs), and the
graceful path already closes TUI panes, so no tmux-name plumbing is added.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(cli): add `omnigent diagnose` environment snapshot
Add a read-only `omnigent diagnose` command that prints a small, secret-free
environment snapshot for bug reports: CLI version, OS/Python, and the server's
auth mode. With `--server <url>` (or a resolvable configured/local server) it
reads the server version and real auth mode from the unauthed `GET /v1/info`
endpoint, so version skew between CLI and server is visible — the same reason
the session-info popover shows `server · host`.
The auth mode doubles as the OSS-vs-managed signal: accounts | single_user |
oidc | header, derived from `/v1/info` when the server is reachable and falling
back to the local environment otherwise (tagged by `auth_source_origin` so the
two are never confused). The snapshot carries no secrets — only versions, OS,
and the coarse auth mode.
`omnigent doctor` (install-ledger migration) is left untouched.
Co-authored-by: Isaac
* fix(cli): address diagnose review — redact server_url, e2e test, help caution
Review follow-ups on the `omnigent diagnose` PR:
- Redact userinfo and query/fragment from the reported `server_url` so a
`--server https://user:pass@host` value can't leak credentials into the
snapshot (the "safe to paste into an issue" invariant).
- Add CLI-level tests (CliRunner + respx over /v1/info) exercising the command
wiring and output format end-to-end, alongside the existing unit tests.
- Note in `--help` that `--server` should point only at a trusted server, since
reaching a managed server may attach stored/ambient credentials to the request
(same behavior as `session export` / `run --server`).
Auth is intentionally still attached to the /v1/info probe: a managed server
sits behind an auth proxy that 401s an unauthenticated request, so dropping it
would break the OSS-vs-managed signal for exactly the managed case. Attaching
credentials to the request does not put secrets in the output, which is what the
"secret-free" guarantee covers.
Co-authored-by: Isaac
* fix(cli): harden diagnose URL redaction + register in subcommand allowlist
- _redact_url: fix two leaks the review found. Scheme-less inputs with userinfo
(`user:pass@host:6767`) were returned unchanged because urlsplit reads the
`user:` as a scheme — now scrubbed. IPv6 literals lost their required `[...]`
brackets when netloc was rebuilt from hostname/port — now the userinfo is
dropped off the authority in place, preserving brackets and host casing.
- Add `diagnose` to `_CLICK_SUBCOMMANDS` so `omnigent diagnose` is reachable
from main() (a registered command missing from the allowlist is rejected as
removed ad-hoc chat). Fixes test_click_subcommands_allowlist_covers_registered_commands.
Co-authored-by: Isaac
* fix(cli): make diagnose URL redaction leak-proof on malformed/scheme-less input
Follow-up on review: _redact_url used urlsplit, which raises ValueError on a
malformed IPv6 URL (the fallback then returned the raw string, leaking any
user:pass@) and left query/fragment intact on scheme-less inputs. Rewrote it as
pure string surgery — cut at the first ?/#, then drop a user:pass@ prefix from
the authority — so credentials and tokens are stripped uniformly regardless of
URL shape, with no parser that can raise. IPv6 brackets and host casing are
preserved.
Co-authored-by: Isaac
The Projects group-header kebab (⋯) rendered next to "New project" even
when its menu had no items to show. With no projects filed, neither the
expand/collapse controls (need projectNames.length > 0) nor "Select
sessions" (needs project sessions) apply, so the menu opened empty.
Gate the kebab on whether either item is available, leaving only the
"New project" button when there's nothing to offer.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
The "My sessions" / "Shared with me" tabs were hidden whenever bulk
selection mode was active, stranding the viewer on whichever scope they
happened to be on. Keep the tabs visible during selection so the scope
stays switchable.
Selection is a single global set while the tabs show disjoint,
ownership-scoped slices, so changing the visible tab now exits selection
mode — otherwise the bulk-action bar would show a stale count carried
over from the other tab. This is centralized in a `switchTab` helper used
by both the tabs' onValueChange and the "New session" snap-back (which
sets the tab outside Radix's onValueChange path), so no tab change can
skip the selection cleanup.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* Add product-analytics abstraction to web frontend
Introduce an opt-in, host-injected analytics seam so an embedding host can
collect user actions (clicks, field value-changes, page views) keyed by a
stable componentId. Fully inert standalone: when no host sink is configured
via OmnigentHostConfig.analytics, every emit is a no-op.
- lib/host.ts: OmnigentAnalyticsEvent type + analytics? sink + getter.
- lib/analytics.ts: emitOmnigentAnalytics, useOmnigentAnalytics
(trackClick/trackValueChange, values redacted by default for PII), and
useOmnigentPageView (re-fires on pathname change, like the unified router).
- Button/Input: optional componentId prop that reports clicks/value-changes.
- lib/routing.tsx: optional componentId on Link (OmnigentLinkProps) so a
link can opt into per-link analytics; standalone strips it.
- App.tsx: central <PageView id> wrapper declares each route's page-view id
next to the route table; SettingsPage keeps its own hook (param-derived
settings.<section> id) as the escape hatch.
- Example componentIds: chat composer send, tasks search, sidebar
conversation switcher, settings "Back to Omnigent" link.
Distinct from lib/telemetry.ts (low-level OTEL HTTP tracing); this is
application-level user-action analytics.
Co-authored-by: Isaac
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
* Ci
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
---------
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
* feat(sessions): auto-connect a wakeable runner on shell create
Creating a shell from the web UI on a session whose runner had gone to
sleep dead-ended on a 502 ("no runner available"), even though the host
was still up and the next chat message would have transparently woken it.
Add `ensure_runner_connected`, which runs the same runner-acquisition
ladder `post_event` uses (wake a stale resumable managed sandbox, launch
a runner on a live host, or relaunch a managed sandbox) without the
message-specific side effects, and call it from `create_session_terminal`
before proxying. Wakeable states reconnect and the shell opens; a
non-host-bound stranded session or an offline external host still 502s
(the CLI reconnect path owns those).
Surface connect state on the "+" → Shell menu item: it stays enabled and
shows "Reconnecting…" with a spinner while the server wakes the runner on
a wakeable session, and is disabled + labeled "Offline" for states the
browser can't reconnect. Widen the menu so the longer label isn't clipped.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(sessions): wait the connect grace before relaunching on shell create
Address PR review: ensure_runner_connected went straight to
_launch_runner_on_host whenever no runner client resolved, so opening a
shell against a session whose runner was merely booting (tunnel not yet
registered) would spawn a second runner and orphan the booting one —
diverging from post_event, which it claims to mirror.
When the session has a pinned runner_id and a live host, first wait
_HOST_BOUND_RUNNER_CONNECT_GRACE_S for it to connect (racing a
host.runner_status query that cuts the wait short if the host reports it
gone), and only relaunch if it's truly dead. Also drop the unused
tuple binding at the call site (the proxy re-resolves the client).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(runner): allow managed mint in InitialAuthTokenFactory fallback
When a managed sandbox runner starts with a host-provided bearer
(_InitialAuthTokenFactory), and that bearer is rejected (401), the
fallback resolver was called with _allow_delegated_mint=False. This
blocked the managed-mint path entirely, leaving the runner with no
credential for its HTTP callbacks.
For managed runners (OMNIGENT_RUNNER_DELEGATED_AUTH=1 + binding token),
the fallback must be able to reach the managed-mint path after the
initial bearer expires — the same path used by runners that start
without a host bearer. Removing _allow_delegated_mint=False restores
this: SDK/OIDC auth still wins when present; managed mint is the
natural last resort for sandbox runners with no user credential.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): pass proxy bearer through managed mint so Apps proxy lets it through
The managed-mint endpoint (POST /v1/runners/{id}/token) is authenticated
by the runner's binding token, but on Databricks Apps deployments the
proxy layer sits in front and requires a valid Authorization header on
every request. With no bearer, the proxy returns 401 before the request
reaches Omnigent — the same symptom as the _allow_delegated_mint=False
regression, but a separate root cause.
Fix: thread an optional proxy_bearer through _make_managed_mint_factory,
_ManagedMintTokenFactory, and _mint_managed_owner_token, passed to
databricks_request_headers as the Authorization header. The initial
host bearer seeds it; after the first successful mint the minted JWT
replaces it as the proxy bearer for subsequent refreshes.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): reuse runner auth factory in codex discover-and-forward
_codex_discover_thread_and_forward was calling _make_auth_token_factory()
fresh, but RUNNER_INITIAL_AUTH_TOKEN is already popped from env by
runner startup — so the fresh call went straight to managed mint with no
proxy bearer, getting 401 from the Apps proxy before reaching Omnigent.
Fix: accept auth_token_factory at the call site, extracted from the
server_client's _RunnerDatabricksAuth (which already carries the correct
proxy bearer). supervise_forwarder also reuses it.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): store auth factory as singleton so all call sites share proxy bearer
Every _make_auth_token_factory() call after runner startup (harness setup,
terminal creation, forwarders) was building a fresh factory with no proxy
bearer, because RUNNER_INITIAL_AUTH_TOKEN had already been popped from env.
Each fresh factory hit the delegated-mint path, got 401 from the Apps proxy,
and left that call site with no credential.
Fix: store the factory built by serve_runner in a module-level singleton
(_runner_auth_factory). Subsequent _make_auth_token_factory() calls with
default args return it directly, so all call sites across orchestration.py
and app.py share the proxy bearer without any individual patching.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): fix __main__ vs omnigent.runner._entry module identity
When the runner runs as python -m omnigent runner, _entry.py executes
as __main__, creating a module object separate from omnigent.runner._entry.
Two bugs:
1. _runner_auth_factory was set on __main__ but read from
omnigent.runner._entry (always None). Fix: set it on the canonical
module via import omnigent.runner._entry as _self_module.
2. isinstance(server_client.auth, _RunnerDatabricksAuth) was False
because server_client.auth is __main__._RunnerDatabricksAuth while
the check used omnigent.runner._entry._RunnerDatabricksAuth. Fix:
use getattr(server_client.auth, _factory, None) instead.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor(runner): drop auth_token_factory param from codex discover-and-forward
Now that _make_auth_token_factory() returns the runner singleton (which
carries the proxy bearer), the explicit param and the server_client auth
introspection that fed it are no longer needed.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor(runner): introduce _set_runner_auth_factory to set singleton
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: use sys.modules to set singleton, restore docstring, remove dup comment
- Replace self-import with sys.modules lookup to avoid the module
importing itself (also sets on __main__ as a fallback).
- Move singleton early-return to after the docstring so __doc__ is
preserved on _make_auth_token_factory.
- Remove duplicated comment block in _codex_discover_thread_and_forward.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: import canonical module before setting singleton to ensure sys.modules registration
sys.modules.get() returns None when running as __main__ because the
canonical name isn't registered yet. Importing it first forces
registration, then both the canonical module and __main__ get the
singleton set.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: shorten overlong docstring in test
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: reuse singleton when server_url matches runner URL
Callers like native_policy_hook.py pass server_url explicitly but still
want the shared factory. The singleton guard now matches on both None
and the runner's own RUNNER_SERVER_URL.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
rendered.count("\n") undercounts when Rich wraps a long status label
(e.g. "✓ Isaac-Databricks-Ai-Gateway") across multiple terminal rows.
The cursor-up escape then doesn't move far enough, leaving stale menu
frames in the scrollback — which makes the "Configure harnesses" block
appear to stack on every loop iteration.
Replace the newline count with _count_terminal_lines(), which strips ANSI
escapes and uses ceiling division of each line's cell width by the terminal
width to count actual visual rows.
Tests cover no-wrap, wrapping, exactly-full-width, ANSI stripping, and the
empty-string edge case.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The claude-native session's Working/idle badge is driven by diffing the
tmux pane (the PTY watcher in resource_registry). That heuristic can't
tell "blocked on a prompt" from "working", and only flips to idle after
~1s of pane quiescence rather than on the real turn edge.
Claude Code writes a per-process status file at
`<config_dir>/sessions/<pid>.json` (its internal "concurrentSessions"
registry, present since v2.1.139) whose `status` flips idle/busy/waiting
on the actual turn edges. Prefer that for the claude-native running/idle
status, falling back to the PTY watcher when the file is absent (old
Claude, missing config dir) or never resolves.
- New `omnigent/claude_native_status_file.py`: `resolve_status_file`
(pid-first via the tmux pane pid, which equals Claude's pid on this
launch path; sessionId cross-check + freshness-bounded scan fallback),
`read_session_status` (busy/waiting -> running, idle -> idle), and a
`SessionStatusPoller` that lazily resolves then mtime-polls the cached
path and emits deduped status edges, deactivating when the file
vanishes on clean exit.
- terminal.py: add `pane_pid_sync()` and an `on_tick` hook so the poller
runs on the existing watcher cadence — no second thread.
- resource_registry.py: for the claude-native role only, build the poller
and drive it via `on_tick`; while it is active the PTY on_activity/
on_idle edges defer status to the file. The PTY watcher keeps owning
the activity badge and exit detection, and reclaims status if the file
never resolves or disappears.
waiting maps to running for now (no new status vocabulary); surfacing a
distinct "needs input" state is a possible fast-follow.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
Update the Discord-watch roster (rotation_roster.json), leaving 9 people in the rotation. Prune elapsed dates from the schedule and extend the
horizon through 2026-10-30 so every upcoming weekday is assigned to a
current roster member.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): tune sidebar vertical spacing rhythm
Refine the sidebar's padding and gaps so the primary nav reads as a
proper section and the row lists sit on a consistent rhythm:
- Primary nav (New session / Automations / Inbox): 8px gap to the
Omnigent header (pt-2), no bottom padding of its own (pb-0); the 16px
gap below now comes from the scrolling list (pt-4), matching the
section-to-section gap-4 rhythm.
- Nav rows and session rows are 32px tall (h-8) with 4px vertical
padding (py-1).
- Section headers (Pinned / Projects / Sessions) get 8px bottom
padding (pb-2).
- Session rows and project folder rows stack flush (gap-0).
- Bulk-action bar uses uniform 6px padding (p-1.5).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): regenerate visual baselines
* test(web): update sidebar spacing assertions to new rhythm
Bring the existing layout assertions in line with the tuned spacing:
primary nav pt-2/pb-0, nav + session rows h-8, iconless section header
pb-2.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): expect 32px session row height after spacing bump
Session rows moved from h-7 (28px) to h-8 (32px) in the sidebar
spacing tune; update the row-layout e2e assertion to match.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
When "Select sessions" is toggled on, the currently-viewed session's row
kept its active background even though it wasn't explicitly selected,
making the selection state ambiguous. Gate the active-route highlight on
`!selectionMode` so a row shows a background only when it's the active
session (normal mode) or explicitly checked (selection mode).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): file new-in-project sessions under their project immediately
Stamp the omni_project label at session create so a session created from
the new-session composer is born filed under its project, instead of
flashing under the ungrouped "Sessions" section for a couple of seconds
until the follow-up project_id move catches up in the search-indexed
session list. The sidebar dual-reads project membership from the label
or the first-class project_id, so the row groups under its project from
its first appearance; the existing move then promotes it to project_id.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(e2e_ui): cover born-filed new-session-in-project flow
Add a Playwright e2e that lands on the /?project=<name> composer and asserts
the create POST /v1/sessions carries the omni_project label, so a session
created inside a project is filed under it immediately (satisfies the
E2E UI Required coverage gate for this web behavior change).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* docs(web): correct the born-filed move-failure catch comment
If the project_id move fails, the session stays filed via its create-time
omni_project label (not unfiled) — fix the stale catch comment.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* perf(web): make add-to-project instant — optimistic move + slim PATCH
Moving a session into a project waited on resolve→PATCH→refetch, with
the PATCH shipping a ~415KB items snapshot, so the row sat in its old
section for seconds. Overlay the membership optimistically from the
cached project id, render folder bodies as the union of their own pages
and the loaded window (so the row lands in-folder in one frame), and
return the PATCH snapshot without items (~1KB).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* chore(server): regenerate openapi.json for the PATCH sessions docstring
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): keep folder-only rows visible through an optimistic move
A row loaded only via an expanded folder's own pagination has no copy in
the flat window for the folder union to re-home, so dropping it from its
source folder blanked it from the sidebar until the refetches landed.
Insert such rows into the target folder's cached page and skip the
removal when nothing else can show them. Adds a browser e2e covering the
sidebar move flow end-to-end.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
Follow-up polish on the grouped/colored help (#3795). Focused on the
one-liner copy and a duplicate listing entry.
- Normalize harness short help to `Launch <Name> with Omnigent.` — was
an inconsistent mix of `Launch [the] <Name> [TUI] in an Omnigent
terminal`, and "in an Omnigent terminal" was noisy.
- Trim over-long / over-specific one-liners:
- `attach`: drop the "— never starts anything" clause (the body still
explains it's a pure client).
- `uninstall`: `Uninstall Omnigent from this machine.`
- `usage`: `Show your Omnigent usage and costs.` (was pinned to
today / 7 / 30 days).
- `upgrade`: `Upgrade Omnigent to the latest release.`
- `debug`: `Internal maintenance commands.`
- Hide the `update` alias (same Click object as `upgrade`) from the
listing via `_ALIAS_COMMANDS`, so it no longer shows as a duplicate
line; it stays registered and runnable.
- Update/extend tests for the new copy and the hidden `update` alias.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Split the top-level `omnigent --help` command list into two sections —
`Harnesses` (agent/harness launch commands) and `Commands` (everything
else) — add brand-accent color, and hide harnesses whose optional extra
isn't installed (with a small notice pointing at `omnigent setup`).
- Add a `format_commands` override on `_OmnigentCLI` that partitions
visible subcommands using a `_HARNESS_COMMANDS` set, sharing one
aligned help column across both sections.
- Colorize headings (`Usage:`, `Options`, `Harnesses`, `Commands`) in
the brand accent, harness names in accent, other command names in
cyan, and option flags in green — via `format_usage`/`format_options`
overrides and a `_help_style` helper.
- Hide extras-gated harnesses (`cursor`, `antigravity`) from the listing
when their SDK isn't importable, via `_harness_extra_checks` (lazy
`find_spec` predicates). The commands stay runnable — running one
offers to install the extra. When any are hidden, show a dim notice
pointing at `omnigent setup` (which lists those harnesses and offers
the install), rather than enumerating extras that may change.
- Color is gated on `NO_COLOR` and Click strips ANSI on non-TTY sinks,
so piped/CI help stays plain. Alignment is ANSI-safe (Click's
`term_len` strips escapes before measuring columns).
- Add tests covering grouping, the extras-gated show/hide, and the notice.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
N/A
## Summary
- Added `--extra`, `--target-version`, and `--dry-run` flags to `omni upgrade`.
- Upgrade commands for `uv tool` and `pipx` now read the originally requested extras from the installer's receipt/metadata and preserve them.
- Explicitly refuses auto-upgrade for `pip` and `uv pip` because those installers do not record requested extras, making a safe automatic upgrade impossible.
- Fixed installer metadata detection in `uv tool` installs by avoiding `Path.resolve()` on the `bin/python` symlink, which previously pointed to the shared uv interpreter and missed `uv-receipt.toml`.
- Added/updated unit and CLI tests covering the new behavior.
## Test Plan
- `uv run pytest tests/cli/test_upgrade_command.py tests/cli/test_update_check.py tests/cli/test_cli.py -q --timeout=60` → **383 passed**.
- `uv run pytest tests/cli/test_update_check.py -q --timeout=60` → **112 passed**.
- Manually built a local wheel, installed it as a `uv tool`, and verified dry-run output.
- Verified `--extra` unions with detected extras.
- Verified `uv pip` install is refused with a manual-upgrade message.
## Demo
N/A
## Type of change
- [x] Feature
- [ ] Bug fix
- [ ] 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
Manual verification was done by building local wheels and installing the current dev version as a `uv tool`:
```bash
# 1. Build wheels
rm -rf /tmp/omnibuild && mkdir -p /tmp/omnibuild
uv build --wheel -o /tmp/omnibuild .
uv build --wheel -o /tmp/omnibuild sdks/python-client
uv build --wheel -o /tmp/omnibuild sdks/ui
# 2. Install as uv tool with the "all" extra
rm -rf /tmp/omni-dev-test
UV_TOOL_DIR=/tmp/omni-dev-test uv tool install --find-links /tmp/omnibuild \
'/tmp/omnibuild/omnigent-0.8.0.dev0-py3-none-any.whl[all]' --force
# 3. Dry-run upgrade from outside the source repo
cd /tmp && /tmp/omni-dev-test/omnigent/bin/omni upgrade --dry-run --target-version 0.8.0
```
Output:
```text
Targeting v0.8.0.
Detected installer: uv
Detected extras: all
Would run: uv tool install --reinstall omnigent==0.8.0[all]
```
Adding `--extra server` unions with the detected extra:
```bash
cd /tmp && /tmp/omni-dev-test/omnigent/bin/omni upgrade --dry-run --target-version 0.8.0 --extra server
```
Output:
```text
Targeting v0.8.0.
Detected installer: uv
Detected extras: server
Would run: uv tool install --reinstall omnigent==0.8.0[all,server]
```
A `uv pip` install is correctly refused:
```text
omnigent was installed with `uv pip`, not `uv tool install`. `uv pip` does not record which extras were requested, so `omni upgrade` cannot preserve them safely. Upgrade manually:
uv pip install -U omnigent
# or, if you need extras:
uv pip install -U 'omnigent[your,extras,here]'
```
## Changelog
`omni upgrade` now preserves requested extras for `uv tool` and `pipx` installs, supports `--extra`, `--target-version`, and `--dry-run`, and refuses to auto-upgrade `pip` / `uv pip` installs because those installers don't record extras.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
N/A
## Summary
- `nodeLinker: hoisted` was a compatibility shim from the npm→pnpm migration
that forced an npm-style flat `node_modules`. Removing it returns pnpm to its
default isolated/symlinked layout (packages under `node_modules/.pnpm/…`),
restoring strict dependency isolation — dependencies must be declared, so
phantom/undeclared deps stop resolving by accident.
- Validated that the blockers the shim was assumed to guard against don't
actually block under the isolated layout (details in Test Plan). The Shiki
cyclic-import crash is handled by the existing `manualChunks` guard in
`web/vite.config.ts` (a chunking concern, independent of the node linker), and
electron-builder v26 collects the production dependency tree correctly through
pnpm's symlinks.
## Test Plan
Validated locally under the isolated layout:
- `pnpm install --frozen-lockfile` — clean and lockfile-consistent (the linker
setting is not part of the lockfile, so no lockfile churn).
- `pnpm --filter web run build` — succeeds; Shiki resolves to a single acyclic
chunk via the existing `manualChunks` guard.
- Electron packaging: `pnpm --filter web run build:overlay` then
`electron-builder --dir` builds and signs the app; inspected the resulting
`app.asar` — it bundles exactly the production dep tree (`electron-updater`,
`js-yaml` + their 14 transitive deps) with zero dev-dependency bloat.
- Tailwind v4 `@source` scan follows the symlink: the emitted CSS is
byte-identical between the hoisted and isolated builds.
- oxlint (schema) and prettier run; `node web/node_modules/vite/bin/vite.js
--version` (Android Gradle entry) and `web/node_modules/.bin/tsc --version`
(iOS Fastlane probe) resolve via pnpm's direct-dependency symlinks.
Not runnable locally — relying on CI to confirm: Docker image build,
`electron-build` full installers, and `android-bundle` / iOS app builds.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [x] Not applicable
## Coverage notes
Node-linker layout has no unit-test surface; verified manually via a full
install + web build + electron `--dir` packaging (inspecting the packaged
`app.asar` dependency tree) + a Tailwind CSS byte-diff, and confirmed the
hardcoded node_modules paths (vite entry, tsc/prettier/oxlint) resolve through
pnpm's direct-dependency symlinks. Remaining platform builds are covered by CI.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
N/A
- Remove the legacy `_LAUNCHERS` fallback from `__init__.py` — all providers
are now resolved exclusively through the `SandboxProviderRegistry`
contribution-based registry.
- Simplify `get_launcher()` to a single code path (no more
`DeprecationWarning` / legacy import fallback).
- Remove unused `warnings` / `importlib` / `importlib.util` imports from
`__init__.py`.
- Add `docs/extending/sandbox_providers.md` documenting how to implement and
register a third-party sandbox provider, including a minimal example
package with `pyproject.toml` entrypoint, the namespace requirement, and
the capability reference table.
```bash
uv run pytest tests/onboarding/sandboxes tests/sandbox tests/cli/test_cli.py tests/server -k managed -q
pre-commit run --files <changed files>
```
All 782 selected tests pass and pre-commit is clean.
N/A
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
Existing tests pass unchanged. The test that expected a `DeprecationWarning`
from the legacy path was updated to no longer suppress it. New docs are
prose-only and need no test coverage.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
N/A
## Summary
- The `manualChunks` guard that keeps Shiki in one chunk (to avoid a cyclic
import crash — the language-index ↔ alias-map split that throws "Cannot read
properties of undefined (reading 'flatMap')" and blanks the Monaco/file
viewer) matched both `/shiki` and `/@shikijs/`. That also swept every
`@shikijs/langs/<lang>` grammar — which Shiki loads via dynamic import as
per-language chunks — into the single, eagerly `modulepreload`ed core chunk.
So ~200 language grammars (~1.68 MB gzip) were downloaded on every initial
page load, even though a session uses only a few languages.
- Exclude `@shikijs/langs/<lang>` from the `shiki` chunk so grammars stay lazy
per-language chunks. Keep Shiki's core, engines, and bundle glue together so
the cyclic core stays intra-chunk — the engines must stay too: excluding them
re-splits the cycle across chunks and reintroduces the `flatMap` crash.
- Initial-load eager JS drops from ~11.8 MB to ~4.37 MB (Shiki 1.68 MB → 466 KB
gzip); grammars become 427 on-demand chunks. Layout-independent (same result
under pnpm hoisted and isolated).
## Test Plan
- `pnpm --filter web run build` succeeds.
- Verified the emitted `shiki` chunk statically imports only the rolldown
runtime (no cross-chunk cycle) and contains `bundledLanguagesAlias`
co-located with its reader — under both hoisted and isolated node_modules.
- Verified per-language grammar chunks (python, rust, typescript, …) are
emitted separately and are NOT `modulepreload`ed by `index.html`.
- Recommended pre-merge smoke test: open the file viewer / Monaco editor and a
markdown code block and confirm syntax highlighting renders.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Verified via build output analysis: the `shiki` core chunk is acyclic with the
alias map co-located (cycle fix preserved), and language grammars are emitted as
separate, non-preloaded chunks. Existing Shiki/code-block tests exercise the
runtime highlighting path; this change only affects chunk grouping, not module
behavior.
## Changelog
Faster initial load — syntax-highlighting language grammars now load on demand instead of all upfront.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Remove the release-specific Gemini fallback from the Antigravity SDK executor. Preserve explicit per-turn and HARNESS_ANTIGRAVITY_MODEL precedence, but omit LocalAgentConfig.model when neither is set so every supported google-antigravity 0.1.x release owns its current default for both API-key and Vertex sessions.
Expand the no-hardcoded-model scanner to recognize dotted, canonical, and normalized Gemini release ids. Add coverage that distinguishes an omitted SDK model from an explicit override, and remove the stale release example from runtime error text.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
The embed build set sourcemap: false, so downstream bundlers that embed this
output (e.g. the Databricks monolith's rspack/webpack) had no input map to
chain through — host-side error stack frames bottomed out at
omnigent-embed.js:<line> instead of the original src/**.
Emit maps so the embedding host can compose them to source. dist-embed is a
build artifact (gitignored), so this ships nothing new; it only enriches the
maps hosts consume via source-map-loader.
Signed-off-by: Hubert Zub <hubert.zub@databricks.com>
Co-authored-by: Hubert Zub <hubert.zub@databricks.com>
* feat(sandbox): scan write_paths for dotfiles too
The dotfile / escaping-symlink masker walked cwd and every read_paths
root but skipped write_paths, so a writable directory granted outside
cwd could still leak — and let the helper overwrite — top-level secrets
like .env / .aws / .ssh.
Fold read_paths and write_paths into one deduplicated, ancestor-first
set via a new merge_scan_roots helper so every granted root is masked,
and a path granted by more than one lever (or nested under another
grant) is walked once instead of once per lever. The dedup resolves
each root a single time and skips nested roots with a lexicographic
cover scan, so the big-grant profile-size guard stays fast.
Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
* fix(sandbox): only drop nested grants when scanning recursively
Review caught that merge_scan_roots dropped a granted root whenever
another grant was its ancestor — but that subsumption only holds when
the walk is recursive. cwd_hidden_scan_recursive defaults to False,
where each walk masks only a root's immediate children, so dropping a
nested grant (e.g. write_paths: [/a/deep/nested] under read_paths:
[/a]) left its top-level dotfiles visible and writable — reintroducing
the exact leak this branch closes, and regressing the prior
per-read-root behavior.
Thread the recursive flag into merge_scan_roots: keep the cwd drop
(unchanged, pre-existing), but only collapse a grant into a kept
ancestor when recursive=True; in top-level-only mode keep every
distinct grant and drop only exact duplicates. Walk the full ancestor
chain (not just the last kept root) so an interleaving sibling name
cannot hide a real ancestor and leave a redundant walk.
Adds regression tests in both backends for the non-recursive nested
grant, plus merge_scan_roots unit coverage for both modes.
Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
* fix(sandbox): exclude framework scratch roots from the write-root dotfile scan
Extending the dotfile mask scan to write_paths also swept in the
framework-added scratch tmpdir (folded into write_roots via
with_additional_write_roots). That dir holds the sandbox's own egress
relay socket `.egress.sock` — a dotfile — so the scan masked it with
`--bind-try /dev/null` (bwrap) / a deny rule (seatbelt), cutting the
relay endpoint and resetting every egress connection. This is what broke
the inner-rest `test_egress_e2e[linux_bwrap]` cases.
Track framework write roots on the policy as `mask_scan_skip_roots` and
drop them (and anything nested under them) from `merge_scan_roots`. These
dirs are created fresh by the framework and never hold pre-existing user
secrets, so scanning them is both pointless and harmful. Genuine
user-declared read/write grants are still scanned.
Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
---------
Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
* lint(models): remove the hardcode baseline
Delete the empty path/count allowlist and its parser, stale-count logic, tests, and special pre-commit trigger. The scanner now rejects every non-owned production model literal while retaining only the AST-verified StaticModelFallback boundary.
Update the migration plan to describe the final configuration/catalog/fallback state. The fully merged issue 3426 audit passes 136 focused tests, mypy, the hardcode scan, and full pre-commit.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* lint(models): scan every production model literal
Remove the model-context name heuristic so positional arguments, bare collections, and config values under arbitrary keys cannot bypass the hardcoded-model check. Preserve docstrings and structurally owned fallback records as explicit non-runtime exceptions, and distinguish complete model ids from stable family-prefix compatibility checks.
Curate the newly exposed production literals by resolving Claude's direct-login custom model through the central owned fallback and replacing release-specific CLI, Bedrock, and loader examples with provider-neutral guidance.
Validated with 172 lint/Claude tests, 63 loader tests, focused mypy, the baseline-free repository scan, and pre-commit run --all-files.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* lint(models): cover the full production tree
Run the hardcoded-model scanner across every tracked Python and supported config/shell file rather than a curated directory list. Exclude tests and generated OpenAPI explicitly, and keep the pre-commit trigger exactly aligned with the scanner surface.
Remove the unused root server config that pinned a stale Databricks model and profile. A repository-wide dry run found no other non-generated production literals outside the existing scan surface.
Validated with the focused lint suite, the baseline-free full repository scan, focused mypy, and pre-commit run --all-files.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): keep Claude custom fallback routable
Resolve the private Sonnet custom-picker id through the exact owned fallback when available, then through the first routable Sonnet-family entry if release naming drifts. Fail clearly when the owned subscription catalog contains no Sonnet entry instead of forwarding an invalid picker id.\n\nRemove the vestigial full-tree scan-root constant, keep the pre-commit parity probe direct, make the Gemini docstring fixture exercise a recognized id shape, and update the migration guide to describe the actual full-tree literal scan and runtime-prose expectations.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* ci(models): configure automation model roles
Replace provider release ids in credentialed workflows with six repository-variable roles covering Anthropic, fast Anthropic, OpenAI, E2E judge, E2E model pool, and image generation workloads.
Make the shared Omnigent agent action require an explicit model input, validate required configuration before writing provider files, and keep fail-open reviewer/image helpers on their existing degradation paths.
Use the protocol-level mock-model fixture for mock-only integration matrices and remove their unused production model-spread configuration.
Validation: parsed all action/workflow YAML; generated integration and backcompat matrices; hardcode lint and staged pre-commit passed.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* ci: fail fast without E2E judge model
Require the repository-level E2E judge model variable before running the required-check script. This turns an absent CI configuration into an immediate, actionable failure instead of allowing a later command to fail ambiguously.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* ci: clarify missing optional model variables
Keep the advisory reviewer ranker, VS Code changelog drafter, and feature-blog image generator fail-open when their repository model variables are empty.\n\nEmit actionable variable names before skipping or falling through to the existing warning path, avoiding malformed gateway requests while preserving the best-effort behavior of all three jobs.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Add a lint step that fails when a `pnpm-workspace.yaml` `overrides:` pin
doesn't satisfy the range the same package declares in a workspace
`package.json`. Overrides outrank package.json, so such a mismatch
silently ignores the declared version — the trap that let a postcss
security bump (`^8.5.18`) land while the override still pinned the
vulnerable `8.5.15`, invisible to both `--frozen-lockfile` and the
lockfile-regen gate (the lock was internally consistent for the pin).
Runs in the lint job beside the existing "Check pnpm-lock.yaml is up to
date" step. The checker uses a small npm-flavored semver comparison
(`^`, `~`, exact, comparators) over the operators this repo uses;
unrecognized ranges are reported rather than passed silently.
Co-authored-by: Isaac
* ci(release): add source-PR demo-video table to release-post PRs
The publish-changelog workflow reformats a published release into a site post
that leaves a `TODO` demo placeholder under each feature, with no pointer to
the source PRs that may already ship a recording. Parse the feature PR refs
from the curated release body (Major new features / Breaking changes sections;
bug fixes are dropped from the post, so from the table too), detect whether
each PR already has a demo video attached (same detection as feature-blog.yml
— uploaded asset links, bare .mp4/.mov/.webm/.m4v URLs, <video> tags; images
not counted), and inject a per-section PR | Title | Demo video? table into the
release-post PR body (and the dry-run preview) so reviewers can drop an
existing clip into a placeholder instead of re-recording.
Runs independent of the LLM reflow so it also helps the raw-body fallback;
best-effort (continue-on-error), leaving the table empty on failure.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* ci(release): group demo-video table by the post's curated features
The demo-video table grouped PRs by the raw release-body sections, so it listed
every feature PR (e.g. all 20 under "Major new features") even though the
published post is curated down to a handful of headline features, each with one
demo placeholder. Reviewers saw far more PRs than the post has slots for.
Have the release-post-formatter emit a RELEASE_POST_PRS map (feature title ->
contributing PR refs) after the post, and build the table from that so its
groups match the post's numbered features and only list the PRs behind them.
Validate the map against the harvested PR set. When no map is present (raw-body
fallback, where the post keeps every feature), fall back to grouping by the raw
release sections as before.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* ci(release): match feature-section headings at any level
The demo-table section parser matched only `## ` headings, but the release body
uses `### ` (h3) section headings, so it found zero feature sections and built
an empty table. Match `#{2,}` and test the heading TEXT with startswith, so
"Major new features" / "Breaking changes" match at any level while "Bug fixes
& hardening" and "Thanks to our community" are still excluded.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
The pnpm-workspace.yaml `overrides:` block force-pinned postcss to
8.5.15 and linkify-it to 5.0.1 — both flagged by open high-severity
advisories (postcss GHSA-r28c-9q8g-f849, linkify-it
GHSA-v245-v573-v5vm). Because the override sits above package.json, the
earlier dependabot bump of postcss to ^8.5.18 (#3385) was inert: the
lock kept resolving 8.5.15, so the CVE was never actually fixed, and the
frozen-lockfile gate saw no drift.
Bump the two override pins to the patched releases and regenerate the
lock (postcss 8.5.18, linkify-it 5.0.2). Both are same-minor patch
bumps confined to security/bug fixes — unlike the vite/tailwind/
lightningcss pins in the same block, they aren't the bundler, so they
don't affect chunk splitting or the Shiki/PDF-worker asset emission the
override comment warns about. CI's Docker build + web test validate the
bundle.
Co-authored-by: Isaac
* feat(models): persist last-known-good catalogs
Persist validated MLflow provider catalogs under the platform user-cache directory so catalog-backed defaults survive transient GitHub and release-CDN outages after one successful fetch.
Keep the existing one-hour freshness window, fall back to stale validated data for at most seven days after a live failure, and record cache schema, upstream schema, source URL, and fetch time. Atomic replacement keeps concurrent writers from exposing partial JSON, while corrupt, incompatible, wrong-source, and over-age entries fail closed.
Make OMNIGENT_DISABLE_CATALOG_LOOKUP bypass memory, disk, and network state for hermetic tests. Cover persistence, fresh reuse, stale provenance logging, corruption repair, schema/source rejection, over-age behavior, concurrent writes, and first-run failure.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): accept compatible catalog schemas
Validate the current MLflow catalog shape without coupling live discovery or persistent cache reuse to one exact minor schema string. Accept major-version-compatible string and integer forms, continue rejecting unsupported majors and malformed values, and document when stale in-memory fallbacks retry discovery.
Production release assets for Anthropic, OpenAI, Gemini, and OpenRouter were verified against the validator; focused catalog tests and full pre-commit pass.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): preserve cache across empty catalogs
Reject empty live catalog model maps so transient or truncated upstream payloads cannot overwrite useful last-known-good data. Tighten compatible schema parsing to ASCII digits so corrupt cache metadata is ignored instead of raising.
Percent-encode provider names in release asset URLs to keep path and query delimiters inert. Add regression coverage for empty-result fallback preservation, non-ASCII schema corruption, and URL construction.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* ci(oss): gate lockfile regen on a consistency check
The OSS lockfile-regen job deleted pnpm-lock.yaml and re-resolved from
scratch every 12h, so any in-range transitive drift on public npm
produced a ~1500-line churn PR that advanced ~20 unreviewed deps for no
functional reason (e.g. #3631). The job exists to keep the tree
Docker-buildable when a manifest change desyncs the lock — not to chase
newer upstream versions.
Add a check-first gate: `uv lock --check` and pnpm
`--frozen-lockfile --lockfile-only` verify each lock still satisfies its
manifests. These pass on a consistent-but-not-latest lock, so routine
drift no longer triggers a regen; only a real manifest/lock desync flips
`drifted=true` and runs the regenerate → Docker smoke → PR steps.
Also correct the PR-body text, which claimed it regenerated "uv.lock +
web/package-lock.json" (the repo locks pnpm-lock.yaml, not
package-lock.json).
Co-authored-by: Isaac
* ci(oss): keep the Docker smoke on the no-drift path
Per PR review: ungate the Docker build + CLI smoke so they run every 12h
regardless of drift. On the drifted path they still validate the freshly
regenerated locks before commit; on the clean path they remain the
ongoing proof that the committed locks + public registries build a
working image — catching buildability regressions independent of
manifest state (a yanked-but-in-range package, a Dockerfile break) that
the check-only gate would otherwise miss.
Co-authored-by: Isaac
* ci(oss): gate each ecosystem's regen on its own drift flag
Per PR review: a single shared `drifted` flag meant a desync in one
ecosystem (say uv.lock) still ran the `rm -f pnpm-lock.yaml &&
pnpm install` from-scratch regen of the other, re-resolving it against
public npm and reintroducing exactly the in-range transitive churn this
job is meant to avoid.
Split into `drifted_uv` / `drifted_pnpm` and gate each Regenerate step
on its own flag. A combined `drifted` (either) still drives the shared
token-mint and open-PR steps; the commit stages only whichever lockfile
actually changed.
Co-authored-by: Isaac
* feat(web): redesign sidebar bulk-selection bar and scope it per section
Rework the sidebar's bulk-selection UI into a single bordered "pill" bar
rendered directly under the header of the section it targets, and give
selection an explicit scope so it acts on the right rows.
- Bar redesign: one pill row with an Exit (X) button, an "N selected"
count at the session-title font size, and icon-only Archive + Delete
actions. Archive shows by default and is disabled until an archivable
session is selected (Delete likewise). Unarchive replaces Archive only
when the selection is entirely archived.
- Row checkbox moved to the left of the session title.
- Selection scope: the Sessions-header trigger selects the flat session
list; the Projects-header kebab's "Select sessions" selects the
sessions nested inside project folders (bar renders under the Projects
header). Entering a scope preserves current folder expansion. The
shift-select range and a stranding guard follow the active scope.
- Fold the Projects expand-all/collapse controls plus "Select sessions"
into a kebab to the right of the New-project (+) button.
Test-only: update unit tests for the new layout/scoping and rewrite the
e2e-ui bulk-actions suite (5 passing) to match the redesign, including a
projects-scope round-trip.
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): resolve projects-scope selection against folders' own rows
Projects-scope bulk selection sourced its action set, shift-select range,
and stranding guard from the global paginated window
(sections.projectGroups), but each ProjectFolder renders from its own
independent useProjectSessions query. A folder member outside the global
window would toggle the count yet silently drop from bulk archive/delete,
break shift-select, or trip the stranding guard.
Each ProjectFolder now reports its rendered rows up via
onConversationsLoaded; the parent unions them (deduped) into a
projectSessionPool that backs the bulk-action bar, the shift-select range,
and the guard — so all three agree on what's selectable regardless of the
global pagination window.
Adds a regression test: with the folder query returning p1,p2,p3 while the
global window holds only p1,p2, shift-select p1->p3 spans all three and
bulk-archive fires with p3 included.
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): surface owned Delete count and guard selection-mode against transient empties
Addresses two non-blocking review notes on the bulk-selection bar:
- Delete acts only on owned rows, so a mixed-ownership selection (reachable
in projects scope, where a folder can hold others' sessions) read
"N selected" while Delete hit fewer. The Delete control's label/tooltip
now shows the owned count ("Delete 2") when it differs from the selection
size. Archive needs no such hint (its enable-gate already forces a
uniform archive group, and archived rows never appear in a selectable
section).
- The stranding guard that exits selection mode when the pool empties now
skips while the sessions query is refetching, so a background refetch
that briefly yields an empty page can't kick the user out mid-task.
Adds a mixed-ownership Delete-label test and updates the layout spec's
label assertion.
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* docs(web): clarify projects-scope stranding guard can't exit on a folder refetch
The exit-on-empty guard suppresses the global query's refetch via
conversationsQuery.isFetching, but the projects pool is fed by per-folder
queries too. Note that the pool unions global-derived membership, so a
single folder's transient-empty refetch can't zero it while any member is
in the global window — only a genuinely empty pool exits. Comment-only.
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
The feature-blog workflow leaves a `DEMO REQUIRED` marker in each drafted
post and tells the reviewer to record a demo, with no hint that the source
PRs may already ship one. Collect the contributing PRs per feature and detect
whether each already has a demo video attached (uploaded asset links, bare
.mp4/.mov/.webm/.m4v URLs, or <video> tags — images are not counted), then
inject a PR | Title | Demo video? table into the draft PR body so reviewers
can pull an existing recording into the marker instead of re-recording.
Reuses the gh pr view call already made to pick the reviewer (extended with
title/body/url). The table is written per feature even when no PR has a video.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* feat(web): rework workspace rail — tabs, shell-as-tab, maximize, + menu
Reworks the desktop right "Workspace" rail's tab strip so open items and
navigation read as one editor-style set, and gives shells a home inside the
rail instead of taking over the chat column.
- Reorder the strip: open file/shell tabs own the flexible left region; the
static nav tabs (Files/Agents/Shells/Tasks/Browser) sit right when tabs are
open, else stay anchored left.
- Shells open as top-strip tabs (desktop): clicking a shell row opens it as a
closable rail tab whose xterm renders in the rail's content slot — the chat
page is undisturbed. Mobile keeps the full-screen drawer.
- Add a full-screen (maximize) toggle pinned to the rightmost edge; maximized
keeps the docked card styling (same inset/height), only the width changes.
- Add a "+" menu ("Open new" → Shell) that trails the last tab when tabs are
open, else sits by the nav tabs. Browser stays a pinned tab (one embedded
WebContentsView per conversation).
- Tighten strip spacing and give the nav icons a consistent hover background;
smaller shell-tab label text.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): keep one ml-auto in the rail strip; drop phantom gap & maximize padding
Follow-up layout fixes to the workspace rail tab strip:
- Only ever one ml-auto in the strip row — two siblings both claiming it split
the free space and stranded the nav group mid-strip. With open tabs the
divider owns ml-auto (dragging nav + maximize right together); with no tabs
the maximize button owns it (nav group stays left).
- The divider dropped its ≥500px container-query gate so it shows at any rail
width instead of vanishing on a narrow rail.
- FileTabsStrip / TerminalTabsStrip return null when empty — an empty wrapper
still consumed a slot in the region's gap and left a phantom gap before the
trailing "+".
- Removed the maximize button's pl-0.5 so it sits flush like the other icons.
Adds regression tests asserting exactly one ml-auto per strip state, the
divider's presence/placement, the no-phantom-gap child count, and no maximize
padding.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): flat tab hover background — opaque fill, no gradient patch
The tab hover used bg-muted, but --muted is a translucent token (6% black).
The close-button overlay then faded in a second translucent gradient on top,
stacking alpha on the right edge into a visible darker patch. Use the same
opaque color-mix selection surface the active tab uses for both the hover
background and the overlay gradient, so hover is a flat even fill.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e): update shell-open tests for rail-tab behavior
Shells now open as tabs in the workspace rail (xterm in the rail content
slot) instead of taking over the main column via MainTerminalView. Update
the three e2e tests that asserted the old main-column flow:
- shells/test_new_shell: assert the shell opens as a rail tab (Close
"zsh · u-…" x + rail-scoped xterm) with the chat surface undisturbed.
- files/test_right_panel: clicking a shell row opens a "zsh · main" rail
tab; xterm connects in the rail, chat not replaced.
- sessions/test_terminal_theme: resolve the connected xterm inside the
Workspace rail rather than main-terminal-view.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): regenerate visual baselines
* feat(web): shell-type picker, pinned rail tab strip, sidebar restore
Follow-ups to the workspace-rail rework:
- "+" menu Shell entry: clicking Shell launches the remembered default type
immediately (selection optional); the submenu check-marks and remembers the
last-picked type (persisted to localStorage), used as the next default.
- Removed the Shells tab's "+ New shell" row — shell creation now lives solely
in the "+" menu. The Shells tab is a pure list.
- Hide the Shells tab (and mobile entry) unless a shell actually exists; merely
declaring shell access no longer surfaces an empty tab.
- Tab strip: nav icons + divider stay pinned left and the "+" stays pinned right
at every rail width — the tabs region is the sole horizontal scroller, and the
"+" sits outside it (no scroll/overlap). Divider shows at all widths again.
- Full screen: collapse the left sidebar on enter and restore its prior state on
exit (collapsed stays collapsed, open reopens).
Updated unit + e2e tests to match (shell-open via the "+" menu; Shells-tab gate).
Co-authored-by: Isaac
* fix(web): keep "+ New shell" in the mobile Shells drawer
Removing the "+ New shell" row broke first-shell creation on mobile, which has
no tab-strip "+" menu. Restore it there only:
- InlineTerminalsSection gains an opt-in ``showNewShell`` prop (default off);
the desktop rail stays list-only, the mobile drawer passes it to surface the
create row.
- The mobile Shells menu entry gates on existing-shell OR declared shell access
(so the drawer is reachable at zero shells), while the desktop rail tab stays
gated on an existing shell.
- Update the two e2e tests that opened a shell via the removed row to use the
"+" menu; the mobile drawer test's docstring clarifies the mobile-only create
path.
Co-authored-by: Isaac
* test(e2e-ui): regenerate visual baselines
* fix(web): restore sidebar on session-switch un-maximize; detangle toggle
Addresses Polly review notes on the full-screen sidebar handling:
- The session-switch reset un-maximizes the rail directly, but didn't restore
the sidebar it collapsed on entry — so maximize → switch conversation left the
sidebar silently collapsed. Extract restoreSidebarAfterMaximize() and call it
from the reset (only when we were maximized).
- Move the sidebar side effect out of the setRightPanelMaximized updater into a
plain toggleRightPanelMaximized handler, so the state setter stays a pure
prev→next flip instead of nesting other setters.
Co-authored-by: Isaac
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
description: Run the Omnigent load test and produce a results file explaining the latencies. Load when the user wants to load-test / stress-test / benchmark Omnigent under concurrency ("load test omnigent", "stress test the server", "how many hosts/sessions/turns can it handle", "load test real agent turns / conversations", "run a load test"). The test makes each simulated user a real omnigent host that creates host-bound sessions and drives real multi-turn conversations with a mocked LLM; it boots its own local stack (dev/loadtest/run.py). Gather inputs, run it, then read the generated summary.md and explain the latency distribution (avg/median/p95/p99, throughput, failures). NOT for single-request latency micro-benchmarks (that is dev/benchmarks/).
---
# Run the Omnigent load test
Drives `dev/loadtest/` end to end: collect inputs → run → read `summary.md` →
explain the latencies. **Each Locust user is a real `omnigent host`** that
registers over the host tunnel, creates host-bound sessions, and drives **real
multi-turn conversations** — every turn is a genuine post→idle loop through the
host's runner, with the **LLM mocked** (zero latency) so the numbers are
Omnigent's own overhead. `-u N` scales the number of hosts.
It **boots its own local stack** (server + mock LLM), so there is no server to
point at, and it runs **from a repo checkout** only. For single-request latency
micro-benchmarks (not concurrency), that is a different tool: `dev/benchmarks/`.
body="$(printf 'Drafts a feature-blog post for **%s**, selected by `feature-blog-scout` at the %s release cut.\n\n> **This is a DRAFT.** Before merging, a human must: record the mandatory demo (replace the `DEMO REQUIRED` marker) and do a final voice pass. The hero image and `author: omnigent` byline are auto-generated — review and optionally replace them.\n\n%s\n\nSource release: %s%s\n<sub>Generated by omnigent `.github/workflows/feature-blog.yml`. Review for accuracy before merging.</sub>' "$title" "$TAG" "$summary" "$TAG" "$mention")"
# Reference table of the contributing PRs and whether each already
# ships a demo video (built in the Draft posts step). Reviewers can pull
# an existing recording from a ✅ PR to replace the `DEMO REQUIRED`
# marker instead of re-recording. Omitted if the table wasn't produced.
demo_table=""
if [ -f "/tmp/demo_table_${idx}.md" ]; then
demo_table="$(printf '\n\n### Source PRs — demo videos\nCheck a ✅ PR for a recording you can drop into the `DEMO REQUIRED` marker.\n\n%s\n' "$(cat "/tmp/demo_table_${idx}.md")")"
fi
body="$(printf 'Drafts a feature-blog post for **%s**, selected by `feature-blog-scout` at the %s release cut.\n\n> **This is a DRAFT.** Before merging, a human must: record the mandatory demo (replace the `DEMO REQUIRED` marker) and do a final voice pass. The hero image and `author: omnigent` byline are auto-generated — review and optionally replace them.\n\n%s%s\n\nSource release: %s%s\n<sub>Generated by omnigent `.github/workflows/feature-blog.yml`. Review for accuracy before merging.</sub>' "$title" "$TAG" "$summary" "$demo_table" "$TAG" "$mention")"
existing="$(gh pr list --repo "$SITE_REPO" --head "$branch" --state open --json url --jq '.[].url' | head -n1)"
# exempts gh from `set -e`, so a non-zero exit hits the else branch.)
if gh pr create --base main --head "$BRANCH" \
--title "chore(oss): regenerate public lockfiles against public PyPI/npm" \
--body "Automated: regenerated uv.lock + web/package-lock.json against public PyPI/npm, validated by a Docker build + omnigent --help smoke (run ${{ github.run_id }}). Merge to keep the public lockfiles current and buildable."; then
--body "Automated: the lockfiles were out of sync with the manifests, so uv.lock + pnpm-lock.yaml were regenerated against public PyPI/npm and validated by a Docker build + omnigent --help smoke (run ${{ github.run_id }}). Merge to keep the public lockfiles consistent and buildable."; then
echo "Opened the regen PR."
else
echo "::warning::Could not open the regen PR automatically (the GITHUB_TOKEN may be disallowed from creating PRs). The branch '$BRANCH' is pushed with the regenerated lockfiles — open the PR by hand:"
body="$(printf 'Publishes the **%s** release post at `/releases/%s` — the curated GitHub Release notes reformatted into the site'"'"'s narrative, prose-driven style.\n\nGenerated by omnigent `.github/workflows/publish-changelog.yml`. Edit the GitHub Release, not this file.' "$TAG" "$VERSION")"
# Append the demo-video reference table: which feature PRs already ship a
# recording a reviewer can drop into the post'"'"'s `TODO` demo placeholders.
if [ -s /tmp/demo_table.md ]; then
body="$(printf '%s\n\n### Source PRs — demo videos\nCheck a ✅ PR for a recording to replace a `TODO` demo placeholder in the post.\n\n%s' "$body" "$(cat /tmp/demo_table.md)")"
echo "Tag ${TAG} already at the converged release commit ${base_sha} — nothing to do." \
| tee -a "$GITHUB_STEP_SUMMARY"
else
echo "::error::Tag ${TAG} already exists at ${tag_sha} (stamped version: ${stamped:-unknown}), which is not the converged branch head ${base_sha}. Delete the tag first if this is recovery (see RELEASING.md)."
echo "::error::Tag ${TAG} already exists at ${tag_sha} (stamped version: ${stamped:-unknown}), which is not the converged branch head ${base_sha}. Delete the tag first if this is recovery (see the maintainer release runbook)."
exit 1
fi
fi
@@ -600,6 +600,10 @@ jobs:
uv run --no-project --python 3.12 --with packaging \
echo "2. Validate the rc from PyPI (see RELEASING.md). No GitHub release is created for rc tags (rcs live on PyPI only) — skip straight to the next rc or the final cut."
echo "2. Validate the rc from PyPI (see the maintainer release runbook). No GitHub release is created for rc tags (rcs live on PyPI only) — skip straight to the next rc or the final cut."
else
echo "2. Merge the CHANGELOG PR, curate the ${TAG} draft notes, then dispatch finalize-release.yml (tag=${TAG})."
echo "::warning::Resource resolution failed with ${TAG} only ${age_h}h old — inside pip's --uploaded-prior-to=P1D window. The nightly catch-up will open the tap PR."
echo "Deferred to the nightly catch-up (${TAG} is ${age_h}h old, inside the 24h PyPI window)." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
exit 1
fi
echo "deferred=false" >> "$GITHUB_OUTPUT"
brew style omnigent-ai/tap/omnigent
- name:Assert the hand-maintained sections survived
if [ -n "$(gh pr list --repo "$TAP_REPO" --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
echo "Bump PR already open for ${BRANCH} — force-push updated it." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
body="$(printf 'Bumps the omnigent formula to **%s** (new sdist url/sha256, resources regenerated via `brew update-python-resources`).\n\ntest-bot builds the bottles on this PR. Review the resource diff — especially that the extras'"'"' deps survived — then apply the `pr-pull` label to publish bottles and merge.\n\nOpened by omnigent `.github/workflows/update-homebrew.yml`.' "$VERSION")"
gh pr create \
--repo "$TAP_REPO" \
--base main \
--head "$BRANCH" \
--title "omnigent ${VERSION}" \
--body "$body"
echo "Opened tap bump PR for omnigent ${VERSION}." | tee -a "$GITHUB_STEP_SUMMARY"
@@ -5,6 +5,193 @@ generated at release time from each PR's `## Changelog` section, tagged by the
PR's `Type of change` (e.g. `[UI]`); the concise, curated highlights live on the
website under `/releases`.
## [v0.8.1] — 2026-08-03
- [UI] Reverted the v0.8.0 "Chat/Terminal switcher in the header" change; the
switcher returns to its previous location. (#3931)
## [v0.8.0] — 2026-08-03
- [Bug fix] Cursor YOLO sessions no longer stall piloted parents on mirrored tool-approval cards when Cursor leaves a lingering pending gate. (#2338)
- [Feature] codex-native startup-timeout errors now name the resolved provider/model routing (and the login-fallback case) instead of pointing at the runner log (#2843)
- [UI / Bug fix / Feature] Pi can now create task plans and display them in the shared Tasks panel without requiring an optional Pi extension. (#2884)
- [Feature] `detect_loop` builtin policy catches agents stuck retrying the same tool call and prompts for approval to break the loop (#3158)
- [Feature] New `detect_thrashing` builtin policy detects when an agent is stuck in a failure loop and alerts the user to intervene. (#3160)
- [Bug fix] kimi sub-agents now report completion to a parent orchestrator instead of leaving the fan-out waiting forever. (#3166)
- [UI / Bug fix / Chore / Test/CI] Conversations show their newest messages immediately, pin the latest turn below the header, and load older context smoothly near the top. (#3228)
- [Docs / Chore / Breaking] The `run_<x>_native` launchers accept a uniform `extra_args`; the per-harness `<x>_args` keyword is deprecated and will be removed in 0.9.0. (#3244)
- [Bug fix] Intelligent routing no longer drops the first message on a new claude-native session (#3257)
- [Feature] `linux_bwrap` sandboxes and their egress rules now run on the Databricks Lakebox backend. (#3258)
- [Feature] `sandbox.kubernetes.secret_mounts` projects a Secret as a rotation-friendly read-only file volume on the runner (#3280)
- [Bug fix] Session cost panel now shows a per-model breakdown for sub-agents/second heads (e.g. Debby's GPT head, Polly's codex sub-agents) running on an unpinned codex model, instead of folding their usage into the total with no per-model entry. (#3287)
- [Bug fix] Fix `antigravity-native` readiness detection for `agy` CLI installs on macOS where OAuth credentials live in Keychain (#3289)
- [Bug fix] Fixed a race condition where deleting two admins at nearly the same time could leave a deployment with zero admins and no way to recover through the API. (#3304)
- [Docs / Chore] Resuming a goose / hermes / antigravity / qwen / opencode native session now attaches to the live TUI instead of double-posting each message through the Omnigent REPL. (#3314)
- [UI / Feature] Workspace rail now shows files and shells as editor-style tabs, opens shells in-rail instead of replacing the chat, adds a full-screen toggle, and adds a "+" menu for creating shells with a remembered type picker (#3333)
- [UI / Bug fix / Feature / Test/CI] `omnigent setup` can import OpenClaw/acpx coding agents from an auto-detected or user-selected config into the generic ACP harness picker, and `omnigent run --from-openclaw <agent>` can try one without saving it. (#3354)
- [Feature] Polly can launch supported Claude and Codex implementation children in goal mode. (#3362)
- [Bug fix] Claude sessions started with a model alias (e.g. Opus) no longer fail on gateway setups that can't pin the alias — the launch resolves to a routable model id. (#3378)
- [Feature] `omni setup` now supports signing in to Antigravity with Google OAuth through `agy`, alongside Gemini API keys. (#3391)
- [UI / Feature] Archived sessions in Settings are now grouped by date (Today, Yesterday, Previous 7/30 days, month/year) for easier browsing. (#3394)
- [UI / Bug fix] Subagent graph view nodes are now clickable and navigate to the selected session. (#3395)
- [Bug fix] Dedupe codex-native's per-session plugin cache to reclaim disk (#3401)
- [Bug fix / Test/CI] Shared-session sub-agent completion notices are attributed to the collaborator who dispatched the sub-agent. (#3409)
- [Bug fix / Test/CI] Codex sessions keep their selected permission mode when resumed on a replacement host. (#3411)
- [Bug fix] Only session owners can approve tools that run with owner credentials in shared sessions. (#3416)
- [Bug fix] Fixed a bug where a malformed or unrecognized tool-call payload on certain harnesses (notably OpenCode) could silently bypass a configured tool-call policy instead of being blocked or asked. (#3418)
- [Bug fix / Feature / Docs / Test/CI] ACP agents can opt out of Omnigent’s MCP relay with `omnigent_mcp: false`, enabling compatible OpenClaw Gateway ACP registrations. (#3420)
- [Feature / Docs] Shared-session agents can distinguish who wrote each message without changing whose credentials execute the session; operators can hide model-visible author labels with an environment flag. (#3422)
- [Bug fix] `/model` and the startup header no longer name an Omnigent provider and model for ACP-backed sessions, which run on the agent's own auth and model. (#3431)
- [Bug fix] Session tokens saved by `omnigent login` are now created owner-only, so a JWT is never briefly world-readable on first login, and an interrupted write no longer discards every stored token. (#3441)
- [Feature / Docs / Chore / Test/CI] Model selection can now represent provider choices through stable intents and normalized capability metadata. (#3443)
- [UI / Feature] Session owners can grant trusted collaborators permission to approve privileged actions without transferring ownership; ordinary editors can reject but cannot approve. (#3446)
- [Bug fix / Feature / Docs / Chore / Test/CI] Default model selection now follows the active provider catalog instead of release-specific model names baked into runtime harnesses, while retaining general-purpose selection policy and Databricks gateway routability. In cold-cache Databricks environments without GitHub egress, configure `executor.model` or a provider `models.default`. (#3448)
- [Feature / Docs / Chore / Test/CI] Smart routing now chooses only from models discovered on the active runner instead of falling back to release-specific model names. (#3450)
- [Feature / Docs / Chore / Test/CI] The Kiro model picker now follows the models and metadata reported by the installed Kiro CLI. (#3452)
- [Feature / Docs] Minimal `omnigent run` agents now discover their default model instead of using a release-specific built-in endpoint. (#3455)
- [Feature / Docs / Test/CI] Provider setup and unconfigured provider runtimes now select current catalog models instead of release-specific built-in defaults, and fail with explicit configuration guidance when discovery is unavailable. (#3456)
- [Docs / Chore] The Kimi launcher example now respects the default model configured in Kimi Code. (#3457)
- [Bug fix] `omni resume` now accepts a conversation id pasted with stray punctuation (trailing period, quotes, backticks) instead of crashing (#3465)
- [UI / Bug fix] Sidebar New session, Automations, and Inbox icons now sit on the same left column (#3468)
- [Chore] Android app now targets Android 16 (API 36) to stay compliant with Google Play's (#3470)
- [Feature / Test/CI] Nightly prerelease builds: every night a `X.Y.Z.devYYYYMMDD` tag of main is cut automatically; install or update with `scripts/update_nightly.sh` or `omni upgrade --nightly` (#3475)
- [Bug fix / Breaking] Agent CLIs (qwen, goose, kimi, hermes, and generic ACP agents) no longer receive unrelated host credentials such as cloud tokens and other providers' API keys; an agent that authenticates from a variable outside its own family now declares it in `os_env.sandbox.env_passthrough`. (#3479)
- [UI / Bug fix] Conversation sidebar text now scales with the Interface font size setting (#3480)
- [Bug fix] Fixed two `claude-sdk` steering bugs: messages sent during an active turn were answered one turn late (a permanent chat desync), and steering several messages at once dropped all but the last. Steered messages are now buffered correctly and all of them reach the model. (#3484)
- [Bug fix] Fix the first message being silently dropped when a session resumes on a managed sandbox / lakebox (#3488)
- [UI / Bug fix] The workspace file view keeps its scroll position — both the file tree and the open file — when you switch between sessions (#3490)
- [UI / Bug fix] Deleting a pinned session now removes it from the sidebar's Pinned section immediately, and sidebar rows no longer grow or shift while being deleted or renamed (#3492)
- [Chore] Faster initial load — syntax-highlighting language grammars now load on demand instead of all upfront. (#3496)
- [Bug fix / Feature] Antigravity (`agy`) sub-agents now reliably receive their first turn, get their approvals dismissed in the terminal, and stop showing as still-running after they have finished. (#3499)
- [Docs / Chore] qwen-native's terminal-start error label now reads "Qwen Code" (consistent with the other native harnesses) instead of "qwen". (#3500)
- [UI / Bug fix] Terminal view scrolling now works with macOS trackpads and with TUIs that enable mouse tracking at startup (OpenCode, Claude Code) (#3510)
- [Bug fix] Runners now retry login-page redirects with refreshed credentials instead of exiting, so a hosted session survives an expired bearer (e.g. after the machine slept through a token's lifetime) and reconnects on its own. (#3511)
- [UI / Bug fix] The chat view shows the "Starting up…" spinner while a message is waking a disconnected runner, instead of nothing until the runner boots; the sidebar row shows a spinner while a session is starting up (#3514)
- [UI / Bug fix] Renaming a session in the sidebar no longer hits the wrong row when the list reorders — row order holds while the pointer is over the list or a rename is in progress (#3515)
- [UI] Collapsed tool runs in the chat view are labeled by what they did ("Ran 1 shell command, read 2 files") instead of "See N steps" (#3518)
- [Feature] Sandbox dotfile hiding is now top-level only by default (opt into the full-tree walk with `cwd_hidden_scan_recursive: true`), and `mask_paths` hides named files or folders. Untrusted-tree sandboxes that relied on recursive masking should set `cwd_hidden_scan_recursive: true` on upgrade. (#3519)
- [UI / Bug fix] Cloning a session that runs in a git worktree now pre-fills the original repo with the worktree branch (instead of the worktree path as the working directory), and the clone dialog blocks creation when the picked working directory doesn't exist on the host (#3521)
- [Bug fix] `omnigent sandbox create --provider openshell` no longer crashes against openshell SDK >=0.0.86; workspace is configurable via `sandbox.openshell.workspace` or `$OMNIGENT_OPENSHELL_WORKSPACE` (defaults to `"default"`). (#3524)
- [Bug fix] Shared-session agents distinguish speakers without treating claimed roles as authorization. (#3527)
- [Bug fix] Direct `omnigent.llms.Client` Anthropic reasoning requests now select adaptive or fixed-budget thinking from live model capabilities. (#3529)
- [Bug fix / Docs / Chore] Model context limits now follow live provider catalog maximum-input metadata instead of adding output capacity or relying on stale built-in model IDs. (#3551)
- [Test/CI] N/A — internal CI enforcement only. (#3552)
- [Bug fix] User messages no longer risk a React hook-order crash when changing from a system marker to regular content. (#3553)
- [Bug fix / Docs / Chore] Pi now routes Databricks models through the API advertised by the live model catalog instead of a release-specific model allowlist. (#3572)
- [Bug fix] Bulk conversation actions now report structured errors when only some items fail. (#3573)
- [Feature] `omnigent import --force` replaces a previously imported chat with the latest local transcript. (#3576)
- [UI / Feature] Subagent graph panel now has zoom in, zoom out, and fit-to-view buttons for easier navigation of large agent trees (#3583)
- [UI / Bug fix] Change a session's model and effort from the config gear while the session is asleep — the change is saved immediately and applies when the next message wakes it. (#3584)
- [Feature / Test/CI] Add a Locust WebSocket load test (`dev/loadtest/`) with a one-command runner and result summary (#3591)
- [Feature] Sandbox now hides dotfiles (`.env`, `.aws`, `.ssh`, ...) under `write_paths` roots, not just `cwd` and `read_paths` (#3596)
- [Test/CI] N/A — internal CI enforcement only. (#3600)
- [Feature] GitHub policy blocks destructive operations (deletes) by default across MCP tools, `git push --delete`, and `gh * delete`; opt in with `allow_destructive: true`. (#3622)
- [Feature / Chore] The Cursor model picker now follows the models advertised by your installed Cursor CLI. (#3624)
- [Feature / Docs / Chore] Pi gateway sessions now list the models currently available from the workspace instead of a release-specific bundled menu. (#3629)
- [Docs / Chore] Setup and tool help no longer recommend release-specific model ids. (#3630)
- [Feature / Docs / Chore] Static model catalog responses now identify fallback ownership and advertise the current GPT 5.6 Sol, Luna, Terra, and GPT 5.5 Codex aliases. (#3632)
- [Feature] Catalog-backed model defaults now reuse a validated last-known-good provider catalog during transient upstream outages or empty responses. (#3641)
- [UI / Feature] Redesigned the sidebar's bulk-select bar and added per-section selection: pick sessions from the flat list or from within project folders (#3677)
- [Chore] Malformed tools, retry, and MCP YAML now fails with actionable parser errors instead of leaking untyped values. (#3705)
- [Chore] N/A — internal type-safety cleanup with no user-facing behavior change. (#3706)
- [Chore] N/A — internal ASGI type cleanup with no user-facing behavior change. (#3707)
- [Chore] N/A — internal migration decoding hardening with no supported-input behavior change. (#3708)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3729)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3734)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3735)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3736)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3737)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3739)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3740)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3741)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3742)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3743)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3744)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3745)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3746)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3747)
- [Bug fix / Chore] Managed Islo hosts now receive configured provider gateways during startup. (#3748)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3751)
- [Chore] N/A — internal type cleanup with no user-facing behavior change. (#3752)
- [Bug fix] Antigravity sessions without an explicit model now follow the installed SDK's current default. (#3762)
- [UI / Bug fix] Moving a session into a project updates the sidebar instantly instead of after a multi-second wait (#3784)
- [UI / Feature] `omnigent --help` now groups launch commands under a **Harnesses** section, colorizes the output, hides harnesses whose optional extra isn't installed (with a notice pointing at `omnigent setup`), drops the duplicate `update` alias line, and tidies the command descriptions (#3795)
- [Feature] `omni upgrade` now preserves requested extras for `uv tool` and `pipx` installs, supports `--extra`, `--target-version`, and `--dry-run`, and refuses to auto-upgrade `pip` / `uv pip` installs because those installers don't record extras. (#3796)
- [UI / Chore] `omnigent --help` command descriptions are tidied (harness rows read "Launch <Name> with Omnigent"), and the duplicate `update` alias line is hidden from the listing (#3797)
- [Bug fix] Prevent `list_files` from running without a conversation scope. (#3804)
- [Bug fix] Reject stale harness continuation requests that cannot resolve an agent model. (#3806)
- [Chore] N/A (internal type cleanup) (#3830)
- [Chore] N/A (internal type cleanup) (#3831)
- [Chore] N/A (internal type cleanup) (#3832)
- [Chore] N/A (internal type cleanup) (#3833)
- [Chore] N/A (internal type cleanup) (#3835)
- [Chore] N/A (internal type cleanup) (#3836)
- [Chore] N/A (internal type cleanup) (#3837)
- [Chore] N/A (internal type cleanup) (#3838)
- [UI / Bug fix] New sessions created inside a project appear under that project immediately instead of briefly showing under "Sessions" (#3869)
- [Bug fix / Breaking] The "GitHub Repo & Branch Access" and "Block Working Directory & Worktree (#3888)
- [Feature] claude-native sessions now derive their Working/idle status from Claude Code's own session file for faster, more accurate turn-edge detection (falls back to the terminal watcher on older Claude versions) (#3906)
- [UI] Tightened the sidebar's spacing so nav, sections, and session rows sit on a consistent vertical rhythm (#3908)
- [UI / Bug fix] Sidebar rows no longer stay highlighted in "Select sessions" mode unless explicitly selected (#3912)
- [UI / Feature] Opening a shell on a sleeping session now wakes its runner automatically instead of failing with "no runner available" (#3919)
- [Bug fix] Pi-native sessions now retain Omnigent system tools and the comment relay. (#3920)
- [Chore] N/A — no user-facing behavior change. (#3923)
- [Bug fix] Fixed a leak where native Codex sub-agents left orphaned `codex app-server` processes after idle reaping, TUI exit, runner shutdown, or a hard host/runner death (#3925)
- [Bug fix / Chore] OpenCode-native model options now fall back to the authenticated server catalog when CLI discovery fails. (#3926)
- [UI / Bug fix] The sidebar's My sessions / Shared with me switch stays visible while bulk-selecting sessions (#3927)
- [Feature] `omnigent diagnose` prints a secret-free environment snapshot (CLI/server versions, OS, auth mode) for bug reports (#3928)
- [UI / Bug fix] Hide the empty Projects header menu (⋯) when you have no projects (#3930)
- [UI] Moved the Chat/Terminal switcher for terminal-first sessions from the composer into the session header (#3931)
- [UI / Bug fix / Feature] Recent conversations reopen instantly while catching up without reordering live response output. (#3932)
- [Bug fix] `pip install` / `uv sync` no longer hangs when pnpm is provided by a corepack (#3986)
## [v0.7.0] — 2026-07-27
- [Bug fix] Hermes thinking now appears in mirrored web conversations. (#1645)
| Bug Report | `bug`, `needs-triage` | Description only | Component, repro steps, version, OS |
| Feature Request | `enhancement`, `needs-triage` | Problem/use case only | Proposed solution, alternatives |
| Feature Request | `Feature`, `needs-triage` | Problem/use case only | Proposed solution, alternatives |
Questions redirect to GitHub Discussions (via `config.yml` contact link) - they aren't actionable work and would clog the issue tracker. Blank issues enabled for anything that doesn't fit a template.
@@ -74,7 +74,7 @@ A maintainer only sees issues that the bot could not fully resolve. The escalati
- **`P0-critical` / `P1-high`** - always escalated; exempt from stale bot
- **`needs-triage` still present** - bot wasn't confident enough to classify
- **Duplicate contested** - reporter reacted 👎 on the duplicate comment
Maintainers work from a filtered view: `is:issue is:open label:P0-critical,P1-high,needs-triage -label:stale`. Everything else is either being handled by the bot/lifecycle or picked up by contributors.
@@ -137,7 +137,7 @@ Duplicates get a 3-day grace period. Reporter can react 👎 to prevent closure.
| Category | Labels | Purpose |
|---|---|---|
| **Type** | `bug`, `enhancement`, `documentation` | What kind of issue |
| **Type** | `bug`, `Feature`, `Docs` | What kind of issue |
(omnigent's PyPI release, which already checks out `omnigent-ai/omnigent`
cross-org). Both require SAML SSO to view.
Two existing workflows in the secure repo are the reference: the Databricks VS
Code extension publish flow (the one to adapt) and omnigent's PyPI release
(which already checks out `omnigent-ai/omnigent` cross-org). Maintainers: the
repo name and workflow paths are in the maintainer release runbook.
## Steps to release
@@ -83,7 +79,7 @@ The one-time setup that makes this possible is tracked below.
| 3 | Verify the build: `pnpm install --frozen-lockfile && pnpm run build && pnpm run package` → valid `.vsix` | local / CI | — (done) |
| 4 | Release-PR workflow bumps version + CHANGELOG; a manually-dispatched release workflow builds the `.vsix` and attaches it (+`.sha256`) to a draft GitHub release | `.github/workflows/vscode-release-pr.yml`, `vscode-extension-release.yml` | — (done) |
| 5 | Ask DECO to register `omnigent-vscode` under the `databricks` publisher + add dedicated `OMNI_VSCE_TOKEN` / `OMNI_OVSX_PAT` secrets (and an `omnigent-vscode-marketplace` environment for the reviewer gate) | Slack `#dev-ecosystem-discuss` ([https://databricks.slack.com/archives/C01KSAWFXG8/p1782971196701749](https://databricks.slack.com/archives/C01KSAWFXG8/p1782971196701749)) | human approval |
| 6 | Add an `omnigent-vscode.yml` publish workflow in the secure repo, adapting the existing [`databricks-vscode.yml`](https://github.com/databricks/secure-public-registry-releases-eng/blob/main/.github/workflows/databricks-vscode.yml) (SAML SSO required) — it already does download → scan → `vsce publish` + `ovsx publish` in one workflow | `secure-public-registry-releases-eng` | DECO grant (step 5) |
| 6 | Add an `omnigent-vscode.yml` publish workflow in the secure repo, adapting the existing Databricks VS Code extension workflow — it already does download → scan → `vsce publish` + `ovsx publish` in one workflow | the internal secure-release repo | DECO grant (step 5) |
| 7 | Populate the dedicated `OMNI_VSCE_TOKEN` + `OMNI_OVSX_PAT` secrets; register rows in `go/npp-release-status`; get sign-off in `#unblock-releases-public` | secure repo + Slack | steps 5–6 |
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.