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
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
* 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>
* refactor(pi): discover inner gateway models live
Replace the seven-model Databricks registry embedded in the inner Pi executor with the workspace's Unity Catalog model-service listing.
Enrich live entries with MLflow context and output limits when available, while retaining the selected-model registration path so catalog outages do not prevent a configured session from launching.
Expose normalized max-output metadata, cover live routing and offline behavior, and ratchet all seven Pi entries out of the hardcode baseline.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(pi): normalize selected catalog aliases
Rewrite a live Unity Catalog alias to the exact configured Pi launch selector before rendering models.json. This keeps the menu deduplicated without dropping the concrete id Pi must resolve at startup.
Also document why explicit selections bypass picker compatibility filtering, remove a stale static-list reference, and make scalar metadata precedence explicit. Preserve live token metadata in the alias regression test.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
The first message is silently ignored (sandbox/lakebox wake) or
double-processed (managed relaunch) because of a race between the
server's persist-before-forward invariant and the runner's
crash-recovery turn detection.
When the server calls session-init (POST /runner/v1/sessions) immediately
before forwarding a message — managed sandbox wakes, sub-agent binding
repairs, host relaunches — the runner loads history during create_session.
Since the server already persisted the message to DB (invariant I1), the
runner sees it as a pending user message and starts a crash-recovery turn.
The subsequent message forward then arrives to an occupied _active_turns,
gets buffered, and is processed a second time once the recovery turn
finishes.
Add suppress_recovery_turn to the session-init envelope. The server sets
it True whenever it calls session-init as part of the message-forward
flow, so the runner skips recovery-turn detection and the forward is the
sole trigger for the turn.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* docs(models): remove stale runtime model examples
Describe Bedrock inference profiles, routing policy inputs, and child-session overrides in provider-neutral terms instead of recommending release-specific model ids in runtime help.
Ratchet the five corresponding hardcode-baseline entries and document that concrete examples belong in tests or provider-owned documentation, where they cannot become stale runtime guidance.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* docs(models): retain Bedrock id shape guidance
Keep the setup prompt provider-neutral while showing the non-obvious inference-profile identifier shape. The hint uses placeholders instead of a release-specific model id, so it remains useful without becoming stale or expanding the hardcode baseline.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* Document provider-neutral model id shapes
Restore useful model-format guidance with synthetic, non-release examples in Bedrock setup, routing policy, and child-session help. Keep concrete release ids out of runtime text so examples teach syntax without becoming stale recommendations.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Add an `allow_destructive` parameter (default `False`) to the GitHub
policy that separately gates irreversible destructive operations
(deletes). Normal writes (create, update, push) are still governed
by `write_repos` / `write_branches`; destructive operations require
BOTH being in `write_repos` AND `allow_destructive=True`.
Destructive operations gated:
- MCP: delete_file, delete_branch, delete_release
- Shell git: git push --delete, git push origin :branch
- Shell gh: delete actions across 13 groups (repo, release, issue,
gist, cache, codespace, project, variable, ssh-key, gpg-key,
secret, label, run)
For MCP, the destructive check fires after the repo allowlist so a
destructive op on a non-allowed repo still gets the repo DENY. For
shell ops, the destructive DENY fires early since even an
undeterminable-repo destructive op should be DENY not ASK.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(sessions): reject undeclared sub_agent_name at create (#3526)
POST /v1/sessions persisted an arbitrary `sub_agent_name` with no check
that the parent's spec declares it. Every downstream site that swaps in
the resolved child spec is guarded by `if ... is not None` with no
`else`, so a name that resolves to nothing left the parent spec, workdir,
harness and instructions in place — silently booting the child as a full
clone of the parent (runaway recursion for an orchestrator), with nothing
logged and nothing failing.
Fail loud at the create route: `_require_declared_subagent` loads the
trusted parent bundle and rejects a name the spec does not declare with
404, before any row is persisted. This mirrors normal `sys_session_send`
dispatch and the AGENTSPEC.md contract that unlisted names are rejected.
The check only fires when the bundle loads and the name is positively
absent; a load failure or absent cache cannot prove the negative and is
left to fail-loud downstream.
Defense-in-depth: the four runner spec-swap sites now log a warning on a
resolve-miss (`_warn_unresolved_sub_agent`) so stale rows or post-create
bundle edits that still reach the fallback are diagnosable instead of
invisible.
Test: test_subagent_create_rejects_undeclared_name asserts the create
route 404s on an undeclared name.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test: declare sub-agents in tests that create children (#3526)
The new create-time gate rejects a `sub_agent_name` the parent spec
doesn't declare, which broke existing tests that spawned children of a
sub-agent-less parent:
- test_sessions_endpoints.py: two external-status tests created a
`worker` child of the default (no-sub-agent) agent. `create_test_agent`
now takes `sub_agents`; both declare `worker`. `build_agent_bundle`
gives each bundled sub-agent a default `claude-sdk` harness (the strict
spec_version:1 parser requires one for an omnigent executor).
- e2e_ui/conftest.py: the `hello_world` fixture now declares a
`researcher` sub-agent inline, so the mobile-workflow and
subagent-tab-title fixtures can spawn a `researcher` child.
Full tests/server/integration/ suite passes (995 passed, 3 xfailed).
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The changelog on main skipped from v0.5.0 to v0.7.0, missing both
released tags. Backfill v0.6.0 and v0.5.1 in version order, and remove
the orphaned [Unreleased] block (its two entries — the Nord theme #2561
and per-harness command overrides #2933 — are already covered by the
v0.6.0 section).
v0.6.0 entries are cleaned from the auto-drafted PR #2960: dropped
non-entries (placeholder "written by Isaac" lines, "DELETE THIS SECTION"
markers, N/A refactor/cleanup notes), de-duplicated entries already
recorded under v0.5.0 (#1835, #2371), and normalized doubled tag
prefixes. v0.5.1 is from PR #2395.
Supersedes and closes#1843, #1897, #2395, #2960.
Co-authored-by: Isaac
* fix(runner): evaluate PHASE_TOOL_CALL policy for sys_call_async background tasks
Out-of-turn sys_call_async dispatches run in a detached asyncio task after
the originating turn ends. The executor adapter's _stable_policy_evaluator
reads _current_ctx which is cleared to None by run_turn's finally block, so
PHASE_TOOL_CALL evaluations always fail closed to DENY regardless of the
configured policy.
Fix by evaluating PHASE_TOOL_CALL directly via the AP server's REST endpoint
before executing the background tool. This bypasses the SSE round-trip (which
requires a live turn stream) and instead calls POST /sessions/{id}/policies/evaluate
inline from _bg(). ASK is treated as DENY since there is no active turn to
surface an approval prompt.
Sessions without a server_client or conversation_id (e.g. tests) skip
evaluation, preserving existing behavior.
Fixes#3233.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): pass arguments as dict in async PHASE_TOOL_CALL evaluation
The initial commit sent target_args (a JSON-encoded string) as the
arguments field. Every other PHASE_TOOL_CALL evaluation path sends a
dict, and the server's policy context builder + built-in safety policies
(e.g. argument-aware rules that inspect arguments.command) expect a dict.
Sending a string caused isinstance(args, dict) checks to fail silently,
so argument-scoped DENY/ASK policies couldn't inspect the async tool's
arguments.
Parse target_args into a dict before building the evaluation body, with
a fallback to {} for malformed input. Add a test assertion that verifies
the forwarded arguments are a dict with the correct contents.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* docs(runner): clarify ASK parking behavior in async policy evaluator docstring
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(claude-sdk): stop claiming live message queue support
ClaudeSDKExecutor.enqueue_session_message() called query() which queues
a new turn on the SDK's stdin rather than injecting into the active turn.
Returning True from this method caused the adapter to emit
injection.consumed, dropping the runner's buffered copy. The next user
message would then trigger a turn with an empty buffer, answering the
previous message — producing a permanent one-turn-behind desync.
Fix: return False from both enqueue_session_message and
supports_live_message_queue. The adapter's existing if-not-accepted
branch retains the message and delivers it as a normal continuation turn
once the active turn ends, preserving in-order delivery.
Closes#3472.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(lint): suppress ARG002 for unused-but-required override params
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(claude-sdk): send all batched steered messages, not just the last
When a user steers multiple messages during a running SDK turn, each is
buffered and the runner collapses them into one continuation turn whose
history ends in several consecutive user messages. On a resumed SDK
session _build_prompt called _extract_latest_user_content, which walks
history in reverse and returns only the FIRST user message it finds — so
the SDK saw just the last steered message and the earlier ones were
silently dropped (they remained in the transcript, making it look like
the second message was "ignored").
Add _extract_trailing_user_content: on resume, collect the whole trailing
run of consecutive user messages (those after the last assistant/tool
message) and concatenate them (blank-line joined for text; merged content
blocks when any message is multimodal). Prior turns stay SDK-cached and
are not replayed.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(models): resolve supervisor wizard defaults
Replace the legacy multi-agent supervisor wizard's OpenAI and Databricks model pins with provider-catalog suggestions while preserving the free-form model prompt.
Unknown custom endpoints now receive no unrelated vendor default and require an explicit model. Add endpoint-specific coverage and remove both wizard entries from the hardcoded-model baseline.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(onboarding): require a supervisor model
Reject empty supervisor model input before generating an openai-agents spec. Custom endpoints must now provide an explicit model, and known providers fall back to operator input if their catalog has no default.
Keep the user on the model-selection step with a clear validation message and cover both custom-endpoint and empty-catalog retries.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* docs(onboarding): map supervisor provider branches
Document how the helper's profile, default OpenAI, and custom-endpoint states correspond to the wizard menu. This makes the explicit-input fallback clear when future endpoint choices are added.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
#2976 moved git untracked-cache setup off the runner startup path into a
daemon thread. The worker now shells out to git at an arbitrary moment, so
it can land inside a test that has swapped the process-global
subprocess.run and be recorded as one of that test's own calls.
That is how it failed CI on an unrelated PR: the databricks login test
asserts on the argv it captured and instead saw a stray
`config core.untrackedCache true`.
Stub GitFilesystemRegistry.start for the suite by default, with an
untracked_cache_start fixture for the worker's own tests, and harden the
login recorder so foreign argv reaches the real runner rather than the
capture list.
Signed-off-by: Ross Sclafani <rsclafani@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1 of the modular native-harness registry refactor landed (10 PRs,
2026-07-28 → 07-31). Bring the design doc in line with what actually shipped:
- Status header, Phase 1 subtotal, effort summary, and bottom line updated from
forward-looking ('1.1–1.3 in review') to Phase 1 complete / Phase 2 next.
- Ledger: 1.8 (#3648) landed; 1.4 marked descoped (with rationale); the 1.7
opencode-e2e follow-up (#3656) recorded; per-PR merge dates added.
- Calibration rewritten as a Phase 1 retrospective: estimate (~20–29 eng-days)
vs. actual (10 PRs / 4 calendar days), the real cost centers (test-shape churn
+ review-caught behavior bugs, enumerated per PR), the correct runner re-scope,
the two intentional behavior deltas (qwen label, antigravity relay), and the
recurring uv.lock / full-suite-only-flake operational friction.
Doc-only; no code change.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* feat(models): discover Cursor picker models from CLI
Replace the generated Cursor base-model catalog with live cursor-agent models discovery on the bound runner. Normalize compound effort variants and legacy dotted Claude spellings into the base-id namespace used by launch, /model switching, and terminal mirroring.
Route the discovered options through the existing session model-options cache, return retryable failures without blocking Cursor launch, and expose the same live listing to model-catalog callers. Remove the obsolete generator and eleven Cursor hardcode baseline entries.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): preserve valid Cursor picker options
Carry forward round-trip protection without restoring the stale generated denylist. Skip and log reversed Claude ids that the Cursor command parser cannot inject, while retaining CLI-advertised models verified against the current agent.
Keep cached picker options visible during asynchronous refreshes so model or effort changes do not transiently blank Cursor's picker. Cover both the parser guard and refresh behavior with regression tests.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): verify exact Cursor picker matches
Resolve the selected model's display name from the same live CLI catalog that supplies the Web picker, then refuse to press Enter unless Cursor highlights that exact row. This prevents fuzzy matching from silently selecting another model without restoring release-specific denylist entries.
Keep default/current tags unique in catalog order and limit stale-option retention to Cursor refreshes, preserving the existing drop-on-refresh behavior for Codex and other runner-backed pickers.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(models): centralize owned static fallbacks
Move the remaining Claude and Codex release-curated aliases into one fallback module. Record the responsible adapter, catalog provenance, and the concrete discovery gap for every registered fallback.
Carry that metadata through ModelListing and expose it in sys_list_models payloads, while preserving existing model ordering and provider behavior. Move—not expand—the nine lint baseline entries so future fallback edits remain explicit.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(cursor): reuse live model metadata when switching
Cache Cursor model display names when the runner serves the live picker catalog and pass the selected label into the TUI bridge. This avoids spawning cursor-agent models a second time for the same selection while retaining live discovery as a cold-cache fallback.
Also require the highlighted picker row to match the complete normalized display label or one of its suffixed variants, preventing similarly prefixed models from being accepted. Clear the cache with the existing session and agent lifecycle caches and cover cached, cold-cache, and fuzzy-match behavior.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(models): key fallbacks by provider constants
Use the canonical subscription and CLI-config kind constants as static fallback registry keys. This removes a silent coupling where renaming a provider kind could otherwise turn an owned fallback into an empty listing.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* lint(models): enforce owned fallback boundary
Allow unavoidable static model aliases only when AST analysis proves they are confined to complete StaticModelFallback records in the central model_fallbacks module. Require literal owner, provenance, and discovery-gap metadata, and reject fallback tuples reused outside those records.
Remove the nine centralized fallback rows from the count-based baseline while retaining the temporary baseline for independent migrations that have not landed yet. Add focused positive and bypass-resistance tests and document the structural exception.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* test(models): scan the owned fallback registry
Run the structural hardcode scanner against the production model_fallbacks module, proving the real stacked records satisfy the owned fallback boundary without count-based allowances.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* test(models): require the fallback registry
Make the production-registry lint assertion fail if model_fallbacks.py is missing instead of passing vacuously. Clarify that only module-level literal tuples qualify for the structural exemption so nested aliases intentionally fail closed.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* Update Codex fallback aliases
Replace the stale Codex fallback entries with the current GPT 5.6 Sol, Luna, and Terra aliases. Keep the change scoped to Codex; no Gemini fallback is introduced.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Creating a project against a container-deployed server failed with 405.
create_app mounts the projects router only when a project store is wired,
and the Docker entrypoint built every other store but never this one — so
POST /v1/projects was not a route at all and fell through to the SPA
catch-all (GET-only), which answers 405. The CLI server path already wires
it, so the same build worked under `omnigent server start` and failed in
the container.
Construct SqlAlchemyProjectStore from the resolved database URL and pass it
to create_app, mirroring the other stores.
Signed-off-by: apeltekci <andrew@peltekci.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Final Phase-1 PR of the modular native-harness registry refactor: move
registry-parallel enumerations onto HarnessCapabilities.
- Add a fork_history axis (ForkHistory enum: none/rebuild/preamble) to
HarnessCapabilities, declared per harness in _BUILTIN_CAPABILITIES. Derive the
server's two fork-history gating frozensets in _sessions/common.py from it
instead of hand-listing. The derivation emits each canonical id plus its
reversed native-<key> spelling, because native-claude/native-codex/native-cursor
are valid ids canonicalize_harness passes through unchanged and the read sites
match on the canonicalized id (guarded by the existing reversed-spelling fork
test) — so the derived sets are a superset of the prior literals.
- Add optional shell_tool_name / shell_tool_prompt fields carrying the harness
bench's shell-tool provocation; delete the bench's hardcoded
_NATIVE_TOOL_PROVOCATION table and read the fields off capabilities in
native_vendor() (byte-identical (tool_name, prompt) per harness).
- Delete the dead _HARNESS_MODULES literal in runtime/harnesses/__init__.py
(~120 lines, overwritten unconditionally by harness_modules() next line).
- Extend the drift-guard tests in test_harness_capabilities.py.
Scope kept tight to the doc's mandate: sets that would need new NativeCodingAgent
identity fields (_ANTIGRAVITY_FAMILY_HARNESSES, _PROVIDER_RESOLUTION_HARNESS,
*_NATIVE_TERMINAL_ROLE) are left as-is; noted as follow-ups.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* feat(models): discover Cursor picker models from CLI
Replace the generated Cursor base-model catalog with live cursor-agent models discovery on the bound runner. Normalize compound effort variants and legacy dotted Claude spellings into the base-id namespace used by launch, /model switching, and terminal mirroring.
Route the discovered options through the existing session model-options cache, return retryable failures without blocking Cursor launch, and expose the same live listing to model-catalog callers. Remove the obsolete generator and eleven Cursor hardcode baseline entries.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): preserve valid Cursor picker options
Carry forward round-trip protection without restoring the stale generated denylist. Skip and log reversed Claude ids that the Cursor command parser cannot inject, while retaining CLI-advertised models verified against the current agent.
Keep cached picker options visible during asynchronous refreshes so model or effort changes do not transiently blank Cursor's picker. Cover both the parser guard and refresh behavior with regression tests.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): verify exact Cursor picker matches
Resolve the selected model's display name from the same live CLI catalog that supplies the Web picker, then refuse to press Enter unless Cursor highlights that exact row. This prevents fuzzy matching from silently selecting another model without restoring release-specific denylist entries.
Keep default/current tags unique in catalog order and limit stale-option retention to Cursor refreshes, preserving the existing drop-on-refresh behavior for Codex and other runner-backed pickers.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(models): centralize owned static fallbacks
Move the remaining Claude and Codex release-curated aliases into one fallback module. Record the responsible adapter, catalog provenance, and the concrete discovery gap for every registered fallback.
Carry that metadata through ModelListing and expose it in sys_list_models payloads, while preserving existing model ordering and provider behavior. Move—not expand—the nine lint baseline entries so future fallback edits remain explicit.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(cursor): reuse live model metadata when switching
Cache Cursor model display names when the runner serves the live picker catalog and pass the selected label into the TUI bridge. This avoids spawning cursor-agent models a second time for the same selection while retaining live discovery as a cold-cache fallback.
Also require the highlighted picker row to match the complete normalized display label or one of its suffixed variants, preventing similarly prefixed models from being accepted. Clear the cache with the existing session and agent lifecycle caches and cover cached, cold-cache, and fuzzy-match behavior.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(models): key fallbacks by provider constants
Use the canonical subscription and CLI-config kind constants as static fallback registry keys. This removes a silent coupling where renaming a provider kind could otherwise turn an owned fallback into an empty listing.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* Update Codex fallback aliases
Replace the stale Codex fallback entries with the current GPT 5.6 Sol, Luna, and Terra aliases. Keep the change scoped to Codex; no Gemini fallback is introduced.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Follow-up to #3599 (PR 1.7). That PR moved the built-in native agent-name
constants into a shared public block in omnigent/native_coding_agents.py and
migrated the claude/codex host e2e tests onto them, but missed the opencode
sibling: test_host_opencode_native_e2e.py still defined a local
_OPENCODE_NATIVE_AGENT_NAME = "opencode-native-ui" literal and asserted a stale
'_ensure_default_opencode_agent did not run' message (that per-harness seeder
was collapsed into _ensure_default_native_agents).
Import the shared OPENCODE_NATIVE_AGENT_NAME constant and update the message so
all three host e2e tests are consistent. Test-only; opt-in e2e (skipped without
OMNIGENT_E2E_OPENCODE_NATIVE=1).
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(harness): registry-driven server seeding loop (PR 1.7)
Collapse the server's built-in native-agent seeding onto the
NativeHarnessProvider seam. The 11 hand-written _ensure_default_<x>_agent
helpers + their 11 _build_<x>_native_bundle partners become two
registry-driven functions in omnigent/server/app.py:
- _build_native_bundle(provider): resolves provider.materialize_agent_spec via
the seam and runs the shared materialize -> bundle -> tar dance. The
per-harness `model` arg variance (codex required kw / kiro,opencode default /
the rest none) is bridged by one inspect.signature check.
- _ensure_default_native_agents(...): loops NATIVE_CODING_AGENTS, resolving the
provider by key and seeding each content-aware via _ensure_builtin_agent.
debby / polly / _ensure_extra_builtin_agents stay hand-written. Removed the now
-dead _<X>_NATIVE_AGENT_NAME constants and the *_NATIVE_CODING_AGENT imports.
Net server/app.py -455/+146.
Redeploy safety: builtin_agent_id(name) is a pure hash of the agent name, and
the names (NativeCodingAgent.agent_name) and bundle bytes are unchanged, so
seeded ids and bundles stay byte-identical (verified: sha256 of
_build_native_bundle output matches the pre-loop named builders across all
model-arg variants). New tests freeze the 11 expected ids and assert the loop
covers every native agent. Updated test_builtin_bundles / test_app to the
generic builder; fixed stale symbol refs in two e2e tests and a scheduled-tasks
integration test.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* test(server): cover the registry-driven native seeding paths (PR 1.7)
The seeding-loop collapse removed ~330 lines that were only exercised
transitively by e2e suites; add direct unit coverage so the new generic path is
fully covered and the coverage gate recovers:
- Parametrize the native bundle-builder tests over EVERY native agent (was a
4-agent sample), so each harness's _materialize_* + bundle path is covered
directly, across both model-arg shapes.
- Cover the two defensive guards in _build_native_bundle /
_ensure_default_native_agents (missing materialize hook, missing provider row).
- Add an end-to-end seed test asserting all 11 native agents register under
their stable builtin_agent_id with a retrievable bundle.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* docs(server): note the model-axis limit of the native seed signature bridge
Address Polly non-blocking note: the inspect.signature bridge in
_build_native_bundle understands only the `model` kwarg; a future harness
whose materializer needs a different required kwarg fails loud at seed time
rather than routing. Comment so the next author knows.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(harness): aggregate built-in native agent-name constants (PR 1.7)
The seeding-loop collapse deleted the 11 private _<X>_NATIVE_AGENT_NAME
constants from server/app.py (the loop uses agent.agent_name directly), which
pushed callers that need one specific built-in onto magic-string literals
("claude-native-ui", "qwen-native-ui", ...) in the tests.
Restore them as PUBLIC constants in omnigent/native_coding_agents.py — the
module that already indexes the registry rows — so seeding and tests share one
named, registry-derived source of truth instead of re-deriving the literal:
- Add CLAUDE_NATIVE_AGENT_NAME ... KIMI_NATIVE_AGENT_NAME (each = the row's
agent_name) to native_coding_agents.
- Point the server + scheduled-tasks tests at the shared constants (drop the
bare "qwen-native-ui" / "antigravity-native-ui" / "claude-native-ui" strings).
- Fold the two host e2e tests' own local _CLAUDE/_CODEX_NATIVE_AGENT_NAME
literals onto the shared constants too.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
Two localized optimizations to SqlAlchemyConversationStore.list_items,
which backs GET /v1/sessions/{id}/items (the web chat transcript read).
- Scope the after/before cursor subqueries to conversation_id so they
land on the (workspace_id, conversation_id, id) primary key as point
lookups. Without it, (workspace_id, id) leads no index and each
paginated page degraded to a workspace-wide scan.
- load_only the seven columns _to_item reads, dropping the wide
search_text Text column that this read path never touches. On
Postgres search_text is TOAST-ed, so omitting it skips a detoast and
roughly halves the bytes pulled per row on a chatty conversation.
Scoping the cursor to the conversation also fixes a latent correctness
edge: a cursor id from another conversation previously resolved its
position workspace-wide and applied it as a cutoff; it now yields an
empty page, guarded by a new test.
Co-authored-by: Isaac
* fix(cli-auth): write the session-JWT file privately instead of chmod-ing it after
_store_entry's docstring already promised the file is written "with user-only
read/write permissions (0o600) - the file may hold session JWTs, which are
sensitive". The implementation did not deliver that:
path.parent.mkdir(parents=True, exist_ok=True)
...
path.write_text(json.dumps(data, indent=2))
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
write_text creates a missing file at the process umask, so on the very first
login - exactly when a session JWT is first persisted - the token sat on disk
readable by every local user until the chmod landed. Measured:
dir mode after mkdir : 0o755
file mode after write_text: 0o644 <- JWT is on disk at this mode
file mode after chmod : 0o600
The parent ~/.omnigent was also left world-traversable, and clear_token
rewrote the same file with no chmod of its own, relying on the mode of a file
it may not have created.
Routes both writers through _write_tokens_file, mirroring the pattern already
used in claude_native_bridge._atomic_write_user_json: a tempfile beside the
target (created owner-only by tempfile before any bytes are written), fsync,
chmod, then os.replace. The directory is created 0o700.
The rename also fixes a robustness bug: write_text truncated in place, so a
write that failed partway left a truncated file, and the JSONDecodeError
handler in _store_entry treats that as {} - silently discarding every stored
token for every server. The temp is discarded on failure and the previous file
is left intact.
Tests: tests/test_cli_auth_token_file_mode.py. Three of the eight fail on the
previous code (the on-disk window, the directory mode, and token loss on a
failed write); the rest pin the final mode, round-tripping, trailing-slash
normalisation and selective clearing.
Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
* style: satisfy ruff format
Pre-commit's ruff-format hook flagged the skipif decorator in the new test
module; it fits on one line.
Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
* refactor(cli-auth): hoist state-dir hardening into _write_tokens_file
Move the 0o700 mkdir + chmod from _store_entry into _write_tokens_file
so every writer routes through it. Previously only _store_entry
hardened the directory, so a clear_token-only interaction left a
pre-existing world-traversable (0o755) ~/.omnigent untightened. Adds a
regression test pinning that clear_token now hardens the dir.
Co-authored-by: Isaac
---------
Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* feat(models): discover Cursor picker models from CLI
Replace the generated Cursor base-model catalog with live cursor-agent models discovery on the bound runner. Normalize compound effort variants and legacy dotted Claude spellings into the base-id namespace used by launch, /model switching, and terminal mirroring.
Route the discovered options through the existing session model-options cache, return retryable failures without blocking Cursor launch, and expose the same live listing to model-catalog callers. Remove the obsolete generator and eleven Cursor hardcode baseline entries.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): preserve valid Cursor picker options
Carry forward round-trip protection without restoring the stale generated denylist. Skip and log reversed Claude ids that the Cursor command parser cannot inject, while retaining CLI-advertised models verified against the current agent.
Keep cached picker options visible during asynchronous refreshes so model or effort changes do not transiently blank Cursor's picker. Cover both the parser guard and refresh behavior with regression tests.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): verify exact Cursor picker matches
Resolve the selected model's display name from the same live CLI catalog that supplies the Web picker, then refuse to press Enter unless Cursor highlights that exact row. This prevents fuzzy matching from silently selecting another model without restoring release-specific denylist entries.
Keep default/current tags unique in catalog order and limit stale-option retention to Cursor refreshes, preserving the existing drop-on-refresh behavior for Codex and other runner-backed pickers.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(cursor): reuse live model metadata when switching
Cache Cursor model display names when the runner serves the live picker catalog and pass the selected label into the TUI bridge. This avoids spawning cursor-agent models a second time for the same selection while retaining live discovery as a cold-cache fallback.
Also require the highlighted picker row to match the complete normalized display label or one of its suffixed variants, preventing similarly prefixed models from being accepted. Clear the cache with the existing session and agent lifecycle caches and cover cached, cold-cache, and fuzzy-match behavior.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(cursor): drop unused parser binding
Keep the model-option setdefault call for deduplication without assigning its return value before the later result loop. This addresses the code-quality finding without changing parser behavior.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(cursor): handle missing CLI during model switch
Catch click.ClickException while refreshing a cold Cursor model catalog so a missing cursor-agent executable becomes the existing handled RuntimeError instead of escaping the runner endpoint as a 500.
Add bridge-level regression coverage for the preserved exception cause. The focused Cursor/native-event suite passes 122 tests and full pre-commit passes.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* feat(telemetry): emit PolicyRegisteredEvent and PolicyDeletedEvent
Add two new telemetry events that fire on policy create/delete for
both session-level and admin-level policies:
- PolicyRegisteredEvent: fired after a successful POST to
/v1/sessions/{id}/policies or /v1/policies. Records handler,
policy_type, scope ("session" or "admin"), session_id, and
anon_user_id so we can see which handlers are being registered and
at what scope.
- PolicyDeletedEvent: fired after a successful DELETE. Looks up the
existing policy first so the handler is available; silently skips
emission when the policy was already absent (idempotent delete).
Both events follow the existing try/except BLE001 fire-and-forget
pattern used by SessionStoppedEvent and SessionDeletedEvent.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor(policy-store): return deleted Policy from delete/delete_default
Previously delete() and delete_default() returned bool, causing a
second PK lookup in the route layer to retrieve the handler before
emitting telemetry. Changing the return type to Policy | None
eliminates that extra round-trip: the store already loads the row to
perform the delete, so we can return the entity at no additional cost.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor(telemetry): drop handler from PolicyDeletedEvent, revert store changes
handler required a pre-fetch before delete to avoid an extra DB
round-trip, which meant changing the store layer. Dropping the field
keeps PolicyDeletedEvent simple and the store interface unchanged.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The host orphan reaper's waitpid fallback uses os.WNOHANG and
os.waitpid(-1, ...), neither of which exists/works on native Windows.
Windows also has no child reparenting to a subreaper, so there is
nothing to reap. The periodic sweep swallowed the resulting
AttributeError, but the final drain in run()'s finally block runs
unguarded and would crash shutdown.
Return early with 0 when os.WNOHANG is absent, matching the reaper's
own "non-Linux is a no-op" contract.
Co-authored-by: Isaac
* refactor(harness): DI-seam runner interrupt/stop — collapse 16 handlers (PR 1.6)
Route the runner's native interrupt / stop dispatch through a
dependency-injected NativeInterruptRunner instead of 16 per-harness closures
plus two hardcoded `if _harness == "<x>-native"` chains in the /events handler.
Mirrors the CodexGoalRunner DI precedent (omnigent/runner/codex/goal.py):
app-scope state (AP client, resource registry, event publisher, sub-agent wake
plumbing, codex bridge-state resolver) is injected at construction, typed via
Protocol.
- New omnigent/runner/native/interrupt.py: the 9 uniform interrupt and 7
uniform stop handlers collapse to two descriptor-driven methods
(_UNIFORM_INTERRUPT / _UNIFORM_STOP); claude interrupt (bridge-id) and codex
interrupt (MCP-startup + turn/interrupt) keep dedicated methods, moved
verbatim. interrupt()/stop() return None for handler-less harnesses so the
caller falls through to the in-process cancel.
- app.py: the two dispatch chains become one runner.interrupt()/.stop() call +
fall-through; the 16 closures are deleted (net app.py -470). Local
`from omnigent.<x>_native_bridge import` stays at call time so bridge-module
monkeypatches keep resolving (no test repoints).
- 12 new unit tests for NativeInterruptRunner.
- Doc: add 1.6 ledger row (gap-fill deferred); flip stale 1.5c row to landed.
Scope: migration-only, behavior-preserving. The antigravity/opencode coverage
gap (no interrupt/stop handler; they fall through to _cancel_inprocess_turn) is
left unchanged and pinned by a no-handler test; wiring agy interrupt_turn() /
opencode client.abort() is a deferred follow-up.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* docs(runner): fix uniform interrupt/stop harness counts in interrupt.py
Address Polly non-blocking doc nit: the module comments said 'nine uniform
interrupt' and 'seven uniform stop', but _UNIFORM_INTERRUPT has seven entries
and _UNIFORM_STOP six (claude/codex interrupt and claude stop are special-cased;
codex/pi alias stop to interrupt). Clarify uniform-vs-total counts. Doc-only.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(pi): route wire APIs from catalog metadata
Replace Pi's release-specific GPT Chat Completions allowlist with normalized Unity Catalog model-service wire metadata shared by native and inner Pi execution.
Thread generic-provider wire configuration through the harness, resolve dedicated AI Gateway URLs back to their workspace API origin, and avoid probing non-Databricks providers. When discovery is unavailable, route unknown GPT models to Responses while retaining the documented system-model compatibility fallback.
Cover Chat, Responses, dedicated-gateway, generic-provider, alias, outage-cache, and Responses-only catalog behavior. Verified 275 focused Pi/catalog tests, isolated runtime spawn-env tests, live production UC metadata, and repository-wide pre-commit.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(pi): hoist model routing imports
Move the catalog, gateway, subprocess, and compatibility imports used by Pi routing to module scope so dependencies are explicit and consistently initialized.
Extract the shared Pi model compatibility predicates into a small leaf module to avoid introducing a model_catalog/pi_native_credentials import cycle. Update tests to patch the module-bound credential resolver.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* Add agy (Google Antigravity CLI) as a 7th polly sub-agent
polly's roster now includes agy alongside claude_code, codex, opencode,
cursor, hermes, and pi. agy drives the antigravity-native harness
(Gemini-native, own Google account auth via ~/.gemini; does not run
Claude/GPT-family models) and follows the same
IMPLEMENT/REVIEW/EXPLORE contract as the other worktree-scoped
implementers, with gate_pushes: false so it can open its own PRs.
Updates the roster count, preflight check, trigger phrases, and
cross-vendor review/cancellation lists in config.yaml; the
investigate/fanout/cross-review skills' vendor lists; and the
structural e2e test assertions (roster tuple, harness family map,
policy-argument count) to match.
Signed-off-by: Imraul Emmaka <ikemmaka@ualr.edu>
* fix(antigravity-native): re-deliver turns agy rejects while verifying the account
agy's TUI composer mounts ~3s after launch, but its account-eligibility
check is not settled until ~7-9s. A turn submitted inside that window is
consumed by agy — the draft leaves the composer, so the submit verifies —
and answered with "We're finishing verifying your account eligibility"
instead of starting a cascade. Nothing retried, so the turn was silently
lost and the terminal sat idle.
Detect the notice after a submit and re-deliver until agy takes the turn,
bounded by 90s. The running-turn marker is checked first so a notice still
rendered from a prior attempt can never re-send a turn that already landed,
and the probe fails open so a future agy that renames its running footer
keeps delivering rather than retrying.
Programmatic first turns — a polly sub-agent dispatch — land in that window
on every launch; interactive users usually type slowly enough to miss it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>
* fix(antigravity-native): treat agy's collapsed-paste placeholder as a rendered draft
agy replaces a paste carrying many line breaks with a single
`[Pasted text #N +M lines]` row instead of echoing the text into the
composer. The threshold is line-count based (~13+ line breaks); total
length does not matter, so a long single-line message still renders
verbatim while a multi-line one never does.
The render gate looks for the message's needle in the composer, which a
collapsed paste can never contain, so delivery raised "agy did not render
the pasted message in its input box before submit" while the draft was in
fact sitting there. Sub-agent task prompts are exactly this shape, so a
polly dispatch failed on its first turn every time; the single-line
follow-up prompts it sent next happened to render verbatim and worked,
which made it look like a startup race.
Recognise the placeholder as draft content in _draft_in_input_region so
both the render gate and the submit verification key off it appearing and
then leaving the composer — the submit stays verified rather than blind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>
* fix(antigravity-native): bind the TUI injector to an explicit bridge dir
The interaction bridge's default TUI injector resolved the bridge directory
from HARNESS_ANTIGRAVITY_NATIVE_BRIDGE_DIR, on the assumption (stated in its
docstring) that "the reader/CLI both run with it set". That is stale: the
reader now runs as a task INSIDE the runner process, which never carries that
variable — it is set only for the harness subprocess by
build_antigravity_native_spawn_env.
So every web approval failed with "HARNESS_ANTIGRAVITY_NATIVE_BRIDGE_DIR is
required" — 100% of the time, not intermittently. The RPC delivery flipped
agy's backend step, but agy's own permission prompt was never dismissed, so
the terminal did not advance and the next typed turn risked landing in the
stale prompt's buffer.
Add tui_injector_for(bridge_dir) and have the reader — which is handed its
bridge_dir — use it. _inject_via_tui stays for callers that genuinely run
with the harness env, with its constraint now spelled out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>
* fix(antigravity-native): close a turn agy reports finished (quiescence backstop)
Turn completion was inferred purely by pattern-matching step types.
_is_turn_close_step has already accreted three special cases — clean text
close, ERROR planner, degenerate DONE — and its own docstring explains that
missing one leaves turn_active stuck True forever: the spinner never clears
and the NEXT turn cannot re-open RUNNING either. Every agy step type it does
not know about is a permanently stranded session, and that list only grows.
agy already publishes the answer. Every GetAllCascadeTrajectories summary
carries a per-cascade CASCADE_RUN_STATUS, which appeared in this codebase
exactly once — in a docstring example — and was never read, even though the
rotation detector already fetches those summaries on every scan.
Use it as a BACKSTOP: when agy reports the bound cascade idle on two
consecutive scans while Omnigent still believes a turn is open, close it. The
step-based close stays the fast path; this only catches what it missed. Being
reconciliation rather than edge detection, it is idempotent and self-healing —
a missed, unknown, or reordered step now costs one detector interval instead
of stranding the session.
Verified against agy 1.1.8 that the status reports RUNNING both while working
and for the entire time a permission gate is parked (75s observed), so the
backstop cannot close a turn that is waiting on a human. Two consecutive ticks
are required so the gap between delivering a turn and agy starting it is not
mistaken for the end of one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>
* fix(antigravity-native): avoid duplicate verification retries
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Imraul Emmaka <ikemmaka@ualr.edu>
Signed-off-by: Khoi Nguyen <khoifish@goodstorytime.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Imraul Emmaka <ikemmaka@ualr.edu>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* feat(web): add zoom controls to subagent graph panel
Add zoom in/out and fit-to-view buttons to the subagent graph panel
using ReactFlow's useReactFlow hook. Widen the zoom range from
0.3–1.5x to 0.1–3x so users can zoom in closer to read small nodes
or zoom out further for large graphs.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* style(web): fix prettier formatting for zoom control buttons
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(openshell): pass workspace to SandboxClient lifecycle methods
The openshell SDK >=0.0.86 added a required `workspace` keyword argument
to `SandboxClient.create()`, `get()`, `delete()`, and `wait_ready()`.
Omnigent never passed it, so `sandbox create --provider openshell`
crashed with `TypeError: SandboxClient.create() missing 1 required
keyword-only argument: 'workspace'`.
Thread a workspace through _OpenShellClient and OpenShellSandboxLauncher,
resolved from: explicit constructor arg (YAML `sandbox.openshell.workspace`),
then `$OMNIGENT_OPENSHELL_WORKSPACE` env var, then "default".
Fixes#3513
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix: bump openshell floor to >=0.0.88 and close test gaps
The `workspace` kwarg landed in openshell 0.0.88, not 0.0.86 — 0.0.86
still has the old signature and would crash with `got an unexpected
keyword argument 'workspace'`. Bump the floor accordingly.
Also record the workspace reaching the fake SDK and assert it in both
the _OpenShellClient and managed_hosts tests.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* chore: strip index-dependent size fields from uv.lock
pypi.org's index serves wheel/sdist sizes while proxy indexes may not,
so re-locks were flipping ~2,900 'size = N' lines back and forth. The
sizeless form is canonical on main; this keeps the diff to the real
dependency changes.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(web): keep the session config gear usable while the session is asleep
The gear required liveness === "online", so an asleep session couldn't
change model/effort even though PATCH /v1/sessions persists overrides
and the next wake applies them. Gate the gear like the composer (inert
only for read-only viewers and unreachable sessions) and make the
native model catalog survive runner death so the picker stays filled:
- relay exit / refresh_state with no runner now mark the per-session
catalog stale instead of deleting it; snapshots keep serving it
- a stale catalog is re-fetched in the background once a live runner
is bound again, and replaced on success
- an asleep claude-native session with a cold cache (server restart)
refills from its host over the host tunnel - the same pre-launch
source the new-session picker uses
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(e2e): scope the mermaid preview assertion to the diagram svg
The rendered Streamdown mermaid block carries chrome icon svgs (zoom /
copy controls) next to the diagram, so the strict single-svg locator
fails with "resolved to 3 elements" on every run since #3498 merged.
Target the diagram svg via mermaid's aria-roledescription stamp, which
also makes the assertion check the diagram itself rather than any svg.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
Follow-up to the non-blocking review notes on #3479.
- codex_executor consumes agent_env.declared_passthrough instead of keeping
its own copy. It already imports agent_env, so the reason the duplicate
existed no longer applies. Test repointed at the shared helper.
- POLICIES.md now explains that agent CLIs get a deny-by-default environment
and what env_passthrough is for. The migration note only ever lived in a PR
description, so the two cases that bite -- a generic ACP agent with no vendor
family, and a goose authenticated by an ambient provider key rather than
gateway routing -- were undocumented.
Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
pypi.org's simple index serves a size for every file while proxy
indexes may not, so each re-lock added or stripped 'size = N' across
~2,900 lines depending on which index resolved it. Make the sizeless
form canonical (the hash is the integrity check): the fixer now drops
size fields and --check flags them, so re-locks from either side
converge on one form.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
ReactFlow's pan-on-drag behavior was intercepting pointer events on
graph nodes, preventing the existing <Link> wrapper from navigating.
Adding the `nopan nodrag` utility classes tells ReactFlow to leave
those events alone so clicks reach the router link.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
Mirror the pvc_mounts config knob for Kubernetes Secrets: project a
pre-created Secret as a read-only file volume on the runner's host
container. A Secret volume (no subPath) is refreshed in place by the
kubelet, so a long-lived runner picks up a rotated credential without a
restart — unlike envFrom, which is frozen at container start.
- server: parse/validate sandbox.kubernetes.secret_mounts at config load
(DNS-1123 name, absolute/normalized/non-reserved path, intra-list and
pvc<->secret path-collision checks), failing loud at startup
- onboarding: add the secret volume + host-container-only volumeMount in
build_pod_manifest (optional=False, defaultMode 0440), threaded through
the launcher
- tests mirror the pvc_mounts coverage
Signed-off-by: bdchatham <bdchatham@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Each open session in the web UI holds a long-lived event-stream HTTP
response. Over HTTP/1.1 browsers cap concurrent connections at ~6 per
origin, so opening several windows/tabs against a raw :8000 deploy fills
the pool with held-open streams and every other request stalls — the UI
appears frozen across all windows while the server is idle.
The bundled Caddy overlay and every managed platform already terminate
TLS with HTTP/2, which multiplexes the streams and dissolves the cap;
the gap was only that nothing told operators this proxy is also the fix.
Document it in the deploy README ("Serving") and point to it from the
Caddyfile. Docs-only; no server behavior change.
Co-authored-by: Isaac
* feat(sandbox): make recursive dotfile hiding opt-in, add mask_paths
The sandbox hid every dotfile under the working directory by walking the
whole tree. On medium-to-large projects that walk is slow and routinely
trips the entry cap, and it masks far more than the secrets it targets.
Make the recursive scan opt-in and add a way to hide specific paths:
- cwd_hidden_scan_recursive (default false) scans only the top level of
the cwd and each read_paths root (including $HOME when it is a granted
read path). The top-level dotfiles that hold most secrets (.git, .env,
.aws, .ssh, ...) are still masked, but the walker no longer descends the
whole tree. Set it true for untrusted trees where a deeply nested
credential file would be an unacceptable leak.
- mask_paths hides a named file or folder regardless of a leading dot,
resolved like read_paths (~ expanded, relative to cwd, no $VAR). Files
are masked as an empty file, folders as an empty view, on top of the
dotfile mask in every mode.
Both backends enforce the new fields: linux_bwrap binds /dev/null for
files and a tmpfs for folders; darwin_seatbelt emits literal/subpath deny
rules. Behavior change: with the non-recursive default, dotfiles nested
below the first level are now readable unless recursion is turned on.
Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
* docs(sandbox): note is_dir symlink behavior for mask_paths
Clarify that the explicit mask_paths classification uses is_dir(), which
follows symlinks — unlike the dotfile walker's follow_symlinks=False — and
that seatbelt emits a harmless literal deny for a missing entry where bwrap
drops it on the re-stat.
Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
---------
Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
* perf(web): render conversations before the full history window loads
Opening /c/<id> blocked first paint on fetchInitialHistoryWindow, which
pages backward (up to MAX_INITIAL_PAGES serial round-trips) until the last
two user prompts are on screen. On a real deployment each page is ~1s, so a
long tool-heavy last turn could stall the transcript for several seconds.
Fetch only the first page in the blocking bind, render immediately, then
page the rest of the window in the background behind a top-of-history
spinner. The previous-prompt heuristic is unchanged — just no longer on the
critical path.
- Extract the window-complete boundary into initialWindowComplete() and
reuse it in both fetchInitialHistoryWindow and the new backfill.
- bindStream fetches one page; backfillInitialWindow continues the same
paging loop after commit, holding loadingMoreHistory so scroll-up/rail
loaders don't double-fetch, generation-guarded like loadMoreHistory.
- New loadingInitialWindow flag drives a "Loading earlier messages…"
spinner above the oldest bubble.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* ⚡ perf(web): Unify initial history loading
- Build the prompt-boundary and viewport-fill window through one post-render loader
- Make the turn rail lazy and remove its eager 200-item history fetch
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* ✅ test(web): Cover lazy history loading
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* perf(web): pin the latest turn to the top with a trailing spacer
Add a LatestTurnSpacer as the last child of the message flow that pins the
newest turn's anchor to the top of the viewport (the newest real user prompt,
or the newest assistant text output when a page deep in a tool chain has no
prompt yet), letting the reply grow below it — the ChatGPT/Claude "question at
top" feel.
As a side effect the spacer keeps the transcript taller than its scroll
container whenever content sits above the anchor, so older history stays
reachable by scroll-up. That makes HistoryAutoLoader's viewport-fill fetch loop
redundant: it now pages only to the previous-prompt boundary (still capped by
initialWindowComplete), and the resize-driven re-fill and spinner-height
measurement are removed.
Spacer height = clientHeight − (anchor→content-bottom) − top gap, clamped to
≥ 0: it shrinks as the reply streams (its own top is fixed by the content
above, not by its height, so scrollHeight stays constant and stick-to-bottom
keeps the anchor pinned) and collapses to 0 once the reply exceeds the
viewport, restoring normal bottom-following.
Co-authored-by: Isaac
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* test(e2e-ui): regenerate visual baselines
* fix(web): keep loading history near the top
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* test(e2e-ui): regenerate visual baselines
* fix(web): preload history sooner near the top
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* refactor(web): show history skeleton for every page
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* fix(web): stabilize scroll during history prepends
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* style(web): loosen history skeleton spacing
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* style(web): use compact history loading indicator
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* fix(web): avoid latest turn spacer flicker
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* fix(web): observe initial history scroll adjustment
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* fix(web): bind history loading to live scroller
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* 🐛 fix(web): freeze spacer to loaded turn
---------
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
Replace the stale exact Qwen context-window registry with metadata from the shared MLflow provider catalog. Keep only the self-describing Anthropic [1m] marker and the conservative 128K offline fallback.
Reuse the onboarding catalog cache for both context sizing and pricing, preserve cache pricing fields in ModelInfo, and support provider-qualified ids, OpenRouter vendor namespaces, and Databricks aliases without release-specific model mappings.
Ratchet the hardcoded-model baseline and document the migration behavior. Cover exact, family, namespace, ambiguity, cache, encoded-metadata, and offline resolution paths.
Tests: 59 focused provider/context-window tests; 110 model-catalog, compaction, and session-override tests; changed-file pre-commit; repository-wide pre-commit except the pre-existing stale routing_pb2.py binding; live MLflow lookup smoke test.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Collapse the terminal-ensure / attach path in create_session_terminal —
11 hardcoded `if terminal_name == "<x>" and session_key == "main"` arms —
behind a single generic `_ensure_native_terminal(...)` shell dispatched
through the NativeHarnessProvider seam. The attach-path sibling of the 1.5b
launch shell (#3500/#3501); reuses the `_launch_<x>` adapters and
NativeLaunchContext. codex/antigravity supply an ownership predicate; codex
supplies a `finalize` for its one-shot policy notice — both run under the
per-session ensure lock, matching the inline arms.
- New shell in runner/native/orchestration.py (view-based existence check,
returns JSONResponse: 200 / 500 / 409), exported from runner/native.
- app.py: 11 arms (~450 lines) -> one collect-then-dispatch block.
- Repoint the HTTP attach-path claude/codex auto_create monkeypatch targets
to the orchestration module (the seam resolves the adapter there).
- 8 new unit tests for the shell.
- Doc: add 1.5c ledger row; flip stale 1.5b-i/ii rows to landed.
Behavior-preserving: qwen error label -> "Qwen Code" (display_name, as 1.5b-i);
antigravity now wires ensure_comment_relay via the base ctx (the landed
_launch_antigravity adapter already passed it).
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
Normalize Databricks Unity Catalog supported_api_types into the provider-neutral ModelWireAPI vocabulary and retain those facts while converting runner catalogs into the id-only routing-client shape.
Replace the exact Pi model exclusion table with a catalog-backed Claude wire check. Pi now keeps Responses-capable GPT models on its supported Responses path, while endpoints explicitly lacking Anthropic Messages are redirected to claude-sdk. Missing metadata from older runners remains unknown and does not trigger a redirect.
Ratchet six retired hardcode allowances and update the migration plan.
Tests: 104 catalog and smart-routing tests; 7 Pi Responses/provider tests; changed-file pre-commit suite. The repository-wide pre-commit run passed every relevant hook and only reported the pre-existing stale routing_pb2.py baseline.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Remove the release-specific model from the Kimi launcher example so an unoverridden session uses the default already configured in the Kimi CLI.
Document the ownership boundary, assert that the spawn environment omits HARNESS_KIMI_MODEL when no model is declared, and ratchet the retired lint allowance.
Tests: 12 Kimi spawn-environment tests; structural example load; staged pre-commit including YAML and hardcoded-model checks.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* feat(models): discover ad-hoc CLI default
Resolve the model for minimal harness-less agent YAMLs from the Databricks OpenAI-family catalog during bundle materialization instead of baking a release-specific endpoint into the CLI.
Preserve --model and OMNIGENT_MODEL precedence, and fail with explicit configuration guidance when discovery is unavailable. Ratchet the removed pin from the hardcoded-model allowlist and document the new behavior.
Tests: 111 discovery-disabled CLI tests; staged pre-commit including the hardcoded-model lint.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* test(models): pin YAML model precedence
Add a direct regression test showing that an executor.model declared in agent YAML remains authoritative over OMNIGENT_MODEL and catalog discovery.
Fail the test if catalog resolution runs, so future changes cannot silently turn an explicit YAML model into a fallback lookup.
Tests: 114 CLI chat tests; targeted pre-commit checks.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* feat(models): discover onboarding defaults
Select provider setup defaults from the live catalog after filtering specialty modalities, using stable family preferences for broadly accessible Anthropic and OpenRouter choices instead of release-specific model pins.
When discovery is unavailable, leave onboarding unpinned so the user supplies an explicit model. Add deterministic catalog fixtures for interactive CLI coverage, ratchet three lint allowances, and document the migration.
Tests: 147 onboarding and configure-models tests; staged pre-commit including the hardcoded-model lint.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): require intended OpenRouter family
Keep OpenRouter onboarding defaults within the catalog's Kimi family. If discovery returns no compatible family member, require the user to enter a gateway model instead of silently selecting a newer proprietary entry.
Correct the setup comments to match Click's prompt behavior: blank input accepts a discovered default, while an unavailable default requires an explicit value.
Tests: 86 provider and resolver tests passed. Targeted pre-commit passed for all modified files.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* test(models): pin offline runtime failure
Cover Anthropic and OpenAI runtime fallback when neither the agent nor provider config names a model and catalog discovery returns no data. Both paths must fail closed with guidance to configure an explicit model or retry discovery.
Document that removing source pins affects shared runtime defaults in addition to onboarding prompts, and clarify that required-family policy tokens use case-insensitive substring matching.
Tests: 88 focused runtime, provider, and resolver tests passed. A broader 153-test run reached 152 passes plus one unrelated host-credential leak in the existing Claude fallback test. Targeted pre-commit passed.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* test: make sandbox-cwd assertions portable across macOS firmlinks
_resolve_sandbox_cwd ends in Path.resolve(), and macOS routes the test's
literal paths through firmlinks (/home via the automounter, /tmp ->
/private/tmp), so the literal-string assertions fail on any macOS dev
box while Linux CI stays green. Compare against the same resolution
instead; on Linux both sides are identical strings.
Signed-off-by: webdevtodayjason <jason@webdevtoday.com>
* test: tidy sandbox cwd portability assertions
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: webdevtodayjason <jason@webdevtoday.com>
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* feat(models): discover ad-hoc CLI default
Resolve the model for minimal harness-less agent YAMLs from the Databricks OpenAI-family catalog during bundle materialization instead of baking a release-specific endpoint into the CLI.
Preserve --model and OMNIGENT_MODEL precedence, and fail with explicit configuration guidance when discovery is unavailable. Ratchet the removed pin from the hardcoded-model allowlist and document the new behavior.
Tests: 111 discovery-disabled CLI tests; staged pre-commit including the hardcoded-model lint.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* test(models): pin YAML model precedence
Add a direct regression test showing that an executor.model declared in agent YAML remains authoritative over OMNIGENT_MODEL and catalog discovery.
Fail the test if catalog resolution runs, so future changes cannot silently turn an explicit YAML model into a fallback lookup.
Tests: 114 CLI chat tests; targeted pre-commit checks.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* test(e2e): pin sessions mock model
Give the sessions-default REPL fixture an explicit mock-server model so the test exercises session routing rather than ad-hoc model discovery.\n\nThe E2E workflow intentionally disables catalog lookup. After ad-hoc defaults moved to catalog resolution, the model-less fixture exited before the REPL opened. Other approval fixtures in this file already pin the same mock-compatible model.\n\nTest: OMNIGENT_DISABLE_CATALOG_LOOKUP=1 OMNIGENT_SKIP_WEB_UI=true uv run --frozen pytest -q tests/e2e/test_repl_sessions_approval_e2e.py::test_sessions_default_flag_works --tb=short\nTest: pre-commit run --files tests/e2e/test_repl_sessions_approval_e2e.py
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
Second half of the runner launch seam, completing 1.5b. Routes the 3 special
arms and the turn-path opencode cold-boot through the seam, and consolidates all
11 create-session legs into one dispatch. Behavior-preserving.
- orchestration: extend the shell _launch_native_terminal with pre_launch (an
async (has_terminal) -> PreLaunchResult callback run inside the lock, so the
has_terminal-dependent rebuild/transfer/needs checks see the same state the
inline arms did), build_context (lazy full-context enrichment for claude's
bundle_dir/agent_name/skills + closures and codex's bundle, run only on
create), and reraise (turn-path opencode converts a launch failure to a 503
instead of publishing a start-error event).
- app.py: replace the 11 per-harness create-session legs with a single
collect-then-dispatch block — each leg only assigns its lock dict, context,
and optional pre_launch/build_context/resolve_agent_spec, then one
_launch_native_terminal call runs them. The 3 special arms (claude rebuild+
transfer, codex needs-check, antigravity payload+transfer) supply their
has_terminal-gated pre_launch; claude/codex supply build_context (codex keeps
the outer spec_entry as agent_spec). Turn-path opencode uses reraise=True.
- Preserve terminal_ready: only claude populated it in the create-session
response, so only claude's dispatch result is captured back (the consolidation
fixes a regression where 1.5b-ii's first cut dropped it).
- Tests: repoint the app-level _auto_create_<x>_terminal monkeypatches that now
route through the seam — claude create-session (events_lifecycle 603/688,
session_resources 2198) and the create-session auto-create guard tests
(terminals_autocreate: claude + antigravity) — to the orchestration symbol the
adapter calls. Add shell unit coverage for build_context (enrich-only-on-create)
and reraise. The terminal-attach/route patches (1.5c path) are untouched.
Net app.py reduction continues; the 11-arm launch chain is gone. Pre-existing
codex gateway-env failures in events_lifecycle are unchanged (codex arm behavior
preserved; those tests are unrelated app-server/gateway artifacts).
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
The seven native resolvers (pi, hermes, kimi, cursor, goose, kiro, qwen) looked up their CLI with a bare shutil.which, while readiness and the SDK executors resolve through resolve_cli_binary's fallback ladder (the nvm/npm/homebrew bin dirs the daemon's frozen PATH omits). A CLI installed only in a ladder dir passes the readiness badge but fails at launch. Route the resolvers through resolve_cli_binary so the badge and the launch agree.
resolve_cli_binary gains a `which` hook so the resolvers keep their existing test seam; the fallback ladder always uses the real filesystem.
Signed-off-by: Andrew Demczuk <andrew.demczuk@gmail.com>
* fix(inner): stop agent-CLI subprocesses inheriting unrelated host secrets
Closes#3445.
pi and codex filtered os.environ before spawning their vendor CLI; goose,
kimi, qwen, acp and hermes did not, so every host secret - cloud tokens, other
providers' API keys - reached those processes, sandboxed or not. hermes was
worst: the no-HERMES_HOME branch passed env=None, which inherits everything.
Implements the decision on the issue.
agent_env.clean_agent_env(allow_prefixes, allow_exact, deny_exact,
extra_allowed, source)
The model is not "no credentials ever". It is a shared safe base (HOME, PATH,
proxy, locale, tmp, XDG, the omnigent-session marker), plus the harness's own
config/provider families, plus whatever the spec declared in
os_env.sandbox.env_passthrough.
Per-harness families, matching the table on the issue:
qwen QWEN_, OPENAI_, DASHSCOPE_
goose GOOSE_
kimi KIMI_, MOONSHOT_ (keeps its documented ambient auth)
acp none - base + env_passthrough only, the agent is arbitrary
hermes HERMES_ (see below)
pi and codex become thin calls. Their sets are preserved exactly, including
codex's OPENAI_API_KEY deny; verified by diffing the new output against the
original inlined logic over a synthetic environment - identical, with and
without passthrough. USER/LOGNAME/SHELL/TZ stay per-harness rather than
entering the shared base, because pi passes them and codex does not and this
refactor must not widen codex's set.
hermes prefix family: HERMES_ only, and deliberately not DATABRICKS_. Hermes
authenticates from files, not the environment - hermes_native_bridge copies
~/.hermes/auth.json and ~/.hermes/.env into the per-session HERMES_HOME
(hermes_native_bridge.py:386-394). HOME still passes, so nothing breaks, and
the credential family this change exists to contain stays contained.
Also restores the launcher's env-prune defense: the sandboxed paths bake
tuple(env.keys()) into with_spawn_env_allowlist, so a full-environ env made
that allowlist a no-op.
Tests: tests/test_agent_spawn_env_canary.py - parametrized over all seven
harnesses, planting nine credential-family canaries and asserting none
survive, plus that each still gets a usable environment, that a harness sees
its own family and not a sibling's, that kimi keeps ambient KIMI_/MOONSHOT_,
that env_passthrough works as the migration path, and that deny_exact beats a
matching prefix. 21 cases.
Executor suites: 967 passed, 13 skipped, 0 failed.
Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
* fix(inner): point the spawn-env canary at the real executors
Addresses review on #3479.
- Extract _build_spawn_env() on qwen/goose/acp/hermes, matching kimi's
existing shape, and parametrize the canary over the real builders with
secrets planted in a monkeypatched environ. The prefix table was a hand
copy, so a harness reverting to os.environ.copy() kept the suite green;
it now fails, which is what the module docstring already claimed.
- Add NODE_EXTRA_CA_CERTS to BASE_ALLOW_EXACT. Node honours it where
SSL_CERT_FILE is ignored, so without it a corporate-CA user upgrading
loses TLS on every Node harness without a NODE_ family of its own.
- Warn in acp_executor._ensure_initialized when the handshake fails or the
child dies first, naming os_env.sandbox.env_passthrough as the likely fix.
Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
---------
Signed-off-by: Mr-Neutr0n <64578610+Mr-Neutr0n@users.noreply.github.com>
Signed-off-by: Mr-Neutr0n <harikp2002@gmail.com>
The policy evaluate endpoint is a BLOCKING hook: a harness waits on its
allow/deny before running a tool. Its payload rules were applied from a chain
of conditionals, and a rule in a branch only ever reaches whichever phase
lands in that branch. Three rules now come from one per-phase schema, and all
three run for every phase.
What was getting through:
- `event.data` was accepted as an object, a string or absent, then normalized
with `or {}`. For a tool phase that means the gate evaluated as though the
caller had sent nothing: every tool-name-scoped policy skipped, and the hook
answering allow. Tool and LLM phases now require an object; a bare string is
a legitimate wire form only on the prompt phase, and an absent payload is
malformed everywhere, since every first-party producer sends one.
- A tool-scoped gate needs a tool name, and only one spelling was accepted.
Producers differ: claude-native and the in-process tool dispatch send
`request_data.name`, the OpenCode plugin sends the tool in `event.target`.
Requiring the first rejected the second with a 400 — and that plugin turns
any non-2xx into ALLOW, so a stricter guard silently disabled every OpenCode
TOOL_RESULT policy rather than tightening it. Any declared source now
satisfies the rule, and the resolved name is written onto the container the
engine reads, so those policies gate instead of merely passing validation.
- `event.context` must be an object when present. An earlier revision of this
message described only two rules while the diff carried three.
`event.type` is also checked before being used as a dict key: an unhashable
value raised inside the lookup and surfaced as a 500 rather than a 400.
The three rules above were previously three independent structures (which
wire types are accepted; which phases need an object payload; where a tool
name may come from), each keyed by phase and each read with a permissive
`.get(phase, default)` fallback. The comment on them already said "one schema
per phase" — the code didn't enforce it: a phase added to the first structure
alone was silently accepted, validated as loosely as possible, and given no
tool-name rule at all, because the other two structures simply had no entry
for it and their lookups defaulted rather than erred. They're now one
NamedTuple per wire type with no default values on any field, so a new entry
cannot be added without deciding both properties at once, and the only
`.get()` left is the outer wire-type lookup, which 400s on a miss instead of
falling back to anything.
The test table enumerates each phase and non-object-data vector and
cross-multiplies them, rather than hand-listing every case — kept in sync
with the production schema by hand, since that schema lives inside a
route-registration closure and isn't something a test module can import. It
asserts the structured error code rather than the status alone, and now
includes a non-empty list alongside the empty one: both are simply
non-dict, but hand-listing only the empty list is coincidentally falsy in a
way a narrower, wrong fix (special-casing falsy values) would have passed.
Five mutations kill it: accepting object-or-string-or-absent everywhere,
requiring a single tool-name spelling, dropping the context rule, validating
the alternative spelling without normalizing it (caught because the oracle
asserts a tool-scoped DENY, not a 200), and giving one phase's schema entry a
wrongly permissive `data_must_be_object`.
A pre-existing test's docstring also claimed OpenCode's plugin sends REQUEST
data as a bare string; it now sends `{"text": ...}` like every other
first-party producer. Reworded to describe why the bare-string form is still
accepted (older/third-party compatibility) without attributing it to
OpenCode's current behaviour.
Signed-off-by: Andrew Reid <andrew@reid.ee>
Unconditionally set CLAUDE_CODE_USE_GATEWAY=1 in the Databricks ucode
subprocess env and stop setting CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS on
that path. Gateway-aware mode keeps tool search on so MCP schemas load on
demand, so the betas-disable knob is no longer needed here.
Update test_ucode_config_for_profile_reads_allowlisted_claude_state to
expect CLAUDE_CODE_USE_GATEWAY=1 in the ucode env instead of the removed
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS flag.
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
Co-authored-by: Bryan Qiu <bryan.qiu@databricks.com>
Co-authored-by: Harry Yao <harry.yao@databricks.com>
* fix(tunnel): give host/runner websocket tunnels a verifying SSL context
On interpreters whose OpenSSL default cert path is uninitialized (python.org
macOS framework builds before Install Certificates.command, and
python-build-standalone interpreters used by uv), ssl.create_default_context()
loads zero trust roots, so the host and runner wss:// tunnels failed with
CERTIFICATE_VERIFY_FAILED and looped on reconnect.
Add omnigent/tls.py (resolve_ca_file + cached client_ssl_context) that resolves
a CA bundle OS-trust-store-first with a certifi fallback, and pass that context
to both tunnel websockets.connect calls for wss:// (ws:// stays ssl=None).
egress/ca.py:_system_ca_bundle now shares resolve_ca_file; certifi is promoted
to an explicit dependency.
Closes#1730
Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
* fix(claude-native): pass a verifying SSL context to wss:// terminal-attach
_websocket_connect opened wss:// terminal-attach connections (the scheme
terminal_attach_url produces from an https workspace base_url) with a bare
default SSL context, so claude-native attach to a remote workspace hit the same
empty-trust-store failure fixed for the tunnels. Route it through
client_ssl_context() for wss:// (ws:// stays ssl=None). Also realign a
ws_tunnel test with the databricks_request_headers rename from main.
Closes#1730
Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
* chore(deps): record certifi in uv.lock
pyproject.toml promoted certifi to an explicit dependency; add it to the
omnigent package's dependencies and requires-dist in uv.lock so
"uv sync --locked" passes in CI. certifi was already resolved transitively,
so its package entry (with hashes) is unchanged — this only records the
direct dependency edge.
Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
---------
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
Co-authored-by: Tomu Hirata <tomu.hirata@gmail.com>
Session-discovered agents start with harness=null (filled lazily on hover
via prefetchAvailableAgentDetails). The fork picker filters candidates with
forkTargetCarriesHistory(a.harness), which returns false for null, so
custom agents were silently excluded from the fork agent dropdown even
though they appear fine in the new-session picker.
Fix: call prefetchAvailableAgentDetails for all agents when ForkSessionForm
mounts (same pattern NewChatDialog uses on dropdown open). The helper is a
no-op for agents whose harness is already known, so re-running on agents
list change is safe.
Adds a test that verifies prefetch is called for a session-discovered agent
(harness=null, sessionId set) on mount.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(web): group archived sessions by date
The archived sessions list in the settings page was a flat
chronological list that became hard to scan. Group sessions under
date headers (Today, Yesterday, Previous 7 days, Previous 30 days,
or month/year for older entries) for easier browsing.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(web): use DST-safe date arithmetic and add grouping tests
Use calendar-based setDate() instead of fixed millisecond offsets for
computing date boundaries in the archived sessions grouping, avoiding
mis-bucketing around DST transitions. Add a Vitest test with a pinned
system clock that verifies all five date group headers render correctly.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(web): share now across grouping, fix test locale/timezone flakiness
- Capture a single `now` in the groupedArchived memo and pass it to
every dateGroupLabel call, avoiding redundant Date construction and
a rare date-rollover inconsistency during iteration.
- Use local-time Date constructors in the test so bucket boundaries
match dateGroupLabel's local-time arithmetic in any timezone.
- Derive the expected month/year label via toLocaleDateString so the
assertion passes under non-English locales.
- Wrap assertions in try/finally so fake timers are always restored.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* feat(policy): add detect_loop builtin to catch agent retry loops
The #1 token-waste pattern is an agent retrying the exact same failing
tool call. max_tool_calls_per_session counts total calls but cannot
detect repeated ones. detect_loop tracks recent (tool_name, args_hash)
tuples in session_state and ASKs when the same call repeats N times
within a configurable sliding window, letting the user break the loop.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* address review: use full SHA-256 digest, add e2e tests
- Remove [:16] truncation from _args_hash to use the full 64-char
hex digest, avoiding false-positive collisions from 64-bit space.
- Add YAML → PolicyEngine e2e tests exercising the full roundtrip:
repeated calls trigger ASK, diverse calls pass, window eviction
works, and non-tool_call phases are unaffected.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* address review: guard params, fix docstring, move e2e test
- Clamp window and threshold to minimum 1 so zero/negative values
cannot cause unbounded state growth or always-ASK behavior.
- Add minimum: 1 constraints to both params in the registry schema.
- Fix docstring to describe actual persisted state shape (list of
SHA-256 hex digests, not tuples).
- Move e2e test from tests/runtime/policies/ to tests/e2e/ per
repo convention.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* feat(policies): add detect_thrashing builtin context policy
Agents that hit repeated tool errors burn tokens without making
progress. Add a new builtin contextual policy that tracks
tool-result outcomes in a rolling window and fires when the agent
appears stuck — either via consecutive errors or a high error rate
within the window.
Two independent triggers (both configurable, both independently
disableable):
- consecutive_threshold (default 5): fires after N straight errors
- window_error_rate (default 0.8): fires when ≥80% of the last
N results (window, default 10) are errors
Error detection is heuristic (common prefixes like "Error:",
"Traceback", "Permission denied", "fatal:", and JSON {"error": ...}
payloads). No server LLM required, unlike detect_task_switch.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(policies): address review feedback for detect_thrashing
- Fix docstring: "exceeds" → "reaches or exceeds" to match the >= check
- Rename misleading test names (test_below_consecutive_threshold_allows
was actually at-threshold; test_window_rate_allows_below_threshold was
at-threshold)
- Retain max(window, consecutive_threshold) history entries so the
consecutive check still works when window < consecutive_threshold
- Rate check now computes over the last `window` entries (not the full
retained history), and reports window size in the reason message
- Add integration test exercising state accumulation across evaluate
calls through the real policy engine
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(policies): harden detect_thrashing against edge cases
- Validate session_state history as list[int] before use; reset to
empty on corruption instead of raising TypeError.
- Guard against window=0 by using effective_window = max(window, 1)
to prevent division by zero in the rate check.
- Use dataclasses.replace in the integration test to preserve all
original RuntimeCaps fields instead of reconstructing with only
execution_timeout.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(policies): add minimum/maximum constraints to detect_thrashing schema
Add validation bounds to the registry params_schema so invalid config
values fail fast: consecutive_threshold >= 0, window >= 1,
window_error_rate in [0.0, 1.0].
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(policies): use Phase enum in detect_thrashing integration test
Use Phase.TOOL_RESULT instead of the bare string "tool_result" in the
PhaseSelector construction, consistent with other integration tests.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(web): clear deleted pinned sessions from the sidebar's Pinned section
The Pinned section reads a sibling ["pinned-conversations"] cache that the
delete mutations' prefix-matched ["conversations"] sweep deliberately skips
(nesting it under that prefix breaks the pin-toggle's cache patch). That
isolation is by design, but it means the delete handlers must drop the row
from the pinned cache explicitly — which they didn't. So deleting a pinned
session removed it from the flat list but left it lingering in the Pinned
section until a full reload.
Mirror the unpin removal in all three delete paths (single-delete onSuccess,
bulk-delete onSuccess, and bulk-delete onError's partial-success branch),
patching the pinned cache in place rather than invalidating for the same
search-reindex-lag reason the list is patched in place.
Co-authored-by: Isaac
* fix(web): keep the sidebar row height stable during delete
The in-flight "Deleting…" status row that replaces an interactive
conversation row used `text-sm py-2` with no height constraint, while
the interactive row uses `sidebar-compact-text h-7 py-0.5`. So starting
a delete didn't just recolor the row — it grew taller and changed font
size, shifting the surrounding list.
Match the deleting row's box metrics to the interactive row (h-7,
sidebar-compact-text font size, otto-sm radius) so the swap only changes
color/opacity.
Co-authored-by: Isaac
* fix(web): keep the sidebar row size stable when editing the title
The inline rename row rendered a `text-sm` (14px) input inside a wrapper
whose `py-1` + `size-7` buttons summed to ~36px, while the interactive
row is `h-7` (28px) with the 13px `sidebar-compact-text` font. So
double-clicking to rename made the row grow taller and bump the font
size, an input visibly larger than the row it replaced.
Match the edit row's box metrics to the interactive row (h-7,
sidebar-compact-text, otto-sm radius) and drop the buttons to icon-xs
(24px) so they sit inside the 28px row, leaving only the muted edit
background to signal the mode.
Co-authored-by: Isaac
* test(e2e): guard pinned-session delete clears the Pinned section
Adds a browser e2e that pins a session (while sitting on `/`, so it isn't
the active chat) and deletes it, asserting the "Pinned" section unmounts
in place — no reload.
Two harness details are load-bearing, and getting them wrong yields a
test that passes even against the buggy build:
- Delete a NON-active pinned session (page on `/`). Deleting the open
session navigates away and refetches; an active session also gets a
WS `removed`-frame reconcile. Either clears the row regardless of the
cache bug.
- Assert the "Pinned" SECTION disappears, not the row's href. While the
delete is in flight the row swaps to a hrefless "Deleting…" status row,
so an href-count assertion flickers to 0 during that transient and
passes spuriously; the section stays mounted until the pinned cache is
actually empty.
Verified it fails (~3s) against a build with the pinned-cache delete
patch removed, and passes with it.
Co-authored-by: Isaac
Collapsed tool runs in the chat transcript now read like the native
CLIs' step summaries ("Ran 1 shell command, read 2 files", "Listed 1
directory") instead of the generic "See N steps". The label is derived
from the folded calls' tool names and arguments in formatToolRunLabel:
- categories: shell / list / read / edit / search, covering omnigent
sys_* tools plus the native harness names (Claude Code Bash/Read/...,
Codex shell/apply_patch, pi & opencode lowercase bash/read/edit/...)
- shell commands that are a bare ls / cat recategorize as directory
listings / file reads, matching the vendor TUIs; codex's login-shell
wrapper (/bin/bash -lc '...') is unwrapped first
- runs of only unrecognized tools fall back to "Called N tools"
- per-step titles added for the native harness tools (Bash prefers the
model-written description, codex shell shows the unwrapped command)
The fold now labels only its own (hidden) contents; the whole-run
count plumbing is gone since the label no longer double-counts the
visible streaming tail.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): show startup spinner when a send relaunches a disconnected runner
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): show a sidebar starting spinner while a session is booting
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): don't rename the wrong session when the sidebar reorders mid-double-click
Double-click rename fired on whichever row received the dblclick event.
Browsers pair the two clicks of a double-click by pointer position and
timing, not element identity, so when the list reordered between the
clicks (an updated_at bump pushing rows around under the cursor) the
second click and dblclick landed on the row that slid into place and
opened rename on it — committing the typed title to a session the user
never aimed at.
Track the last two clicks each row receives and enter rename only when
the row saw both clicks of the pair; a dblclick preceded by a single
recent click means the double-click started on a different row and is
ignored.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): freeze sidebar order under the pointer so single-click actions hit the aimed row
The double-click guard can't help single-event interactions: a right-click
(or kebab click) that lands just after a background updated_at bump opens
the context menu of whichever row slid under the cursor — the menus are
visually identical, so the user renames (or archives, deletes, stops) a
session they never aimed at.
Fix it upstream of any one interaction: while the pointer is inside the
conversation list, pin every row's sort key at its first-seen value so
rows cannot move under the cursor at all. Keys accumulate lazily in
sortByUpdatedAtDesc (covering project folders and pages loaded mid-hover)
and clear when the pointer leaves, snapping the order back to reality.
The active row's frozen key captures its ActiveChatOverride value so
dropping the override mid-hover (clicking another row) can't move it
between the clicks of a double-click either.
Also rebuild the element tree per rerenderSidebar call in the row-actions
test harness — re-rendering the identical element let React bail out
without re-invoking the sidebar, silently ignoring mid-test data swaps.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): hold sidebar order while a rename edit is open, not just while hovered
The order freeze keyed off pointer position alone, but the pointer
naturally drifts out of the sidebar while typing a new title — the hold
released mid-edit and background updated_at churn resumed shuffling rows
around the open input. Moving the edit row's DOM node also blurs the
input, committing a half-typed title.
Rows now report an in-progress inline rename through RowEditHoldContext,
and ConversationList keeps the sort-key freeze active while the pointer
is inside the list OR any rename edit is open. The frozen-key map clears
only once neither hold remains, so the order snaps back on commit/cancel
(or pointer-leave with no edit open).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): engage the rename-edit order hold before paint
A passive effect reports the hold after paint, leaving a one-frame
window — when rename starts with the pointer already outside the list
(context-menu portal) — where a background updated_at reorder could
move and blur the just-mounted input. useLayoutEffect closes the gap.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): make trackpad wheel scrolling work in the terminal view
xterm's built-in wheel-to-mouse-report conversion damps sub-50px pixel
deltas by 0.3x and emits at most one report per DOM event, so macOS
trackpad scrolling over a mouse-tracking TUI (Claude Code, tmux mouse on)
barely moves. Replace it with a custom wheel handler that accumulates
deltas at face value and emits one SGR report per whole line, deferring
to xterm's native handling when the pane program isn't tracking the
mouse (e.g. a plain shell on the control transport).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(terminals): replay pane screen/input modes in the control-mode attach seed
capture-pane records cell contents only, so a TUI that entered the
alternate screen and enabled mouse tracking before the web client
attached (OpenCode, vim — anything that sets modes once at startup)
left the browser xterm believing no tracking was active: wheel events
sent nothing and the terminal view could not scroll until the program
happened to re-toggle its modes. Reconstruct the modes from tmux's pane
flags and replay them around the seed — alt screen before the content
so it never pollutes primary scrollback, mouse tracking/encoding and
DECCKM after the cursor restore.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(e2e_ui): pin wheel-to-SGR-report forwarding for mouse-tracking shells
A program in a user shell enables any-motion + SGR mouse tracking and
records its stdin; a slow trackpad-sized wheel gesture over the xterm
must land >=3 wheel-up reports. xterm's damped built-in conversion
yields <=1, so this fails without the accumulating wheel handler
(verified against an unfixed UI build).
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(terminals): harden seed metadata parsing and quote e2e log path
Address review: pad missing/empty tmux mode-flag fields so a flags
anomaly costs only the optional mode replay, never the cursor and
alt-screen state; quote the wheel-log path typed into the e2e shell.
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* test(e2e_ui): type-annotate the wheel test's tmp_path fixture
Signed-off-by: dbczumar <corey.zumar@databricks.com>
---------
Signed-off-by: dbczumar <corey.zumar@databricks.com>
* fix(web): preserve file browser scroll position across session switches
The Files panel's scroll container never tracked its position, so
switching conversations collapsed the list to a loading state and
clamped scrollTop back to 0 with nothing to restore it.
Cache scrollTop per conversation (and per Changed/All view) in a
module-level map — the same pattern FolderTree uses for expanded
paths — restoring it once the view's data is ready, and gating saves
on having restored first so the loading-state clamp can't overwrite
the cached value.
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
* fix(web): survive the loading clamp when restoring file browser scroll
The first cut restored scrollTop once when isLoading turned false — but
the files queries are disabled (not loading) until the environment query
resolves, so the restore fired against the short placeholder, clamped to
0, and the clamp's scroll event overwrote the cached position.
Gate on data presence instead, re-assert the target via an
animation-frame loop until the container can hold it (or its height
stops changing), and keep saving off until the restore settles.
Also re-sync FolderTree's expanded-paths state from its cache when the
conversation changes without a remount — previously the tree kept the
prior conversation's expanded set, which also skewed content height at
restore time.
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
* fix(web): keep the open file's scroll position across session switches
The app remembers which file is open per session and re-opens it in the
viewer on switch-back — at the top. The earlier fix only covered the
Files panel list, so what users actually saw (the open file's content)
still reset.
Extract the clamp-surviving restore logic into a shared useScrollRestore
hook (FilesPanel now consumes it) and wire persistence into every viewer
surface, keyed per conversation + path: the Monaco code editor and diff
viewer (via their scroll APIs), the FileViewer content area, the
markdown/notebook previews, and the TipTap markdown editor.
Verified end-to-end in a real browser: Playwright tests scroll, switch
sessions via the sidebar, switch back, and assert the offset returns —
for both the file tree and an open markdown file.
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
* fix(web): harden scroll restore against async content growth and Monaco clamps
The restore loop gave up as soon as the container's height held still
for one frame — but previews grow in bursts (async syntax highlighting,
image decode, lazy notebook cells), so a single stall stranded the
reader at the top. Replace the giveup with a 1.5s deadline that keeps
re-asserting the saved offset, and settle immediately on wheel/touch/
pointer input so the user is never fought for the scrollbar.
The Monaco surfaces saved onDidScrollChange offsets unconditionally, so
a not-yet-laid-out editor's clamp-to-0 event could permanently overwrite
the cached position. A shared attachEditorScrollRestore helper now
suppresses saves and re-asserts the target until it's reached, the user
scrolls, or the budget expires — the same contract as the DOM hook.
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
---------
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
Doc sync / Classify and draft docs (push) Has been cancelled
CI / Pytest (databricks) (push) Has been cancelled
CI / Pytest (inner-rest) (push) Has been cancelled
CI / Pytest (integration-mock) (push) Has been cancelled
Lint / Version lockstep check (push) Has been cancelled
Sync OpenAPI to site / Open sync PR on omnigent-site (push) Has been cancelled
UI Preview / notify (push) Has been cancelled
UI Preview / build (push) Has been cancelled
OSS Scorecard / Scorecard analysis (push) Has been cancelled
UI Preview / cleanup (push) Has been cancelled
CI / Pytest (repl-sdk) (push) Has been cancelled
CI / Pytest (runtime-policies) (push) Has been cancelled
CI / Pytest (server-approvals) (push) Has been cancelled
CI / Pytest (server-rest) (push) Has been cancelled
CI / Pytest (slack) (push) Has been cancelled
CI / Pytest (spec-llms) (push) Has been cancelled
web Tests / web test (push) Has been cancelled
CI / Pytest (tools) (push) Has been cancelled
CI / Pytest (misc) (push) Has been cancelled
CI / Pytest (runner-app) (push) Has been cancelled
CI / Pytest (runtime-core) (push) Has been cancelled
CI / Pytest (runtime-harnesses) (push) Has been cancelled
CI / Pytest (server-integration) (push) Has been cancelled
CI / Pytest (stores) (push) Has been cancelled
CI / Pytest (stores-postgres) (push) Has been cancelled
CI / Pytest (stores-mysql) (push) Has been cancelled
CI / Pytest (codex-parity) (push) Has been cancelled
Windows (native) / Windows smoke + unit (push) Has been cancelled
Lint / Pre-commit checks (push) Has been cancelled
CI / Coverage report (push) Has been cancelled
UI Preview / deploy (push) Has been cancelled
Native hook subprocesses (codex, claude, kimi, hermes, cursor) and the pi/opencode
JS extensions have been POSTing directly to the Omnigent server with a baked
30-minute bearer token. After expiry, every hook invocation pays ~1.7s for
credential re-discovery. The relay approach eliminates this class of failure
entirely by removing the server bearer from hook configs.
Changes:
relay handler (claude_native_bridge.py):
Add POST /policies/evaluate to the tool relay HTTP server. The relay
authenticates callers with its existing non-expiring local token and
proxies to the Omnigent server using asyncio.run_coroutine_threadsafe
with the runner's refresh-capable server_client (86400s timeout to
match ASK gate long-polls). session_id is written into tool_relay.json
so hook subprocesses can identify the session without a separate config.
runner/app.py:
Pass server_client and session_id to start_tool_relay so the relay can
serve the /policies/evaluate proxy endpoint.
native_policy_hook.py:
Add read_relay_policy_config(bridge_dir) helper that reads tool_relay.json
and returns (relay_url, relay_token, session_id), and relay_policy_evaluate_url.
Add _RELAY_URL_ENV / _RELAY_TOKEN_ENV constants for env-var harnesses.
hook subprocesses (codex, claude, kimi):
Read tool_relay.json first via read_relay_policy_config; fall back to
direct server call (policy_hook.json / permission_hook.json) when the
relay is not yet up. Remove _PersistingReauth from codex_native_hook.
hermes/cursor hook subprocesses:
Check _OMNIGENT_RELAY_URL / _OMNIGENT_RELAY_TOKEN env vars; fall back to
existing _OMNIGENT_AUTH_HEADERS path when absent.
hermes_native_bridge.py:
Add inject_relay_into_policy_hook which rewrites omnigent-policy-hook.sh
with relay env vars after ensure_comment_relay runs.
orchestration.py:
Wire ensure_comment_relay into _auto_create_pi_terminal (new param) and
inject relay coords into pi config.json and hermes wrapper script after
relay starts. Wire ensure_comment_relay into opencode policy_env via
OMNIGENT_RELAY_FILE. Remove _policy_hook_auth_loop and related refresh
machinery (_register/_unregister_policy_hook_auth, _POLICY_HOOK_AUTH_SESSIONS).
pi extension JS:
Add relayCredentials() that re-reads config.json for relayUrl/relayToken
on each call; evalNativePolicyHttp prefers relay URL and token over direct
server call.
opencode plugin JS:
Add relayCredentials() that re-reads OMNIGENT_RELAY_FILE (tool_relay.json)
on each call; evaluate() prefers relay over direct server call.
pi_native_bridge.py:
Add inject_relay_into_config to write relayUrl/relayToken into config.json.
All harnesses keep a direct-server fallback so sessions started before the
relay is up (first-call race) continue to work. The relay path is taken on
every subsequent call once tool_relay.json is written.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(models): discover Kiro picker catalog
Replace the curated Kiro model picker table with the CLI's JSON model listing so newly released, renamed, or retired Kiro models no longer require an Omnigent source update.
Run discovery on the bound runner, expose it through a dedicated model-options endpoint, and reuse the server's asynchronous single-flight cache so snapshots never block on the CLI process.
Preserve Kiro-provided default, description, context-window, and credit-rate metadata in picker rows, remove four hardcode allowances, and document the discovery boundary.
Tests: 115 Kiro, runner lifecycle, and server snapshot tests; staged pre-commit run; manual validation against kiro-cli 2.10.0 output.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* test(kiro): cover picker discovery failures
Exercise the runner endpoint's retryable 503 path when Kiro CLI model discovery fails so the server keeps its picker cache cold instead of treating failure as an empty successful catalog.
Extend the session snapshot round-trip to verify provider descriptions and rate units survive NativeModelOption's extra-field wire schema alongside context windows and rate multipliers.
Tests: 116 Kiro, runner lifecycle, and snapshot tests passed. Live kiro-cli 2.15.1 discovery returned nine models with auto as the sole default. Targeted pre-commit passed.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(kiro): narrow discovered picker contract
Remove the unused kiro_base_model_options compatibility alias because its old pure lookup contract became a blocking CLI subprocess and no production caller remains.
Stop emitting isCurrent for Kiro because the CLI discovery response does not provide current-session state and the Web picker derives the selected row from model_override.
Cover missing and mismatched CLI defaults in the discovery mapper and verify that the Web picker falls back to its Default sentinel, leaving Kiro responsible for choosing the actual default. Refresh the Kiro picker E2E fixture and wording to match live discovery.
Tests: 118 focused Kiro/runner/snapshot tests; 4,705 Web tests; targeted pre-commit checks.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
First half of the runner launch seam. Wires the provider's auto_create_terminal
field (declared since 1.1, never dispatched) and collapses the 8 uniform
create-session launch arms in runner/app.py onto it. Behavior-preserving.
- orchestration: add NativeLaunchContext (flat dataclass of the inputs the 11
builders may need, incl. claude's closures), PreLaunchResult (skip /
force_recreate / needs_terminal for the special arms in 1.5b-ii), 11 thin
_launch_<x>(ctx) adapters that unpack the context and call the unchanged
_auto_create_<x>_terminal builder with that harness's exact kwarg subset, and
the shared shell _launch_native_terminal(harness, ctx, *, ensure_locks,
pre_launch=None, resolve_agent_spec=None). The shell runs the lock /
existence-check / pending+error-event mechanics every arm shared and resolves
the adapter via resolve_hook(provider, "auto_create_terminal").
- Option A (adapters, builders unchanged) keeps the 21 direct-call builder tests
intact. agent_spec is resolved lazily via resolve_agent_spec inside the create
block, preserving each arm's error semantics (pi unwrapped; cursor/opencode/
kimi swallow OmnigentError via _resolve_session_agent_spec_or_none; the rest
pass no resolver).
- harness_plugins: repoint auto_create_terminal to omnigent.runner.native:_launch_<key>.
- app.py: the 8 uniform arms (pi, cursor, kiro, opencode, goose, hermes, qwen,
kimi) become one _launch_native_terminal call each, picking the per-harness
lock dict (kept app-scope so session cleanup can pop by name). Net -256 lines.
- qwen's launch-error label is now "Qwen Code" (uniform display_name) vs the
former lowercase "qwen" — cosmetic; no test asserted the literal.
Deferred to 1.5b-ii: the 3 special arms (claude/codex/antigravity) and the
turn-path opencode cold-boot, which still use the direct builders.
Tests: unit-cover each adapter's kwarg subset and the shell's branches
(create / existing-skip / force-recreate teardown / skip+needs_terminal /
start-error event / lazy-spec-only-on-create / non-native None). The workflow-
init HTTP suite exercises the real launch path for the uniform arms and stays
green. Pre-existing codex gateway-env failures in events_lifecycle are unchanged
(verified identical on clean main; codex arm untouched here).
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
Remove the release-specific smart-routing model table and require the runner worker catalog for routing candidates. When discovery is unavailable, leave the harness on its provider-resolved default instead of selecting a stale fallback.
Order catalog candidates by normalized provider-relative cost tiers while preserving catalog order as the tie-breaker, and express the built-in judge rubric through stable fast, balanced, and powerful intents rather than vendor model-name tiers.
Apply the same discovery-only rule to sys_advise_models, ratchet eight hardcode allowances, and document the remaining wire-compatibility exclusions as a separate migration boundary.
Tests: 67 focused routing/session tests; staged pre-commit run.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* feat(models): resolve runtime defaults from catalogs
Adapt MLflow provider listings into normalized resolver candidates with tri-state capability metadata, context windows, provider-relative cost tiers, and deterministic family filtering.
Replace release-specific defaults across workflow ucode routing, SDK executors, Databricks execution, and Claude/Codex/Pi/OpenCode native launch paths. Explicit request, spec, ucode, and provider-configured models continue to win; unresolved defaults now use the active provider catalog and fail clearly when discovery has no compatible model.
Improve model-version sorting so provider prefixes, dates, endpoint sizes, and unrelated numeric families do not distort catalog order. Ratchet ten obsolete hardcode allowances and document the runtime migration boundary.
Tests: 168 catalog/workflow tests; 519 executor tests; 449 native tests; staged pre-commit run.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): honor overrides before defaults
Apply per-session and CLI model overrides to the effective executor spec before spawn-environment builders attempt provider default resolution. This keeps explicit request values authoritative when catalog lookup is unavailable.
Preserve an explicit OMNIGENT_MODEL value when --harness selects the runtime, and allow model-only E2E overrides when the YAML owns harness selection. Add deterministic fixture models to unrelated tests so catalog-disabled CI does not depend on discovery.
Tests: 8 catalog-disabled CI regressions; 284 broader CLI/runtime/runner tests; staged pre-commit including the hardcoded-model lint.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): preserve catalog default policy
Route default-intent catalog resolution through the existing general-purpose selection policy after family filtering. This retains specialty-model exclusion and provider pins while leaving non-default intents on metadata ranking.
Require dynamically discovered Databricks defaults to use gateway-routable databricks-prefixed ids, report actionable catalog misses to direct executor callers, and model context capacity from max input tokens rather than input plus output budgets.
Add regression coverage for constrained defaults, lagging provider pins, OpenAI specialty variants, Databricks Claude/OpenAI routing, and context-window normalization.
Tests: 85 focused catalog/provider tests passed; 150 broader tests produced 149 passes plus the documented ambient Claude-login failure. Live Databricks catalog verification found 14 Claude and 16 OpenAI entries, all gateway-prefixed. Pre-commit passed all relevant hooks; repository-wide web-prettier and stale routing protobuf checks remain baseline failures.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): offload catalog discovery
Run cold catalog resolution on the existing dedicated thread helper from async Codex, Databricks, Open Responses, OpenAI Agents, and Pi turn paths. Model-less first turns can now wait for remote discovery without blocking the shared event loop for the catalog timeout.
Keep explicit and configured model precedence synchronous and unchanged. Make Pi's internal model resolver async so its Databricks fallback follows the same non-blocking boundary.
Add a regression that verifies catalog discovery executes outside the event-loop thread and update Pi resolver tests for the async contract.
Tests: 405 affected executor tests passed. Targeted pre-commit passed, including formatting, Ruff, and the hardcoded-model lint.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(models): offload Claude catalog lookup
Move the remaining Claude SDK Databricks catalog fallback onto the dedicated thread helper so a cold remote lookup cannot block the async turn loop.
Restore direct Pi coverage tying a catalog-selected Databricks default to dynamic models.json registration. This preserves the prior unknown-model invariant even when the selected gateway id is newer than Pi's curated static entries.
Tests: 251 Claude SDK and Pi executor tests passed with the documented macOS path-canonicalization test deselected. Targeted pre-commit passed, including Ruff and the hardcoded-model lint.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(harness): route native spawn-env through the provider seam (PR 1.5a)
Collapse the two near-identical 11-arm native spawn-env dispatch chains in
runner/app.py (create-session ~2567 and dispatch ~6092) onto the provider seam.
Each block becomes one guarded call to a registry-driven helper; net -171 lines
in app.py. Behavior-preserving — every native harness produces the identical
spawn env before/after.
- harness_plugins: populate `spawn_env_builder` on all 11 built-in providers
(uniform `omnigent.<key>_native_bridge:build_<key>_native_spawn_env`) and add
a `bridge_id_label_key` field, set to `omnigent.<key>_native.bridge_id` for
the three label-based harnesses (codex/opencode/antigravity). The label key is
derived (not imported) to keep harness_plugins import-light; a test pins the
derivation against the real bridge constants.
- runner/native/orchestration: add `_resolve_native_spawn_env(harness, session_id,
*, server_client, optional_labels)`. It resolves `provider.spawn_env_builder`
and handles the three shapes — bare (session id only), label (bridge id from
`bridge_id_label_key`), and two named specials: claude (bridge id via the
runner helper with a server-side fallback) and hermes (writes its policy-hook
config before building). Returns None for non-native harnesses so the caller
keeps its SDK spawn env. Re-exported via runner/native/__init__.
- runner/app: both blocks now call the helper; the per-harness bridge imports and
label-key reads are gone.
The two special-cases (claude/hermes) stay named branches in the helper rather
than fully data-driven provider fields — their only consumers are single call
sites, and 1.5b's NativeLaunchContext will reshape the right calling convention.
Tests: extend the provider-paths-resolve + required-hooks tests to cover
spawn_env_builder; pin bridge_id_label_key against the real constants; add
`_resolve_native_spawn_env` unit coverage for all four shapes + the non-native
None path. The existing workflow_init codex-bundle-dir spawn-env test (the
end-to-end behavior-preservation proof) stays green unchanged.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* test(harness): hoist spawn-env test imports to module level
Move the per-test `_resolve_native_spawn_env` and
`CODEX_NATIVE_BRIDGE_ID_LABEL_KEY` imports (added in 1.5a) up to the module
import block. No behavior change; test-only cleanup.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
## Related issue
N/A
## Summary
- `@testing-library/jest-dom` never declares `vitest` as a (peer) dependency.
Under pnpm's store layout, jest-dom's `declare module "vitest"` matcher-type
augmentation can't resolve `vitest`, so it silently fails to merge and `tsc`
loses every DOM matcher (`toBeInTheDocument`, `toHaveClass`, …) — even though
they register fine at runtime. See vitest-dev/vitest#10411.
- Declare the missing peer via pnpm `packageExtensions` so pnpm links `vitest`
into jest-dom's scope and the augmentation resolves. This is a root-cause fix
at the dependency layer — no hand-written type shim needed.
- Note: `type-check` still has unrelated pre-existing errors and is not yet
gated in CI; this fix only removes the jest-dom matcher category.
## Test Plan
- `pnpm install --frozen-lockfile --filter web` — lockfile stays consistent.
- `pnpm --filter web run type-check` — jest-dom matcher errors drop from 1589
to 0 (remaining errors are unrelated and pre-existing).
- `pnpm --filter web run test` (e.g. `src/shell/WorkspacePanel.test.tsx`) —
15/15 pass under Node 22; runtime is unaffected.
## 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 by comparing `pnpm --filter web run type-check` jest-dom error counts
(1589 → 0) and running the existing vitest suite (unaffected). The change is
dependency-resolution config only, with no new runtime code to test.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
The module docstring only showed the session policy REST API, implying
CEL policies can't be declared statically. Both static paths work and
are now shown: config.yaml policies (handler + factory_params, parsed
by omnigent.inner.loader) and bundled agent specs (guardrails.policies
with a function {path, arguments} mapping, parsed by
omnigent.spec.parser — which does not read factory_params). Verified
both forms against their parsers.
Signed-off-by: Harry Su <tiecheng.su@robinhood.com>
* fix: add codex_cli_version to fake app-servers in tests; fix ruff format
- Add codex_cli_version = None to all _FakeCodexAppServer classes so they
satisfy the new attribute read in the orchestration bypass_hook_trust gate
- Collapse the multiline boolean in orchestration to satisfy ruff format
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: symlink hooks.json into private CODEX_HOME so user hooks fire
hooks.json was never symlinked, so user hooks declared there were silently
ignored in private sessions. Add it to _CODEX_HOME_GLOBAL_INSTRUCTION_FILES
so it's symlinked in full sessions but skipped in minimal_config (title
worker) mode. Trust is no longer a concern since --dangerously-bypass-hook-trust
is passed to runner-owned TUI sessions.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: merge user hooks.json into policy hooks file instead of clobbering symlink
_write_codex_policy_hooks_file was using os.replace() which destroyed the
hooks.json symlink created by _populate_codex_home_config, silently dropping
all user hooks. Now when the path is a symlink, we read the user's hooks,
merge them after the policy hooks for each event (plus any user-only events),
remove the symlink, and write the merged payload as a regular file.
User hooks from ~/.codex/hooks.json now fire in private sessions.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* style: collapse _merge_user_hooks signature to one line (ruff format)
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Scopes the listing to system.ai models only via the parent filter and
raises the result cap to 1000, matching the recommended API call at
/ajax-api/2.1/unity-catalog/model-services?max_results=1000&parent=schemas%2Fsystem.ai.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): scale conversation sidebar text with the font-size setting
The sidebar's compact text was pinned to a fixed `--sidebar-font-size:
13px`, so the Appearance font-size setting only moved the surrounding
rem-based padding while the text stayed at 13px. Express the variable in
`rem` (0.8125rem = 13px at the 16px default) so it rides the root
font-size, which already folds in `--ui-font-scale` and the mobile bump.
Drop the explicit `line-height` on `.sidebar-compact-text`: single-line
rows use fixed height + flex centering (line-height inert), and the two
line-clamped previews now inherit the root's unitless 1.5, which scales
with the text for free.
Co-authored-by: Isaac
* test(e2e-ui): regenerate visual baselines
---------
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
* feat(models): add intent resolver contracts
Define stable model intents and provider-neutral metadata for capabilities, context windows, cost tiers, and wire APIs. Capability support is tri-state so incomplete provider listings cannot be mistaken for positive support.
Add deterministic resolution precedence for explicit choices, configured defaults, live catalogs, and documented static fallbacks. Catalog order remains the tie-breaker, while provider-specific preference policies can override ranking without changing callers.
Expose normalized metadata through model catalog entries and payloads without changing any executor or routing defaults in this slice.
Tests: 108 focused resolver, catalog, and smart-routing tests; staged pre-commit hooks.
Part of #3426
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(models): keep resolver intents caller-backed
Limit the public model intent vocabulary to default, fast, balanced, and powerful because those are the only purposes represented by current callers.
Express tool use, image generation, structured output, and similar requirements through explicit capabilities instead of speculative intent-to-capability mappings. Remove the unused large-context ranking path and update resolver tests and migration guidance accordingly.
Tests: uv run --no-sync pytest -q tests/test_model_resolver.py tests/test_model_catalog.py tests/server/test_smart_routing.py; pre-commit run
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* refactor(models): complete wire API contract
Cover every model-endpoint request shape implemented by the provider adapters by adding Bedrock Converse and naming Gemini generateContent explicitly. Keep native CLI and ACP transports outside the model wire protocol vocabulary.
Clarify that explicit model overrides bypass compatibility constraints, intent tiers are best-effort ranking preferences, and uncatalogued explicit resolutions have unknown family and metadata. Add regression coverage for those semantics and for the complete wire API vocabulary.
Tests: uv run --no-sync pytest -q tests/test_model_resolver.py tests/test_model_catalog.py tests/server/test_smart_routing.py tests/llms/test_openai_adapter.py tests/llms/test_anthropic_adapter.py tests/llms/test_gemini_adapter.py tests/llms/test_vertex_adapter.py tests/llms/test_bedrock_adapter.py tests/llms/test_databricks_adapter.py; pre-commit run
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* docs(harness): revise Phase 1 estimates from runner exploration
Reading the runner dispatch surface (not guessing) changed the shape of the
remaining work, so update the proposal's estimates and plan:
- Split PR 1.5 into a serial runner sub-stack: 1.5a spawn-env (bounded, the
first measurement), 1.5b launch (the epicenter — _auto_create_<x>_terminal
has 11 divergent signatures, so the seam passes a NativeLaunchContext to a
uniform provider.auto_create_terminal(ctx) adapter with pre_launch hooks,
not a single positional call), 1.5c terminal-route.
- Re-scope 1.6 interrupt/stop upward (Med -> Med-High, 2d -> 3-4d): every
handler closes over app-scope state (server_client, resource_registry,
_publish_event, module dicts), so extraction needs a DI context, not a move.
- Revise totals: Phase 1 ~17-25 -> ~20-29 eng-days; overall ~26-37 -> ~29-41
across ~12 -> ~14 PRs; critical path rewritten to the serial runner chain.
- Add a Calibration subsection recording the learning from 1.1-1.3 (additive
PRs come in under estimate; the real cost is test-shape churn; the runner is
the back-loaded risk) and settle the "signature uniformity" open question
with the confirmed finding.
Docs-only; no code paths affected.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* docs(harness): record harness-bench compatibility with native plugins
The harness bench's selection + driver layer is already registry-driven:
manifest.py auto-adds every NATIVE_TUI capability as a BenchProfile and the
NativeTuiDriver is selected generically, so a community native plugin
enumerates and gets a profile with zero bench edits. Record the two remaining
gaps and where they close:
- Provisioning needs registry-driven agent seeding — closed for free by PR 1.7
(the native driver provisions against a pre-seeded <harness>-ui agent).
- Tool-call probe metadata is hardcoded (_NATIVE_TOOL_PROVOCATION) — fold
optional shell_tool_name / shell_tool_prompt capability fields into PR 1.8 so
the probe reads off the registry; until then those probes skip (non-fatal).
Add a "Harness bench compatibility" subsection, extend 1.8's scope with the
tool-probe fields, and give 2.4 a benchable acceptance criterion (the example
plugin runs `python -m tests.harness_bench --harness <plugin> --live` green).
No new phase or standalone bench-migration PR.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(harness): route native resume through the provider seam (PR 1.3)
Collapse the two hand-written native-resume dispatch chains onto
native_dispatch.resolve_hook_for_key(key, "run_native"):
- resume_dispatch._dispatch_wrapper: 10 `if native_agent.key == "<x>"` arms →
one resolved call.
- chat._redirect_native_resume_if_needed: 6 arms + the 6
_run_<x>_native_resume_redirect helpers → one resolved call that derives the
redirect notice from the agent row (wrapper_name == agent.harness,
native_command == agent.key, both verified equal to the old literals) and
passes auto_open_conversation. Deletes the helpers.
Behavior change (intended fix): routing through the seam covers all 11 natives,
closing two latent coverage gaps that double-posted each user turn (the exact
hazard the cursor/kimi docstrings warned about):
- chat redirect covered only 6 of 11 — goose/hermes/antigravity/qwen/opencode
resumes fell through to the Omnigent REPL.
- resume_dispatch covered only 10 of 11 — opencode fell through the same way.
No test pinned either old fall-through; added a chat goose regression test, a
chat unknown-wrapper → False test, and a resume_dispatch opencode test.
Also:
- native_dispatch.resolve is no longer cached — dispatch happens once per
resume/launch/seed, import_module already caches the module, and caching the
resolved attribute silently defeats monkeypatch.setattr("...:run_x", ...),
which the resume/CLI tests rely on. Dropped reset_resolve_cache_for_tests.
- Normalize the cli.py _NativeTerminalDispatchSpec launch table to
args_param="extra_args" (finishing 1.2's spelling migration into the launch
hub) and update the tests that captured the old <x>_args kwarg.
Net -261 lines. Full resume/chat/cli/native suites green; new-failure delta vs.
the clean tree is zero.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* docs(harness): record PR 1.3 in the progress ledger
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
- Gate both approval event and resolve URL paths at owner access
- Prevent shared editors from authorizing tools using owner credentials
Refs #2150
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(harness): normalize native launcher pass-through args (PR 1.2)
The 11 run_<x>_native launchers each spelled their pass-through arg
differently (claude_args, pi_args, ...). The provider seam needs one uniform
spelling to call them generically. Introduce extra_args as that spelling and
keep <x>_args as a back-compat alias.
- Add native_terminal.normalize_extra_args(): reconciles extra_args vs the
legacy <x>_args alias — extra_args wins, the legacy alias emits a
DeprecationWarning (removal targeted for 0.9.0), neither yields ().
- Give all 11 run_<x>_native entry points a keyword-only extra_args and make
<x>_args an optional deprecated alias, normalizing at the top of each body
so the deep internals keep using the existing local variable unchanged.
- Migrate the internal callers (resume_dispatch ×10, chat resume-redirect ×6,
cli_native ×11) to extra_args so nothing in core trips the new warning; the
alias exists purely for external back-compat.
- Tests: unit-cover the four normalize_extra_args branches. Existing native
tests that still call <x>_args= now double as back-compat coverage.
No behavior change: with default warning filters the full native + hub suite
is green (verified the failure set is byte-identical to the clean tree; the
handful of red tests are pre-existing gateway-env artifacts unrelated to this
change).
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* docs(harness): record PR 1.2 in the progress ledger
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
The scheduled server-compat matrix builds its default version set from
all tags, filtering only rcN. Dev/pre tags are snapshots of main, so
main-vs-them cells add no compat signal, and under the 256-job matrix
cap they evict the oldest final releases — the coverage the workflow
exists for. A stray v0.4.0.dev0 tag is already in the live matrix
today, and a nightly prerelease lane would add ~25 such tags a month.
Explicit VERSIONS dispatch overrides still accept prerelease tags.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(repl): don't report an Omnigent credential for ACP-backed sessions
acp / acp:<slug> / goose / qwen aren't in _HARNESS_FAMILY, so
default_provider_for_harness treats them as unmapped and falls through to
the configured anthropic/openai default. describe_active_credential then
hands back that provider's default_model and credential source, and both
the /model readout and the startup header render it as the active model.
But an ACP agent carries its own auth and picks its own model — the
executor only forwards a model at session/new when send_model_in_session_new
is set. So `omnigent run --harness acp:<agent>` confidently names a model
and an API key the session never touches.
Declines these harnesses at the resolver rather than the readout, so the
startup header stops fabricating too. The predicate reads the declared
capability record (ACP_SUBPROCESS + OWN_AUTH) instead of a hardcoded list,
so community ACP plugins are covered without further edits.
Signed-off-by: apeltekci <andrew@peltekci.com>
* fix(repl): scope the own-auth credential decline to acp/goose and keep overrides visible
The own-auth predicate wrongly included qwen: a harness mapped in
_HARNESS_FAMILY is provider-routed at spawn (_build_qwen_spawn_env injects
the configured openai-family default via
configure_agent_harness_with_provider, and QwenExecutor exports
OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL into the qwen subprocess —
see test_qwen_uses_openai_global_default), so its readout naming that
provider was truthful, and declining it fabricated "own auth" in the other
direction. The decline now applies only to unmapped ACP_SUBPROCESS +
OWN_AUTH harnesses (acp/acp:<slug>, goose, unmapped community ACP plugins).
The predicate is public now, so the REPL stops importing a private name,
and the manual acp:<slug> split is gone (canonicalize_harness already folds
it).
The own-auth readout also no longer claims an Omnigent-side /model override
does not reach the agent — model_env_keys() covers acp and goose, the
process manager respawns on a model change, and goose applies the override
as GOOSE_MODEL — and a live override is shown instead of hidden.
Tests: the resolver-level case now uses a key-kind openai default, the kind
the unmapped fallback actually fabricated (a subscription default was
already declined before the fix, so the previous case pinned nothing), and
new cases pin override visibility and qwen's provider-routed readout.
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: Dhruv Gupta <dhruv.gupta@databricks.com>
DELETE /auth/users/{user_id} checked whether another admin existed and
deleted the target in two separate, unlocked transactions. Two
concurrent deletes of two different admins could each observe the
other as the remaining admin, both pass, and both apply, leaving
the deploy with zero admins and no in-app recovery path.
Lock the current admin set before counting it (BEGIN IMMEDIATE on
SQLite, SELECT ... FOR UPDATE on other dialects) so the check and
the delete happen in one transaction. A concurrent delete of a
different admin now blocks until the first commits and re-observes
the up-to-date count instead of a stale one.
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
## Related issue
N/A
## Summary
Bump the Android module's `compileSdk` and `targetSdk` from 35 to 36 to meet
Google Play's requirement that apps target API level 36 by August 30, 2026.
This required updating the full Android toolchain:
- AGP 8.6.1 → 9.1.1 (AGP 9 has built-in Kotlin support)
- Gradle wrapper 8.9 → 9.3.1
- Gradle Play Publisher 3.12.1 → 4.0.0
- AndroidX dependencies to versions compatible with compileSdk 36 (e.g.,
`androidx.core` 1.18.0, `androidx.activity` 1.12.4, `androidx.webkit` 1.15.0)
- Robolectric 4.14.1 → 4.16.1
The `org.jetbrains.kotlin.android` plugin is no longer applied because AGP 9
bundles Kotlin compilation support. Build-script helper tasks that previously
used the Gradle `exec { }` DSL were switched to `ProcessBuilder` to stay
compatible with the new Kotlin/Gradle DSL scope, and `android.sdkDirectory`
was replaced with `androidComponents.sdkComponents.sdkDirectory`.
## Test Plan
Ran the full local Android build pipeline:
```bash
cd web/android
./gradlew :app:assembleDebug :app:lintDebug
./gradlew :app:bundleRelease
./gradlew :app:assembleDebugAndroidTest
```
All completed successfully and produced a debug APK, release AAB, and androidTest
APK with zero lint errors.
## 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 by running `:app:assembleDebug`, `:app:lintDebug`, `:app:bundleRelease`,
and `:app:assembleDebugAndroidTest` locally. The existing CI `android-bundle.yml`
workflow uses the Gradle wrapper and JDK 17, both compatible with the updated
toolchain.
## Changelog
Android app now targets Android 16 (API 36) to stay compliant with Google Play's
latest target API level policy.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
The repo migrated to a pnpm workspace (pnpm-workspace.yaml and
pnpm-lock.yaml at the root, packageManager: pnpm@11.15.1) but
setup.py's _build_web_ui still shelled out to 'npm install' / 'npm
run build' from inside web/. That path looked for a package-lock.json
that doesn't exist there (the lockfile is pnpm-lock.yaml at the
workspace root), so npm re-resolved from package.json alone and
hard-failed on the @lobehub/fluent-emoji@4.1.0 peer range
(react@^19 vs the pinned react@18.2.0) with ERESOLVE.
Migrate _build_web_ui to pnpm, matching deploy/databricks/build.sh
and the CI workflows (.github/workflows/e2e-ui.yml):
- Resolve pnpm via shutil.which('pnpm'), falling back to
'corepack pnpm' (corepack ships with Node 22+ and auto-pins the
version from package.json's packageManager field).
- Run from the workspace root (cwd=root), not web/, so pnpm uses
the committed pnpm-lock.yaml.
- 'pnpm install --frozen-lockfile --filter web' then
'pnpm --filter web run build' — exactly the CI commands.
--frozen-lockfile guarantees the build is reproducible and
resolves @lobehub/fluent-emoji against react@18.3.1 under the
workspace's strictPeerDependencies: false, avoiding the peer
conflict that broke npm.
Also enforce the Node.js 22 LTS floor up front via a new
_require_node_22 helper that fails fast with a dedicated, actionable
message if 'node' is missing or reports < 22 — instead of failing
deep inside the toolchain with an opaque error.
All existing skip/force env vars are preserved:
OMNIGENT_SKIP_WEB_UI=true (opt out), OMNIGENT_BUILD_WEB_UI=1
(force rebuild), skip-when-bundle-exists, skip-when-web-absent.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
N/A
## Summary
- Split `SandboxLauncher` into a layered hierarchy: `SandboxLifecycle`
(lifecycle + capabilities), `SandboxExecTransport` (run/put/stream/exec),
`SandboxHostLauncher` (abstract start_host), and `ExecModelHostLauncher`
(default start_host + run_background + materialize_workspace).
- `SandboxLauncher` is now a backward-compat alias for `ExecModelHostLauncher`.
- Migrated Kubernetes to inherit `SandboxHostLauncher` directly — it no
longer needs a fake `run()` that raises; the entrypoint-as-host model
(Pod boots running the host) has no exec transport at all.
- All 8 providers now declare an explicit `capabilities` property instead
of relying on class-var derivation.
- Updated the registry's `isinstance` guard to check `SandboxLifecycle`
(the common base) so both exec-model and entrypoint-as-host providers pass.
- Updated the Kubernetes test that asserted `run()` raises to assert the
method does not exist instead.
## Test Plan
```bash
uv run pytest tests/onboarding/sandboxes tests/sandbox tests/cli/test_cli.py tests/server -k managed -q
pre-commit run --files <all changed files>
```
All 780 selected tests pass and pre-commit is clean.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Existing provider and CLI tests pass unchanged, confirming backward
compatibility. The Kubernetes test was updated to reflect that `run()` no
longer exists on the launcher. The registry test was updated for the
`SandboxLifecycle` guard message.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
A conversation id pasted with surrounding punctuation (e.g. a trailing
period) crashed `omni resume` with a raw StatementError traceback from
the local store's Uuid16 bind. Strip the punctuation a paste drags
along — none of it can be part of a valid id — and resume the id the
argument contains, canonicalized to bare hex so downstream consumers
never see a legacy spelling. Error only when no valid id remains.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Add a `./gradlew recordScreenshots` task that captures four real-WebView
screenshots of the Android shell on a device/emulator, with zero manual
setup — Gradle starts and stops both the Vite dev server and an isolated
omnigent backend automatically.
Screens captured (app/build/screenshots/):
- server_select.png — native ConnectActivity (server-entry screen)
- home.png — SPA landing page (sidebar closed)
- session_list.png — SPA home with sidebar drawer open (?sidebar=open)
- session.png — session/chat page with a real seeded user message
How it works:
- startBackendServer: launches `omnigent server` in a throwaway mktemp
data dir (OMNIGENT_DATA_DIR/CONFIG_HOME/DATABASE_URI isolated from
~/.omnigent, no-auth on loopback), pre-registers examples/kimi_hello.yaml.
- seedDemoSession: POST /v1/sessions with an initial user message so the
session screenshot has real content.
- startWebDevServer: launches `node vite --host 127.0.0.1 --port 5173`
directly (avoids spawning npm/pnpm whose grandchild is hard to kill),
reuses an existing server if present. Vite proxies /v1 to the backend.
- Per screen: pm clear + pre-grant POST_NOTIFICATIONS, then drive the real
ConnectActivity → MainActivity flow via UI Automator (am instrument, not
AGP's connectedDebugAndroidTest which auto-uninstalls and deletes the
screenshot before we can pull), then adb pull the PNG.
- stopWebDevServer / stopBackendServer: tear down both + clean temp dir.
The test (ScreenshotTest.kt) is pure UI Automator (out-of-process, black-box):
it launches the app from the launcher, types the server URL (base + route
path) into ConnectActivity, taps Connect, waits for the floating switch pill
as the "shell is up" signal, then captures via UiDevice.takeScreenshot. The
session-list screen uses the ?sidebar=open query param (AppShell reads it on
mount to open the conversation drawer) since uiautomator can't see inside the
WebView to tap the toggle button.
Dependencies added (pinned to the AGP 8.6 / compileSdk 35 toolchain):
androidx.test:runner 1.6.2, :rules 1.6.1, ext:junit 1.2.1
androidx.test.espresso:espresso-core 3.6.1
androidx.test.uiautomator:uiautomator 2.4.0
Also sets testInstrumentationRunner = AndroidJUnitRunner.
Usage:
ANDROID_SERIAL=emulator-5554 ./gradlew recordScreenshots
open app/build/screenshots/*.png
Requires an emulator or unlocked device. The backend/Vite are fully managed
— no separate terminals needed.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* fix(release): harden finalize, cut, and homebrew against the 0.7.0-cycle failure modes
- finalize: docs sweep is advisory (never blocks publish), untagged drafts
are rebound automatically, tag input is normalized
- release: bump-main gates in shell so CLI-dispatched boolean inputs cannot
silently skip the post-release main bump
- update-homebrew: defer inside PyPI's 24h --uploaded-prior-to window and
add a nightly catch-up that no-ops when the formula is current
- uv.lock: gitpython 3.1.50 -> 3.1.55 (clears 8 OSV advisories that tripped
the Security Scan on every lock-touching PR)
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix(images): serialize image builds and raise the build timeout to 120m
At the v0.7.0 cut the rc1 (21:51) and final (21:57) tag builds ran
concurrently under SHA-keyed concurrency, raced each other's layer cache
cold, and the final build died on the 60m job timeout — no v0.7.0 or
latest images until a manual re-run a day later. A single serialized
group lets the later build reuse the earlier one's layers; 120m gives a
genuinely cold build headroom.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
N/A
This is the final npm -> pnpm migration step for the OSS repo.
- Adds `editors/vscode` and `deploy/cloudflare` to `pnpm-workspace.yaml` so
they use the root `packageManager: pnpm@11.15.1` and the shared
`pnpm-lock.yaml`.
- Removes the per-package `package-lock.json` files and deletes the now-obsolete
`scripts/normalize_package_lock_registry.py` hook/script.
- Merges the three remaining categories of build-script approvals into
`pnpm-workspace.yaml` (`@vscode/vsce-sign`, `esbuild`, `keytar`, `sharp`,
`workerd`) so `pnpm install` works at the workspace root.
- Migrates VS Code and release workflows to `setup-pnpm`:
- `.github/workflows/vscode-extension-release.yml`
- `.github/workflows/vscode-release-pr.yml`
- `.github/workflows/release-omnigent.yml`
- Updates the lockfile regen workflows to refresh `pnpm-lock.yaml` instead of
the old web-only `package-lock.json`:
- `.github/workflows/oss-regenerate-and-smoke.yml`
- `.github/workflows/oss-regen-on-comment.yml`
- Updates `editors/vscode/README.md`, `editors/vscode/PUBLISHING.md`, and
`deploy/cloudflare/README.md` to reference pnpm commands.
- Removes the deprecated `.github/actions/setup-node` composite action.
- `pnpm install --frozen-lockfile --filter omnigent-vscode` passes locally.
- `pnpm install --frozen-lockfile --filter omnigent-cloudflare` passes locally.
- `uv run pre-commit run --all-files` passes (after dropping the package-lock
registry hook).
- Inspected remaining `npm install` occurrences in workflows; the only survivors
are transient agent CLI installs (`@anthropic-ai/claude-code`,
`@openai/codex`) that are intentionally not tracked in the lockfile.
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
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
Verified the new workspace packages install from the frozen pnpm lockfile and
that the pnpm-only lockfile regen scripts produce a valid lock. The VS Code
workflow commands were checked against the package names/filters from
`pnpm-workspace.yaml`.
N/A
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
The linux_bwrap sandbox mounts a fresh procfs under --unshare-pid, but a
Lakebox microVM masks /proc so that mount returns EPERM and the sandbox
fails to start. That blocked linux_bwrap — and the L7 egress management
built on top of it — on the Lakebox backend.
Bind the existing /proc instead of mounting a fresh one, but only on
outer sandbox backends known to be safe for it (allow-list: lakebox).
The backend is read from OMNIGENT_HOST_SANDBOX_BACKEND when set, else
autodetected via the /run/lakebox marker. Everywhere else the fresh-proc
mount and its fail-closed behavior stay unchanged.
Binding /proc exposes the outer process list and world-readable per-proc
files (cmdline/comm/stat/status). The retained user namespace still
blocks ptrace-gated files (environ/mem/maps/fd) and --unshare-pid still
contains signalling, so the leak is acceptable on a single-tenant
Lakebox microVM.
Signed-off-by: Thomas Garnier <6202935+mxatone@users.noreply.github.com>
The kimi forwarder mirrored wire content but never posted an
external_session_status edge — the only native forwarder that didn't
(claude/codex/opencode/cursor all do). A kimi sub-agent therefore finished,
delivered its answer to the transcript, and left the parent waiting on it
forever: _mark_subagent_terminal_and_wake was never reached, so no result
ever landed in the parent's inbox.
kimi's wire has no turn.end row; its agent loop steps while step.end carries
finishReason 'tool_use' and stops on 'end_turn' (1:1 with turn.prompt across
every recorded session). Map that edge to external_session_status: idle,
carrying the turn's final assistant text — the runner delivers an empty
result when an idle edge forwards none.
Signed-off-by: apeltekci <andrew@peltekci.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(harness): add NativeHarnessProvider seam foundation (PR 1.1)
First, additive step of Phase 1 of the modular native-harness registry
(designs/harness-modular-registry-proposal.md). Introduces the behavior
side-channel that later PRs will dispatch through; no hub is rewired yet, so
this changes no runtime behavior.
- Add `NativeHarnessProvider` (frozen dataclass of dotted import-path strings
for a native harness's lifecycle hooks) and the `native_providers` field on
`HarnessContribution`, plus `native_providers()` / `native_provider_for_key()`
accessors.
- Populate 11 built-in provider rows uniformly from the `omnigent.<key>_native`
module layout (`run_<key>_native`, `_materialize_<key>_agent_spec`, and the
`_auto_create_<key>_terminal` builder re-exported from `omnigent.runner.native`).
Hooks that are still runner closures / inline dispatch (interrupt, stop,
spawn-env, bridge-dir) stay None until those hubs migrate onto the seam.
- Add `omnigent/native_dispatch.py`: a lazy, per-path-cached resolver over the
existing `load_object`, with `resolve` / `resolve_hook` / `resolve_hook_for_key`
so hubs resolve a hook instead of branching on `key == "<x>"`. Import hygiene
preserved — provider rows hold strings; only the resolver imports the target
modules, and only at dispatch time.
- Tests: provider rows cover every native agent 1:1, required hooks are set, and
every populated built-in path actually resolves to a callable (guards against
a typo'd path or renamed symbol); resolver colon/dot forms, caching, and
unset-hook / unknown-key None paths.
The validator still rejects community native metadata (Phase 2 flips it).
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* docs(harness): add implementation-progress ledger (PR 1.1)
Add an append-only "Implementation progress" ledger to the modular-registry
proposal so each PR in the stack records its own status without editing the
plan tables (which would conflict across the 1.1→1.2→1.3 stack on every
rebase). Seed it with 1.1 (#3239, in review).
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* 🔧 chore(lint): Block hardcoded model pins
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* 🔧 chore(lint): Tighten model baseline guard
- Reject duplicate path/model rows so baseline allowances cannot silently accumulate.
- Document heuristic false-negative and multiline-config gaps, plus the bounded full-scan tradeoff.
- Add focused coverage for duplicate baseline validation.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* 🔧 chore(lint): Guard model scan configuration
- Cross-check the pre-commit trigger against the scanner's tracked roots, extensions, exclusions, and allowlist path to prevent silent drift.
- Share the source-extension set across path discovery and scanning.
- Report malformed allowlist counts with consistent path and line context; cover both review cases with focused tests.
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
---------
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(ui): sub-agent sessions never show reconnect modal when runner dies
A sub-agent session with a dead runner classified as local_stranded,
which disabled the composer and showed the CLI reconnect modal — a
flow designed for top-level host-bound sessions. Sub-agents have no
host binding and can't be relaunched from a CLI command; they recover
via their parent's live runner (server-side heal, #3151).
- Add kind field ("default" | "sub_agent") to Session type and
map it from the wire in sessionFromWire
- Thread kind through LivenessRow and livenessRowFromSession
- Add row 7a in useSessionLiveness: sub_agent with dead runner →
runner_asleep (composer open) instead of local_stranded
Fixes#3413
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
# Conflicts:
# web/src/hooks/useSessionLiveness.ts
* fixup: add kind and backgroundTaskCount to sessionsApi test fixture
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test(e2e_ui): sub-agent dead runner keeps composer open, no reconnect modal
Regression test for #3413: a sub-agent session with a dead runner was
classified as local_stranded, showing the CLI reconnect modal and
disabling the composer. After the fix (kind=="sub_agent" → runner_asleep)
the composer stays enabled and the "Agent disconnected" banner is absent.
Creates a real child session (parent_session_id set → kind="sub_agent"),
patches the browser's health poll to report runner offline, and asserts
the composer is usable and no reconnect banner appears.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fixup: splice kind from session snapshot into livenessRow when sidebar conv present
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fixup: expose kind in SessionResponse so the UI can detect sub_agent sessions
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fixup: don't re-initialize session on heal — parent runner already hosts the child
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fixup: re-init session for native sub-agents, skip for SDK sub-agents
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fixup: regenerate openapi.json for kind field in SessionResponse
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fixup: update heal docstring + add SDK sub-agent no-init test
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* 🐛 fix(server): heal sub-agent stale runner_id on message-send
A sub-agent copies its parent's runner_id at creation and is never
repointed when the parent's runner is relaunched. The message-send path
returned a permanent 503 for any sub-agent whose runner had
idle-timed-out, even while the parent's replacement runner was healthy
(host_id is None short-circuits all existing relaunch paths).
- Extract _heal_subagent_runner_binding_via_parent from
_recover_subagent_status_forward_via_parent: walks the ancestor chain
(immediate parent → root), waits for the live runner tunnel, calls
replace_runner_id on the child, returns the live client
- Wire the heal into the message-send path after the managed-launch
rendezvous, guarded to kind=="sub_agent"; sets
_runner_needs_session_init=True so the child's harness is initialized
on the healed runner before dispatch
- Refactor _recover_subagent_status_forward_via_parent to delegate
binding repair to the shared helper (no behavior change for the
status-forward path)
- Add regression tests: heal succeeds, no-live-ancestor preserves 503,
top-level sessions not treated as recoverable children
Fixes#3067
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
# Conflicts:
# omnigent/server/routes/sessions.py
* fixup: rebase onto main, apply heal to routes_events.py, fix lint
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fixup: fix test payload format and monkeypatch targets for routes_events
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The AI-agent workflows install the Claude Code / Codex CLIs with a bare
`npm install` after `cd`-ing into a workspace subdir (`.cc-cli` / `.codex-cli`)
that has no package.json of its own. npm then walks up to the nearest ancestor
package.json to resolve the project root.
Once a repo-root package.json was added, that ancestor became the repo root, so
the install landed in `${GITHUB_WORKSPACE}/node_modules` instead of the subdir.
The follow-up `node node_modules/@anthropic-ai/claude-code/install.cjs` (run from
the empty subdir) then failed with MODULE_NOT_FOUND, breaking Polly review,
issue/security triage, doc-sync, and the run-omnigent-agent action. The
`added 2 packages` line (claude-code has zero deps) was the tell that npm had
reconciled the root tree rather than an isolated install.
Install into `${RUNNER_TEMP}/omnigent-{cc,codex}-cli` instead — outside the
checked-out tree, so no ancestor package.json can ever capture the install. This
matches the pattern e2e-ui.yml and flake-stress-ui.yml already use.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* ci: bump @anthropic-ai/claude-code 2.1.170→2.1.212
v2.1.170 has a corrupted npm cache entry on GitHub Actions runners
causing install.cjs to be missing after `npm install`. Bumping to the
current stable (2.1.212) forces a fresh fetch and clears the bad entry.
Also bumps the ci-deps/package.json pin (was 2.1.163) and the
run-omnigent-agent action default to keep everything consistent.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* ci: update pnpm-lock.yaml for claude-code 2.1.212
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Codex materializes its versioned plugin store (openai-curated templates,
browser, presentations, ...) into $CODEX_HOME/plugins/cache on session
start. Because codex-native points CODEX_HOME at a private per-session
home, codex re-materializes ~44 MB of identical plugin data into every
session — the dominant on-disk cost once the upstream logs_2.sqlite TRACE
bloat (openai/codex#28224) is fixed in codex >= 0.142.0.
Symlink plugins/cache from the shared source home into each private home,
mirroring the existing skills-symlink pattern. The cache is content-
addressed read-only reference data (verified byte-identical to the shared
copy), so unlike config.toml it needs no per-session isolation. Skipped in
minimal (title-sidecar) mode, which runs no plugins. Best-effort: a symlink
failure logs and lets codex repopulate its own copy.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
- Launch bare agy for Google OAuth and verify with agy models\n- Keep Gemini API-key setup available alongside native sign-in
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
Running a full workspace install without filters complained about ignored
build scripts for @anthropic-ai/claude-code, @google/genai, and protobufjs.
These come from the .github/ci-deps package and are legitimate; approving
them lets Scope: all 4 workspace projects
Already up to date
Done in 194ms using pnpm v11.15.1 / undefined
[ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL] Command "dev" not found at the workspace root run scripts
instead of erroring.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* fix(claude-native): never launch a bare family alias a gateway rejects
A family alias (opus/sonnet/haiku/fable) selected on a provider config
whose tier has no ANTHROPIC_DEFAULT_*_MODEL pin is canonicalized by
Claude Code to an Anthropic id (e.g. claude-opus-4-8) that gateways
404, failing session start with "There's an issue with the selected
model". Resolve unpinned aliases to the provider's default model in
resolve_claude_native_model_selection, which launch, sticky handoff,
and /model injection all route through.
Also stop offering the static subscription alias rows to provider
configs with no pins: the picker now lists the one model the config is
known to route.
Co-authored-by: Isaac
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
* refactor: trim the unpinned-alias fix to its minimal form
Shorten the resolver docstring and the pin-less catalog fallback, drop
e2e assertions already implied by the single-row count, and fold the
three alias-passthrough regression tests into one.
Co-authored-by: Isaac
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
* fix(claude-native): scope alias remap to endpoints that reject canonical ids
Review feedback on the unpinned-alias guard:
- Only rewrite an unpinned family alias when the config routes through a
gateway/Bedrock endpoint; the Anthropic API (api.anthropic.com or no
endpoint override) resolves aliases natively, so API-key providers keep
their alias routing and the static picker catalog.
- Respect managed-settings tier pins: Claude Code applies them to the
spawned process, so a managed pin means the alias still routes.
- The runner's /model handler now resolves the session launch config
instead of reading the in-memory cache, so alias resolution survives a
runner restart (cold cache previously skipped the remap).
Co-authored-by: Isaac
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
---------
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
- Add .github/ci-deps to the root pnpm workspace so it uses the shared
pnpm lockfile and install machinery.
- Regenerate pnpm-lock.yaml entries for the e2e-ci-deps package.
- Replace npm install --ignore-scripts in ci.yml and flake-stress-e2e.yml with
pnpm install --frozen-lockfile --ignore-scripts --filter e2e-ci-deps.
- Update electron-build.yml to use setup-pnpm and filter installs for web and
web/electron.
- Update omnidev source so the local dev supervisor installs and runs Vite
with pnpm.
- Update developer docs (README.md, CONTRIBUTING.md, web/README.md,
web/electron/README.md, dev/omnidev/README.md, tests/e2e_ui visual/README.md
and COVERAGE_GAPS.md) to reference pnpm commands.
- Add a minimal root package.json with packageManager: pnpm@11.15.1 and remove
the explicit version from .github/actions/setup-pnpm so CI uses the same
source of truth.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* fix(onboarding): detect agy settings.json as login fallback on macOS
On macOS, agy 1.1.7+ stores OAuth credentials in Keychain and writes
only ~/.gemini/antigravity-cli/settings.json (no oauth_creds.json).
The existing gemini_auth_has_credential() missed this and falsely
reported 'harness antigravity-native is not configured'.
Accept the existence of settings.json as a fallback signal when no
token files are found. This is safe because the caller
(resolve_native_antigravity_launch) uses it only for an informational
warning — agy always re-drives OAuth on first run regardless.
- Update gemini_auth_has_credential() with settings.json fallback
- Update docstrings to document the third detection path
- Update warning message in antigravity_native_launch.py
- Add unit test for settings.json-only detection
- Fix _GEMINI_DIR isolation in existing test
Signed-off-by: ElliotSun <elros1109@gmail.com>
* fix(onboarding): prove agy login via CLI, not settings.json existence
The macOS lockout this fixes is real: agy 1.1.7+ keeps OAuth in the
Keychain and writes no token file, so the file-only check reported
antigravity-native as unconfigured and connect.py refused to spawn a
runner for a user who was in fact signed in.
Accepting the bare existence of ~/.gemini/antigravity-cli/settings.json
as the fallback signal does not work, because omnigent creates that file
itself: the CLI launch path calls ensure_agy_feedback_survey_disabled
under the real home before agy starts, and build_agy_launch emits no HOME
override. One `omni antigravity` run therefore satisfied the credential
gate forever, on every platform — turning a hard launch gate into a
no-op and letting a runner spawn that dies on its first turn. That is
worst on headless hosts, where agy's OAuth prompt has no TTY.
Ask the CLI instead. `agy models` exits 0 only when signed in and reads
the credential wherever agy stored it, Keychain included, so nothing
omnigent writes can satisfy it. This mirrors ambient._claude_login_detected,
which already solves the identical Keychain split for Claude Code, and
reuses the probe harness_install already wires as the gemini family's
status command.
The fallback is gated on macOS: Linux writes a real token file, so its
absence is a true negative there and the fallback would only add a
subprocess while weakening a signal that works. Failures — missing
binary, non-zero exit, timeout, unreadable home — all read as False,
because readiness must never raise.
Content inspection of settings.json was the alternative considered. It
was rejected as unverifiable from here: no key in that file is known to
mark a completed sign-in on 1.1.7, so keying on one risks reintroducing
the very lockout being fixed.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
* docs(skills): note agy's macOS Keychain credential in the e2e pre-flight
The pre-flight tells the reader agy's token lives under ~/.gemini, which
leaves a Mac developer on agy 1.1.7+ hunting for a file that is never
written. Name the Keychain case and the `agy models` fallback that
gemini_login_detected() now uses there.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
---------
Signed-off-by: ElliotSun <elros1109@gmail.com>
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Co-authored-by: SabhyaC26 <sabhyachhabria@gmail.com>
* fix(cursor-native): auto-accept lingering tool gates under --yolo
cursor-agent's Run Everything mode still sometimes leaves pendingToolCall
markers long enough for Omnigent to mirror ApprovalCards and stall a
piloted parent. When the session launched with --yolo/--force/-f, accept
those tool gates in-pane instead of parking a web card; AskQuestion still
surfaces as deliberate human input.
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Sam Armstrong <armstrongflg@gmail.com>
* fix(cursor-native): satisfy ruff format and PIE810 on yolo args
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Sam Armstrong <armstrongflg@gmail.com>
* fix(cursor-native): make yolo auto-accept bounded and fail-closed
Auto-answering a tool-approval gate is a safety boundary, so the accept path
now refuses to act on anything it cannot confirm, and always has a way out.
The accept was previously a blind keystroke loop: it never checked that a
prompt was on screen, recorded a send to a dead pane as a success, and had no
attempt cap or fallback. A gate that `y` does not clear therefore degraded from
a visible stall into a literal `y` typed into cursor's composer every two
seconds for the life of the session, with no card ever surfaced.
The accept key now goes out only while `capture_cursor_pane` shows cursor's
parenthesised accept hint, at most three times, and at most once per poll pass
(cursor renders one prompt at a time). A dead pane, a send tmux rejects, or a
gate still pending after the budget all fall back to the same ApprovalCard the
non-yolo path shows, so the worst case is the visible stall we have today.
Because a call accepted this way is never seen by a human, the INFO line now
carries an argument preview: it is the only record Omnigent approved the call.
`cursor_launch_args_enable_yolo` was failing open in the same spirit —
`--yolo=false` and `--force=false` both read as enabled, because only the
presence of the `=` form was checked. Explicit off-values are now honoured, and
a bare `--` ends the flag scan so a `-f` in the prompt text that follows is
text rather than a request to bypass approvals.
Tests cover the bounded retry, the fallback to a card, an idle pane, a dead
pane, an undelivered keystroke, an explicit non-yolo session, and the
off-value / end-of-flags argv cases. The design doc gains a section on the
fail-closed contract and drops its claim that Omnigent never sends a verdict of
its own initiative; its stale `Code:` pointer at the runner wiring is refreshed
to where that wiring now lives.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
* fix(cursor-native): re-apply yolo wiring where auto-create now lives
`_auto_create_cursor_terminal` moved out of `omnigent/runner/app.py` into
`omnigent/runner/native/orchestration.py`, which left `app.py` a re-export
shell and this branch's wiring hunk applying to code that no longer runs.
Derive `auto_accept_approvals` from `launch_config.terminal_launch_args` at the
live call site instead.
This kwarg is the only thing that turns the in-pane auto-accept on, and it is
one line inside a large function, so a future move can drop it and leave the
feature inert with the whole suite green. Pin it: the auto-create harness now
captures the elicitation supervisor's kwargs, and a parametrized test asserts
the derived stance for `--yolo`, `--force`, `-f`, `--yolo=false`,
`--auto-review`, and no args. Deleting the kwarg fails all six.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
---------
Signed-off-by: Sam Armstrong <armstrongflg@gmail.com>
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: SabhyaC26 <sabhyachhabria@gmail.com>
The runDebug, listDevices, and reverseProxy Exec tasks called
`commandLine("adb", ...)`, relying on adb being on PATH. The Gradle
daemon is long-lived and may have been started from an environment
whose PATH doesn't include platform-tools (e.g. homebrew's
android-commandlinetools), so the spawn fails with
"A problem occurred starting process 'command 'adb''" — even though
AGP's own installDebug succeeds because it resolves adb from the
SDK directory internally.
Resolve adb from android.sdkDirectory instead, mirroring AGP, so
the custom launch tasks are independent of the daemon's PATH.
* docs(DBSPEC): remove stale DBOS/tasks references
The tasks table and DBOS were removed (migration
b9c1d2e3f4a5_drop_tasks_table), but DBSPEC.md still described the
old DBOS-backed workflow design: the tasks table schema, the
try_deliver/close_inbox steering handshake, and the TaskStore
method mapping. Updated the doc to match current state — turn
state now lives in-memory in the runner (_active_turns,
_session_message_buffers), and conversation_items.response_id is
just an app-generated grouping id with no backing table.
Also added the created_by column to conversation_items, which
existed in code but was missing from the doc.
Signed-off-by: Harry Su <tiecheng.su@robinhood.com>
* docs(DBSPEC): correct FK section — no DB-enforced FKs, cleanup is explicit app code
Addresses the blocking review: the previous revision claimed an ON DELETE
CASCADE FK on conversation_items.conversation_id, but
p1a2b3c4d5e6_remove_all_fks dropped every FK (Rule R032) and
delete_conversation cleans up children before parent explicitly. Also
precision-fix response_id as harness- or app-generated per review.
Signed-off-by: Harry Su <tiecheng.su@robinhood.com>
* docs(DBSPEC): correct table count, deletion order, and position allocator
The accuracy pass left five claims that don't match the code:
- The opening line said four tables in the default schema. There are 17
in `db_models.py`, and none sets an explicit schema — the same doc names
labels, comments, and policies as tables a hundred lines later. Scope the
sentence to the four tables this doc covers and point at the models as the
full list.
- `delete_conversation` was described as deleting comments and policies
before the conversation rows. It uses two transactions: the AP one drops
FTS rows, items, labels, and the conversation rows; a second best-effort
transaction then cleans up comments, policies, session permissions,
conversation metadata, and session-scoped agents *after* the conversation
is gone. The doc also omitted three of those tables and hid the
best-effort tradeoff the method's own docstring calls out.
- "Turn state is not persisted to this schema at all" was overstated. The
authoritative state is in-memory, but `persist_live_status` mirrors
`live_status` / `pending_elicitation_count` onto
`omnigent_conversation_metadata` so any replica can render session status.
- The "Delete agent" row documented cancelling in-flight turns for the
agent's live sessions. No such mechanism exists: `AgentStore.delete` is a
bare row delete with no production caller and no HTTP route, and
session-scoped agent rows are removed by `delete_conversation`.
- The position allocator no longer runs `SELECT MAX(position) + 1`.
`append()` reads and advances the `conversations.next_position` counter
under `_lock_conversation`, keeping allocation O(1); the `MAX(position)`
scan survives only as a one-time backfill for pre-counter conversations.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
---------
Signed-off-by: Harry Su <tiecheng.su@robinhood.com>
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Co-authored-by: SabhyaC26 <sabhyachhabria@gmail.com>
- Define a root pnpm-workspace.yaml with web/ and web/electron/ packages.
- Move npm overrides from web/package.json into workspace overrides, using a
shared catalog: for react, react-dom, and shiki.
- Preserve 7-day dependency cooldown via settings.minimumReleaseAge: 10080.
- Delete web/package-lock.json and web/electron/package-lock.json; add the
generated root pnpm-lock.yaml.
- Update web/electron/package.json scripts to use pnpm --filter web run build:overlay.
- Remove web/.npmrc and web/electron/.npmrc; no committed .npmrc (CI forces the
public registry via env var).
- Add .github/actions/setup-pnpm so all workflows can share a pinned pnpm
11.15.1 + Node setup.
- Convert lint.yml and web-tests.yml to pnpm; update ui-snapshot and e2e-ui
workflows.
- Update the web-prettier pre-commit hook to run web/node_modules/.bin/prettier
directly when present.
- Update justfile to prefer pnpm for Electron recipes and lockfile normalization.
- Ensure remaining npm-based workflows (editors/vscode/, .github/ci-deps/,
deploy/cloudflare/) are untouched and continue to work.
- Add pdfjs-dist worker URL import so Vite emits the worker asset under pnpm's
hoisted node_modules layout.
- Force shiki and its first-party packages into a single build chunk to avoid a
Cyclic top-level import that produced a 'flatMap' runtime error in Monaco.
- Pin build-tool versions to the legacy npm lockfile (vite 8.1.0, tailwindcss
4.3.1, jiti 2.7.0, lightningcss 1.32.0, postcss 8.5.15) so bundler behavior
stays consistent with the pre-migration builds.
- Update tests/e2e_ui/test_pwa_build.py to omit the now-incorrect -- separator
when forwarding --outDir to pnpm run build:embed.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
On a claude-native session with intelligent routing on, the routed model
was selected but the user's first message was silently dropped — the model
switched, no error surfaced, but no turn ran.
The server issued TWO unsynchronized writes to the same tmux pane: a
standalone model_change event (which typed /model <routed> into the pane)
AND, separately, the user's message (typed in via inject_user_message).
These raced. The message keystrokes landed mid-switch, inject_user_message
never saw its draft, hit its submit-blind fallback, and returned without
error. Model applied, message gone.
Fix: remove the second writer by folding the switch into the message turn,
mirroring how the SDK/pi path already applies the routed model as one
operation.
- Executor (ClaudeNativeExecutor.run_turn): the routed model already
arrives in ExecutorConfig.model and was being discarded. It is now
applied: when config.model differs from the pane's model, type /model
then inject the message — both under the existing _inject_lock, in
order, exactly once. inject_user_message's prompt-ready gate + verified
submit then guarantee delivery. _applied_model is seeded lazily from
read_launch_model so turn 1's routed pick is compared against the spawn
model rather than blindly re-issued.
- Server (_sessions/orchestration.py): the routed model rides in-band on
the message (model_override, an extra field the harness MessageEvent
forwards into ExecutorConfig.model), and the separate racing model_change
POST is dropped. The manual composer /model picker path (PATCH ->
model_change) is untouched.
Adds three executor tests: /model precedes the message in order under one
lock; no /model without a routed model; no /model when already on the
routed model. The ordering test fails against the prior discard-config
behavior.
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* Bump version to 0.8.0.dev0
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore(release): keep uv.lock at main's shape, stamp workspace versions only
The bump workflow's full relock rewrites every entry with new-uv metadata
churn; restoring main's lock and stamping just the workspace versions keeps
the PR reviewable. Workspace package blocks verified identical to the
relocked version.
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>
* fix(codex): attribute per-model usage for turns with no pinned model
codex_executor's TurnComplete.usage never carried a "model" field, unlike
every other relay executor (claude-sdk, cursor, copilot, openai-agents,
pi). For a codex-harness agent that pins no llm.model (e.g. Debby's
gpt head, which deliberately defers to the harness/provider default),
_accumulate_session_usage's model-resolution fallback chain had nothing
to resolve to, so the turn's flat token/cost totals still accumulated
but session_usage.by_model silently never got an entry for it.
Stamp the turn's resolved model (already in scope as run_turn's `model`
argument) onto the usage dict extracted from tokenUsage/updated, mirroring
claude_sdk_executor's observed_model pattern.
Signed-off-by: Sato Taiga <antfgxgres@gmail.com>
* test(sessions): add regression test for codex per-model usage attribution
Exercises the real _accumulate_session_usage and GET /v1/sessions/{id}
API against a codex-harness agent with no pinned llm.model (Debby's gpt
head's exact shape): a usage delta with no "model" key still accumulates
the flat total but leaves by_model empty (the bug), while one carrying
"model" (as codex_executor.py now stamps it) gets a by_model entry that
also surfaces through the session snapshot the web UI's cost panel reads.
Signed-off-by: Sato Taiga <antfgxgres@gmail.com>
---------
Signed-off-by: Sato Taiga <antfgxgres@gmail.com>
N/A
- Added `min_version` and `max_version_exclusive` to `HarnessInstallSpec` and made `harness_cli_installed` probe `--version` when bounds are declared, so setup and dispatch fail loud for outdated CLIs.
- Implemented generic `--version` parsing + PEP 440 comparison with date-version normalization so Cursor and Hermes calendar-version strings compare correctly.
- Wired code- and changelog-derived version floors for all CLI-backed native harnesses (e.g. Claude >=2.1.161, Codex >=0.137.0, Cursor >=2026.06.02, Kimi >=1.47.0, Hermes >=2026.06.05).
- Updated the CLI setup overview and install prompt to show "Needs upgrade" and the detected/declared versions instead of claiming a present-but-outdated CLI is "not installed".
- Added the `version-too-low` readiness reason and surfaced it in the web UI badge/notice; also made Cursor native auth-aware so it now reports `needs-auth` when installed but not logged in.
- Fixed the readiness-layer lookup so `version-too-low` correctly surfaces for all native harnesses that declare a version floor (Claude, Cursor, OpenCode, Kiro, etc.) instead of falling back to `binary-missing`.
- Preserved the existing `antigravity-native` credential gate: an installed `agy` CLI without a stored Gemini credential still reports not-ready.
- Added E2E UI coverage for the new `version-too-low` warning and updated readiness unit tests for version-bound and credential-bound behavior.
```bash
uv run pytest tests/onboarding/test_harness_install.py \
tests/onboarding/test_harness_readiness.py \
tests/cli/test_configure_models.py \
tests/test_codex_native.py -q
npm run --silent test -- --run src/lib/harnessSetup.test.ts src/shell/NewChatDialog.test.tsx
```
N/A — the change is mostly backend/UX copy; no new visual components.
- [x] Feature
- [ ] Bug fix
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [x] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
Manual verification: ran targeted backend/web test suites after each change and confirmed `omnigent setup`/`harness_cli_installed` now report “installed (vX) but not supported” rather than “missing” for outdated CLIs.
Native harness setup now checks the installed CLI version and prompts to upgrade if it is too old.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
The MITM egress proxy verifies upstream TLS against the system trust
store built by _system_ca_bundle(). It read only the consolidated
cafile (get_default_verify_paths().cafile/openssl_cafile) and ignored
the capath directory. Corporate MDM / IT-managed roots are commonly
installed as loose files under capath (with hashed symlinks) rather than
merged into the cafile, so they were missing from the proxy's trust
store. Any upstream host whose chain relies on such a root then failed
verification (e.g. a corp-intercepted github.com returned 502 from the
proxy) even though the host's own tools trusted it.
Read capath too: concatenate the loose PEM certs from the capath
directory onto the cafile bundle (dedup by resolved path, skip non-PEM
entries), keeping the certifi fallback when neither yields any certs.
Added tests: a CA present only as a loose capath file lands in the
bundle, and non-PEM files in capath are skipped.
Signed-off-by: Nishith Sinha <308961816+nhsdb@users.noreply.github.com>
Co-authored-by: Nishith Sinha <308961816+nhsdb@users.noreply.github.com>
Tools invoked by generic name (awk, python3, editor, pager, ...) resolve
through /usr/bin/<name> -> /etc/alternatives/<name> -> real binary. The real
binaries already live under the mounted /usr, but /etc/alternatives was not
bound, so the intermediate symlink node was missing inside the jail and the
lookup failed with 'command not found'.
Bind /etc/alternatives read-only in the default _DEFAULT_ETC_DIRS list,
alongside the existing /etc/ssl and /etc/ca-certificates dir binds. It is a
directory of symlinks (no secrets); read-only means the mapping cannot be
repointed, and every target is a binary already exposed under /usr, so this
grants no new capability -- it only restores standard name resolution.
Linux (bwrap) backend only; darwin_seatbelt is unaffected by this mechanism.
Signed-off-by: Nishith Sinha <308961816+nhsdb@users.noreply.github.com>
Co-authored-by: Nishith Sinha <308961816+nhsdb@users.noreply.github.com>
Native harnesses (claude-native / codex-native) report a cumulative
SESSION total, not a per-model split. `_persist_native_cumulative_usage`
SET each active model's `by_model` bucket to the whole running total, so a
session that switched models mid-run double-counted the shared baseline:
the previous model kept its last cumulative snapshot while the new model
was set to the full total, and summing the buckets exceeded the session
total (e.g. total $11.91 but opus $10.80 + sonnet $11.91).
Attribute only each report's growth (new - old) to the currently-active
model instead, mirroring the relay path's per-model delta accumulation.
Per-model token and cost buckets now hold each model's own usage and sum
to the flat session total across model switches. Deltas are clamped >= 0
so a lowered / rebased report never claws usage back out of a bucket (the
flat totals are likewise monotonic-clamped).
Read-only reporting (`omni usage`, the web session sidebar) needs no
change — it reads `by_model` verbatim, so corrected data flows through.
Existing sessions keep their already-stored buckets; this corrects
attribution for turns recorded after it ships (not backfillable).
Co-authored-by: Isaac
Signed-off-by: Jakub Majorek <majorek.jakub@gmail.com>
* feat(web): add a harness credential from the New Chat setup dialog (M3 frontend)
Frontend for Setup From the Web UI — turn a yellow needs-setup harness
green from the browser (Claude/Codex/Pi) via an inline equal-weight auth
form (adopt / subscription signpost / API key / gateway), plus the setup
dialog UX cleanups. Gated behind the existing harness_install_enabled cap.
Rebased onto latest main (the M3 backend #3088 is now upstream, so only
web/ + follow-up backend fixes remain) and folded in the Polly review
notes: stable option keys, clear secret fields on save, and a note that
default_model/wire_api are backend-accepted but reserved for a follow-up.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(web): scope useHosts refocus-refetch to the setup flow (Polly review)
staleTime:0 + refetchOnWindowFocus was app-wide across ~8 useHosts
consumers, bumping /v1/hosts volume on every refocus. Make it an opt-in
refetchOnFocus flag; only the setup dialogs (NewChatDialog, HarnessSetupDialog)
that need live readiness recovery pass it. Others keep the 30s stale window.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(web): guard the credential form against double-submit + close test gaps
Address Pat's review:
- Gate both form onSubmit handlers on !busy so hitting Enter in the field
during an in-flight save can't re-POST the secret (the Save button was
already disabled, but the keyboard path wasn't guarded).
- Add a double-submit-guard test, plus direct hook tests for
useStoreCredential (path/body split, JSON detail + non-JSON error parse,
cache patch + detect invalidation) and useDetectedCredentials
(GET/parse, empty-body fallback, enabled/host gating).
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(web): let Pi adopt an openai-family credential too (Polly review)
Pi consumes both anthropic and openai and the daemon adopts a detected
credential under its OWN family, so a host with only $OPENAI_API_KEY could
back Pi — but the adopt filter scoped to Pi's single write-default family
(anthropic), hiding that affordance. Add harnessCredentialAdoptFamilies
(Pi -> both families) and filter the adopt row on it; the paste/gateway
paths and the cross-family guard for Claude/Codex are unchanged.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
---------
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(codex-native): carry hook trust across private CODEX_HOME copy
When codex-native provisions a per-session private CODEX_HOME and copies
config.toml into it, the [hooks.state] keys inside the copy still reference
the global ~/.codex/ paths. Codex keys trust records by the absolute path of
the hooks file, so every key misses and Codex opens an interactive "Hooks need
review" prompt on every launch. Headless sub-agents can never answer it, so
the app-server never emits thread/started and the run dies on the 15s timeout.
Fix: two changes to _populate_codex_home_config:
1. Symlink hooks.json from the global home into the private home (alongside
auth.json). This makes the user's hooks reachable at the private path.
2. After copying config.toml, rewrite [hooks.state.*] key path prefixes from
source_dir to target_dir. The hash values are left untouched, so trust is
neither widened nor weakened — it is only carried across the copy that
Omnigent itself performs.
Fixes#3268.
Signed-off-by: Tomu Hirata <tomu@omnigent.ai>
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: gate hooks.json symlink on not minimal_config; drop redundant re import
The minimal_config path rebuilds config.toml from scratch with only
model_provider/model_providers/profiles — no [hooks.state] entries.
Symlinking hooks.json there with no trust state re-introduces the
interactive trust prompt for the title worker. Gate the symlink (and
the trust-key rewrite that gives it meaning) on not minimal_config.
Also remove the redundant `import re as _re` inside
_retarget_codex_hook_trust_keys; re is already imported at module level.
Addresses Polly review feedback on #3343.
Signed-off-by: Tomu Hirata <tomu@omnigent.ai>
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(codex-native): flush accepted hook trust back to global config on close
When a user accepts the hook-trust prompt inside a session, Codex writes
[hooks.state] entries into the per-session private config.toml — but those
are discarded when the session ends because the private CODEX_HOME is
ephemeral. So the prompt reappears on every launch.
Fix: in CodexNativeAppServer.close(), call _merge_codex_hook_trust_back to
read [hooks.state] from the private config.toml, translate the path keys
from the private home back to the global ~/.codex/ prefix, and upsert them
into ~/.codex/config.toml atomically. The next session's _populate_codex_home_config
copies the global config (now with the trust entries), and
_retarget_codex_hook_trust_keys translates the paths forward to the new
private home — so Codex sees the hooks as already trusted and skips the prompt.
The write is best-effort: any failure is logged as a warning rather than
raised, since the session has already ended.
Fixes#3268.
Signed-off-by: Tomu Hirata <tomu@omnigent.ai>
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: assign tmp before try block to avoid unbound variable warning
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu@omnigent.ai>
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(claude-sdk): surface ResultMessage is_error as ExecutorError, not assistant text
When the Claude SDK reports a harness-level failure (e.g. an expired
login or unauthenticated session), the terminal ResultMessage carries
is_error=True and the failure text in result. The executor was ignoring
is_error and assigning result directly to response_text, so the error
appeared in the conversation as though the model had said it — with no
error item, no harness attribution, and no log line.
Fix: check is_error before touching response_text. When true, set
terminal_error (the existing path that yields ExecutorError and returns)
and log an error line naming the agent. When false, the existing
response_text assignment runs unchanged.
Also add is_error to _ResultMessageObj so the Protocol matches the
SDK's actual shape (it was only declared on _ToolResultBlockObj before).
Closes#3282
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(claude-sdk): use getattr for is_error, handle null result, add unit test
Address Polly review feedback on #3342:
- Use getattr(result_msg, 'is_error', None) instead of direct attribute
access so that existing test doubles that only set session_id/result
don't raise AttributeError (matching the sibling getattr calls for
session_id and usage in the same block).
- When is_error=True but result is None/empty, fall back to a generic
'claude-sdk harness error' message rather than silently dropping the
failure.
- Add test_result_message_is_error_yields_executor_error: verifies that
a ResultMessage with is_error=True is routed to ExecutorError and does
not appear in TurnComplete.response.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(test): wrap long assertion string to satisfy ruff E501
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): stop short links collapsing table columns in chat markdown
Streamdown styles links with `wrap-anywhere` (overflow-wrap: anywhere),
which also drops the element's min-content width to a single character.
Inside its `table-layout: auto` table that let a link-only column be
squeezed to ~2ch, so a short link like "#3090" stacked one or two
characters per line while the prose columns took all the width.
Narrow links inside table cells to `break-word`: overlong URLs still
soft-wrap, but min-content stays at the longest unbreakable run so the
column can no longer be squeezed below it. Prose links keep `anywhere`.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* test(e2e-ui): guard markdown table link column width in the browser
The CSS fix for the collapsing "PR #" column is only observable with a
layout engine, so the vitest companion can pin the rule and its selector
scoping but not the width. This adds the browser-side half: a seeded
assistant message renders the table shape that triggered the bug — a
link-only `#` column, wide prose columns, and a full-URL column — and
asserts the short link stays on one line box, its cell is at least as
wide as the link, and a long URL still soft-wraps inside its cell.
Verified against the pre-fix stylesheet: `#3090` stacks across 5 line
boxes without the `overflow-wrap: break-word` narrowing, 1 with it.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(seatbelt): resolve two-hop proxy symlink when detecting CPython install root
When a Python interpreter is installed via `uv tool install`, the
executable is a two-layer symlink:
~/.local/share/uv/tools/<pkg>/bin/python → (proxy)
~/.local/share/uv/python/cpython-3.12.X-.../bin/python3.12
The literal proxy path grandparent (`tools/<pkg>/`) has no CPython
`lib/python*` markers, so `_interpreter_install_root` returned None.
`_add_topmost` then raised OSError before ever checking the resolved
path, causing every session to fail with:
darwin_seatbelt: helper interpreter at '.../uv/tools/omnigent/bin/python'
resolves under the unsafe ancestor '/Users'; ...
Fix: in `_add_topmost`, when the literal path yields no install root,
resolve it one level and retry `_interpreter_install_root` on the
resolved path before giving up. The resolved CPython install root
(which does carry the canonical markers) is then granted as the narrow
subpath, matching the existing behaviour for direct uv-python installs.
Also update the OSError message to say 'CPython install root' and note
that both the literal and resolved path were tried, and fix the
matching assertion in the existing test.
Fixes#3237.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(seatbelt): grant pi_dir and $TMPDIR write root so sandboxed pi can boot
Two follow-up fixes found by running `omnigent run --harness pi` with
darwin_seatbelt enabled end-to-end:
1. with_additional_read_roots silently dropped pi_dir
When the spec declares no read_paths, resolve_sandbox returns
read_roots=None (meaning 'no spec-supplied grants').
with_additional_read_roots bailed early on None, so the pi node_modules
dir granted by _try_sandbox_pi was never added to the policy. Result:
pi failed with 'Cannot find package .../pi-ai/index.js' because the
seatbelt profile had no subpath rule for the nvm install tree.
Fix: treat None as an empty list rather than 'already unrestricted' —
the caller is explicitly widening the policy and must be honoured even
when the spec has no grants of its own.
2. PI_CODING_AGENT_DIR was created under $TMPDIR, which wasn't granted
_try_sandbox_pi granted /tmp as a write root, but on macOS $TMPDIR is
/var/folders/.../T/ (not /tmp). PI_CODING_AGENT_DIR is created with
tempfile.mkdtemp() which uses $TMPDIR, so pi got EPERM trying to write
its extension/settings. Fix: also grant tempfile.gettempdir() alongside
/tmp.
With all three fixes (two-hop symlink detection, read-roots None handling,
TMPDIR grant) `omnigent run /tmp/pi-sandbox-bundle --harness pi` boots and
completes a full turn end-to-end under darwin_seatbelt.
Fixes#3237.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: route kimi and inkling through Responses API via system.ai.* ids
Kimi and inkling never send finish_reason in /chat/completions streaming
responses, causing Pi to throw 'Stream ended without finish_reason'.
These models work correctly via the Responses API at /ai-gateway/codex/v1
using their system.ai.* model ids (system.ai.kimi-k2-7-code,
system.ai.inkling).
- Add system.ai.kimi-k2-7-code and system.ai.inkling to
_DATABRICKS_RESPONSES_MODELS in the executor
- Add _DATABRICKS_TO_SYSTEM_AI mapping in pi_native_credentials so live
endpoint fetch translates databricks-* ids to system.ai.* and routes
them to the gpt_responses bucket (openai-responses at /ai-gateway/codex/v1)
- Update _pi_needs_responses_api to treat system.ai.* models as responses
- Update _pi_provider_for_model to route system.ai.* to databricks-openai
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(review): restore substring reasoning fallback and fix run-path translation
Addresses Polly's review of #3307:
1. Restore 'kimi'/'inkling' to substring reasoning check in _fetch_pi_model_lists
so unmapped variants (renamed/versioned endpoints not in _DATABRICKS_TO_SYSTEM_AI)
still get reasoning:true — preventing silent regression.
2. Translate databricks-* model ids to system.ai.* in the executor run path
(_build_env_and_dir) so model_override='databricks-kimi-k2-7-code' correctly
routes to the databricks-openai (Responses API) provider, not databricks-completions.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: move GLM to Responses API via system.ai.glm-5-2
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: move Qwen3 to Responses API via system.ai.* ids
Qwen3 returns array content with tool calls via /chat/completions causing
[object Object] errors. system.ai.qwen3-next-80b-a3b-instruct and
system.ai.qwen35-122b-a10b work correctly via the Responses API.
Also removes qwen3 from _unsupported_in_pi since it's now handled.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor: replace hardcoded system.ai map with keyword-based detection
- Replace _DATABRICKS_TO_SYSTEM_AI exact-id dict with _databricks_to_system_ai()
function that detects by keyword (kimi, inkling, glm-5, qwen3, qwen35) and
derives system.ai.* id by stripping 'databricks-' prefix. Handles future model
variants automatically without needing to update an exact-id map.
- Apply the same swap in model_catalog._fetch_databricks_listing so sys_list_models
returns system.ai.* ids directly, letting the LLM use the correct id immediately.
- Use specific fragments (glm-5 not glm) to avoid false-positives like
zai-org-glm-4-7 which has no system.ai.* alias.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(review): fix _ensure_rpc selector translation; revert GLM to completions path
Addresses Polly's blocking issues:
1. Normalize model id to system.ai.* at the top of _ensure_rpc so that both
models.json and the provider/model selector see the same id. Previously only
_build_env_and_dir translated the id but _ensure_rpc still built the selector
from the untranslated databricks-* id, causing 'Model not found' in Pi.
2. Revert GLM (databricks-glm-5-2) back to the completions path. GLM works fine
via /chat/completions with finish_reason=true — moving it to the Responses API
was unnecessary and undocumented. Removed from _SYSTEM_AI_MODEL_KEYWORDS and
_DATABRICKS_RESPONSES_MODELS.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor: use Unity Catalog model-services API for Pi model discovery
Replace /api/2.0/serving-endpoints with /api/2.1/unity-catalog/model-services
which returns system.ai.* model ids directly with supported_api_types metadata.
Benefits:
- No databricks-* → system.ai.* translation needed
- Authoritative API capability info: models with 'openai/v1/responses' in
supported_api_types go to the Responses provider; others to completions
- Embeddings excluded cleanly via has_embedding check
- sys_list_models returns system.ai.* ids directly via _fetch_databricks_uc_listing
Also add _ensure_rpc id normalization so databricks-* model_override values
are translated before building the provider/model selector.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: route all system.ai.* models through AI Gateway (omnigent-openai)
system.ai.* ids are not valid at /serving-endpoints — they only work
via the AI Gateway at /ai-gateway/codex/v1. Previously, system.ai.*
models without openai/v1/responses in UC metadata (kimi, inkling,
qwen3) were routed to omnigent-completions at /serving-endpoints,
causing 404 errors.
Route all system.ai.* models to omnigent-openai regardless of UC
supported_api_types.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(test): update test to expect all system.ai.* models in gpt_responses
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi-native): surface Pi model errors as visible error items in web UI
When Pi's API call fails (e.g. 404 for unknown model id, 400 for
unsupported API type), the extension was silently returning from
message_end with no output, leaving users with an empty turn.
Post an external_conversation_item of type 'error' when message.stopReason
is 'error', so the error appears in the web UI chat.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(tests): update model_catalog tests for Unity Catalog API format
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: revert Qwen3 from responses API - Pi sends fields that Qwen3 rejects
/ai-gateway/codex/v1/responses rejects Pi's standard Responses API fields
(parallel_tool_calls, temperature:null, top_p:null) for Qwen3, causing 400.
Route Qwen3 back to omnigent-completions until either:
- Pi adds compat flags to suppress these fields for non-standard providers
- The upstream array-content fix (earendil-works/pi#7062) lands to fix [object Object]
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: restore Qwen3 to Responses API path via system.ai.*
Pi only sends store:false in requests - the earlier 400 was from a stale
session before the routing fix. Confirmed minimal Pi request works fine
for Qwen3 via /ai-gateway/codex/v1/responses.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(review): scope UC listing to pi path only; fix test fixtures
Polly's review correctly identified that using _fetch_databricks_uc_listing
for all Databricks providers leaks system.ai.* ids to non-pi harnesses
(claude-sdk, codex, openai-agents) that only understand databricks-* ids.
Revert model_catalog.py to use _fetch_databricks_listing (serving-endpoints)
for sys_list_models. _fetch_databricks_uc_listing remains available but is
only used internally by pi_native_credentials._fetch_pi_model_lists.
Also fix test_model_catalog.py fixtures to use the correct serving-endpoints
payload shape (databricks-* ids) rather than the UC model-services shape
(system.ai.* ids) which the non-pi listing never emits.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test(model_catalog): update pi tests for UC model-services API
Pi harnesses now call `/api/2.1/unity-catalog/model-services` and return
`system.ai.*` model ids instead of `databricks-*` ids. Update the test
fixtures and expected ids to match:
- `_databricks_transport`: now serves both the serving-endpoints page
(non-pi) and a UC model-services page (pi harness calls).
- `test_databricks_listing_filters_to_chat_llms`: expect `system.ai.*`
ids and matching family assertions.
- `test_databricks_listing_skips_explicitly_non_ready_endpoints`: rewrite
to use UC format (UC has no per-service readiness flag).
- `test_listing_failure_reported_and_not_cached`: switch to codex-native
harness to test generic failure/retry without UC routing complexity.
- `pi-everything` parametrize: update expected ids to `system.ai.*`.
- `model_catalog.py`: add TTL cache for UC listings (same `_listing_cache`
with a `"uc:"` prefixed key) so pi harness calls cache-hit correctly.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(model_catalog): fix ruff RUF005 and E501 lint errors
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(test_model_catalog): shorten docstring to fix E501
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi_executor): scope system.ai.* Responses-API routing to kimi/inkling/qwen only
system.ai.claude-* and system.ai.meta-llama-* ids should route to their own
providers (Anthropic surface and completions respectively), not the Responses
API. Previously _pi_needs_responses_api returned True for *all* system.ai.*
ids, which would have routed llama to the Responses endpoint.
Fix: check _SYSTEM_AI_MODEL_KEYWORDS in the system.ai.* branch so only kimi,
inkling, and qwen3 variants return True. Claude is already caught upstream by
the "claude" substring check in _pi_provider_for_model.
Also update stale docstrings in _needs_responses_api and _unsupported_in_pi
that still mentioned qwen3 as excluded (it was re-enabled via the Responses API).
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(pi): route GLM via Responses API (system.ai.* ids)
GLM has the same finish_reason issue as Kimi/inkling on /chat/completions.
Route it through the AI Gateway Responses API by adding "glm-" to
_SYSTEM_AI_MODEL_KEYWORDS (uses "glm-" not bare "glm" to avoid matching
"zai-org-glm-4-7" which has no system.ai.* alias).
- Remove GLM from _PI_REASONING_MODEL_FRAGMENTS (reasoning:true is a
completions-path flag; not needed for Responses API).
- Remove GLM from the reasoning:true assignment in _fetch_pi_model_lists.
- Update test: kimi no longer gets reasoning:true (Responses API path).
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): remove gpt-oss from _unsupported_in_pi; it routes via Responses API
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): exclude all Gemini models from Pi, not just gemini-2-5
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): exclude only gemini-2-5 from Pi; other Gemini models use completions
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): drop redundant qwen35 keyword; qwen3 already matches qwen35 ids
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor(pi): remove _databricks_to_system_ai; catalog always returns system.ai.* for pi
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): remove reasoning:true from kimi/inkling static model entries
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(pi): route Gemini via /ai-gateway/mlflow/v1/chat/completions using system.ai.* ids
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): fix _unsupported_in_pi to only exclude gemini-2-5; gemini-3+ route via mlflow
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor(pi): remove static kimi/inkling/qwen3 entries from _DATABRICKS_RESPONSES_MODELS
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): route system.ai.* llama/other models to mlflow gateway; rename provider to databricks-mlflow
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): use generic base URL for non-Databricks providers (OpenAI API key, LiteLLM)
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi): address Polly review — fix 4-tuple annotation, system.ai.gpt routing, gpt-oss exclusion, UC listing filter
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(model_override): strip system.ai.* prefix for vendor-direct providers (OpenAI key, etc.)
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
A native Pi session routed through a Databricks gateway whose OAuth token
can't be resolved (expired refresh token) launched fine but every message
silently failed to reach the model — no reply, no error. `_databricks_pi_provider`
caught all failures in one try/except and still returned a provider whose
`!databricks auth token` apiKey fails at request time; because pi-native
dispatches turns fire-and-forget, the failure never round-tripped back as an
Omnigent error.
Split credential resolution from the (benign) model-list fetch so a genuine
auth failure carries a `credential_warning`. At terminal auto-create, surface
that warning as an `error` item via `external_conversation_item`: it renders as
the web UI's distinct error banner (not a misleading assistant bubble),
persists across reload, is a non-content item type so it never enters the next
turn's context, and posts without queuing an agent turn (safe on a session
whose model is unreachable).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Claude Opus 5 (released 2026-07-24) was missing from the curated
_SUBSCRIPTION_STATIC_MODELS["claude"] list. Verified empirically against
Claude Code 2.1.220: 'claude-opus-5' -> is_error:false; the dated form
'claude-opus-5-20260724' and a 'claude-opus-5-fast' variant both return
is_error:true, so neither is added.
Placement follows the existing convention: tiers descend
fable -> opus -> sonnet -> haiku, newest version first within a family
(matching claude-sonnet-5 ahead of claude-sonnet-4-6), so opus-5 slots
between fable-5 and opus-4-8.
The web mirror (web/src/lib/claudeNativeModels.ts) needs no change: it
lists version-agnostic aliases ('opus' resolves to the latest Opus) by
design, not pinned ids.
Signed-off-by: Abdullah Said <abdullahsaid89@gmail.com>
Co-authored-by: omnigent <noreply@omnigent.ai>
* ci(ui-snapshot): make the visual-baseline gate merge-blocking
The UI Snapshot visual-regression check was advisory ([non-blocking]) and
not in the required-checks set, so a UI change could land without
regenerating the committed baselines — which is how the baselines drifted
stale on main (every PR since #3311 fails the gate identically).
Register it as a required merge gate:
- Drop the "[non-blocking]" suffix from the job name.
- Add "UI Snapshot (visual baselines)" to REQUIRED and ALLOW_SKIP in
merge-ready/required.sh, plus a workflow_for mapping. It's safe as a
required check: a PR touching no render input skips the render via the
`detect` job's `if` gate, and an if-skipped job reports success — so
non-UI PRs satisfy the check instead of sitting pending. ALLOW_SKIP +
workflow_for let the gate tell that genuine skip from a still-pending run.
- Add "UI Snapshot" to merge-ready.yml's workflow_run triggers so the gate
re-evaluates when the snapshot workflow completes.
- Update the visual README's merge-blocking section.
This PR edits ui-snapshot.yml (a render input), so the gate runs here and
fails on the stale baselines; the `update-ui-snapshot` label regenerates
them onto this branch to turn it green.
Co-authored-by: Isaac
* test(e2e-ui): regenerate visual baselines
---------
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
* fix(sessions): fall back to localStorage when pinning against an old server
A pin created in the new UI while the server is still pre-upgrade was lost
on the server upgrade. The pin toggle PATCHes `omnigent.pinned`; an old
server (no per-user pin concept) stores it as a bare label, but the
upgraded server's read path (`_labels_for_viewer`) drops every bare/
`omnigent.pinned.*` key and only surfaces the caller's own
`omnigent.pinned.<user>` key — so the bare-key pin silently vanishes. The
localStorage→server migration couldn't recover it either, since that pin
was never in localStorage.
This complements the earlier migration-gate fix (which protected pins made
*before* the UI upgrade). Now the toggle also checks `filterHonored`: when
the server can't store pins, it writes the pin to localStorage (the same
store the pre-upgrade UI used) instead of PATCHing a doomed bare key. The
pin renders immediately (sidebar unions localStorage pins) and later
migrates through `useMigrateLocalPinsToServer` like any pre-upgrade pin.
Once the server can store pins, the toggle uses the server as before.
- Move the legacy-pin localStorage helpers from Sidebar.tsx to the leaf
sidebarNav module (+ a single-id `setLegacyPinnedConversationId`) so the
toggle hook can use them without an import cycle.
- Tests: unit coverage for the toggle's old-server fallback (pin/unpin to
localStorage, no PATCH; normal PATCH path once honored), and an
end-to-end case in the backwards-compat suite that pins DURING the
UI-before-server window and asserts it survives the server upgrade.
Co-authored-by: Isaac
* fix(sessions): surface local-write failures in the old-server pin fallback
Addresses a review note: the old-server pin toggle's localStorage write is
the pin's only persistence, but it went through the best-effort
`writeLegacyPinnedConversationIds`, which swallows write errors (e.g.
storage quota exceeded). So a failed write let the mutation report success
and the optimistic patch show the pin, while it silently vanished on reload
— with no rollback.
Split out a throwing `...OrThrow` raw write. The old-server fallback
(`setLegacyPinnedConversationId`) now uses it, so a failed write rejects the
mutation → `onError` rolls back the optimistic patch and the UI honestly
shows the pin didn't take, matching the server PATCH path. The migration's
best-effort write is unchanged (a failed write there just retries next load).
Test: the fallback rolls back the optimistic pin when the local write throws.
Co-authored-by: Isaac
## Related issue
N/A
## Summary
- Add `omnigent/onboarding/sandboxes/types.py` with shared dataclasses
(`SandboxCapabilities`, `SandboxSpec`, `SandboxInfo`, `HostContext`) and the
new `SandboxError` exception hierarchy.
- Add `omnigent/onboarding/sandboxes/registry.py` with a contribution-based
provider registry that mirrors `omnigent/harness_plugins.py`: built-in
providers are declared as a `SandboxProviderContribution`, community
packages register via the `omnigent.sandbox_providers` entrypoint group, and
broken plugins are recorded in `load_errors` without breaking core startup.
- Add `omnigent/community/sandbox/__init__.py` as a namespace package so
third-party providers can ship code under `omnigent.community.sandbox.*`.
- Validation enforces that community provider code lives under the community
namespace, rejects name collisions, and checks metadata consistency.
- Add a `capabilities` property to `SandboxLauncher` that derives feature flags
from existing class variables and overridden transport methods.
- Migrate CLI and managed-host call sites from direct class-var reads
(`supports_cli_bootstrap`, `can_resume`, `supports_local_port_forward`) to
the new `capabilities` object.
- Add unit tests for types, registry behavior, validation, and entrypoint
discovery.
No provider implementations were changed; this is purely a surface-layer
refactor toward a pluggable sandbox provider interface.
## Test Plan
```bash
uv run pytest tests/onboarding/sandboxes tests/sandbox tests/cli/test_cli.py tests/server -k managed -q
pre-commit run --files omnigent/onboarding/sandboxes/types.py omnigent/onboarding/sandboxes/registry.py omnigent/onboarding/sandboxes/base.py omnigent/onboarding/sandboxes/__init__.py omnigent/onboarding/sandboxes/bootstrap.py omnigent/community/sandbox/__init__.py omnigent/cli_sandbox.py omnigent/server/managed_hosts.py tests/onboarding/sandboxes/test_types.py tests/onboarding/sandboxes/test_registry.py
```
All 779 selected tests pass and the targeted pre-commit hooks pass.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
New unit tests in `tests/onboarding/sandboxes/test_types.py` and
`tests/onboarding/sandboxes/test_registry.py` exercise the registry,
contribution validation, types, and capabilities derivation. Existing
provider and CLI tests pass unchanged, confirming backward compatibility.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* feat(scheduled): add Model + Reasoning-effort pickers to the task dialog
The scheduled-task create/edit dialog previously omitted model and effort,
sending only agent_id so tasks always ran with the agent's configured
defaults. Add lightweight Model + Reasoning-effort controls, gated by the
selected agent's capability exactly like the interactive New Chat dialog:
they render only for native coding agents that carry the model/effort
surface (Claude Code) and are hidden for agents without it (Codex, plain
SDK agents, etc.).
- New scheduled-local ModelEffortFields component reuses the shared option
lists (CLAUDE_NATIVE_MODELS + the version-agnostic aliases, and
CLAUDE_NATIVE_EFFORTS) rather than importing the 26-prop
HarnessConfigModal, which is bound to smart-routing / cost-control /
per-turn model loading and disproportionate for a saved task. When a host
is pinned it uses that host's live model options; with none pinned (the
common case) it falls back to the static Claude aliases.
- Hoist CLAUDE_NATIVE_EFFORTS into the shared HarnessConfigControls module
so both dialogs share one source of truth.
- Wire modelOverride + reasoningEffort through create and update (both
already round-tripped by scheduledTasksApi.ts — no client/API change).
Unselected ("Default") omits the field on create so the fire path uses
the agent's defaults; on edit, Default sends null to clear a prior
override. Edit mode prefills both controls from the loaded task.
No permission/approval/cursor mode picker and no new API field: this is a
pure frontend change.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* test(automations): e2e for model + effort selectors
Extend tests/e2e_ui/scheduled/test_scheduled_tasks_page.py with UI journeys
for the model + reasoning-effort selectors added to the scheduled-task
create/edit dialog:
- controls visible + default to "Default" for a capability-gated agent
(Claude Code)
- controls hidden (with the "uses defaults" hint) for a non-capable agent
(seeded Codex task, asserted via the edit dialog)
- create persists a concrete Model + Effort pick (asserted via the REST API)
- create with both controls left on Default persists null overrides
- edit prefills the controls from a seeded task's stored overrides
LLM-free like the sibling tests: exercises only the dialog, REST, and the
rendered row. Uses Playwright expect() auto-waiting, no sleeps.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
---------
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
## Related issue
N/A
## Summary
- Remove the dedicated Settings → Appearance → Sidebar → Font size card and the `lib/sidebarFontPreferences` module, since users should use the global Interface font size control instead.
- Clear the legacy `omnigent:sidebar-font-size` localStorage key on app boot so anyone who previously changed the sidebar font size falls back to the default 13px.
- Add a "Reset to defaults" button at the bottom of the Appearance section that opens a confirmation dialog and resets all appearance choices: mode, terminal theme, color palette/custom theme, workspace panel default, hide-unconfigured-harnesses toggle, and interface/code font size and family.
## Test Plan
- Updated unit tests in `web/src/pages/SettingsPage.test.tsx` covering the reset flow and the absence of the sidebar font size control.
- Added a Playwright E2E test in `tests/e2e_ui/sessions/test_appearance_reset.py` to verify the sidebar card is gone and the reset dialog restores defaults.
- To verify locally after installing web dependencies:
- `cd web && npm run type-check`
- `npx vitest run src/pages/SettingsPage.test.tsx`
- `pytest tests/e2e_ui/sessions/test_appearance_reset.py`
## Demo
N/A — UI change; a screen recording of the reset confirmation dialog is recommended before merge.
## Type of change
- [ ] Bug fix
- [ ] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [x] E2E tests added / updated
- [ ] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
local web dependencies are not installed in this environment, so the local type-check and vitest runs could not be executed. CI will run the web test suite on the PR branch.
## Changelog
Settings → Appearance no longer has a separate Sidebar font size control, and a new "Reset to defaults" button restores every appearance preference after confirmation.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* feat(projects): name the project in the new-session hero, drop the tray chip
When starting a session from within a project (a `?project=` landing), the
composer used to show the project as a pill in the footer tray while the hero
kept its generic "What should we do?" prompt. Move that context into the hero
instead: the heading shows the project name and Otto's eyes are swapped for the
same folder icon the sidebar uses for a project. The footer project chip
(`LandingProjectPicker`) is removed — filing on create still uses the same
`selectedProject` state, just without the redundant chip.
The folder icon renders in a fixed-height (`h-18`) box matching Otto so the
vertically-centered composer doesn't shift when toggling between the plain and
in-project landings.
Co-authored-by: Isaac
* fix(projects): clamp long project name in the new-session hero
A 100-char project name (the server-side cap) rendered at text-3xl overflowed
the centered container: the icon+heading flex row sized to its content with no
width bound, so the h1's min-w-0/line-clamp had nothing to act against. Give the
row w-full and keep the heading min-w-0 + line-clamp-2 + break-words so a long
name wraps to two lines and ellipsizes instead of overflowing. Add a test
asserting the clamp class contract on a 100-char name.
Co-authored-by: Isaac
The same-second filename collision was disambiguated by pid alone. A pid is
only unique across processes — a process that crashed more than twice within
one second reused its own pid, so every report after the first collision was
written to the same path and silently destroyed its predecessor. Saving five
reports in one second left two files on disk with three crash reports lost,
with rotation held wide enough that nothing should have been pruned.
Keep counting past the pid-suffixed name until the path is free.
test_save_report_writes_and_rotates encoded the bug: it asserted all five
returned paths still existed while rotation kept only two, which could only
hold when the collision collapsed them onto two names. It now asserts the
newest report survives its own rotation pass, and a new test pins the
no-overwrite guarantee with rotation held wide.
Signed-off-by: apeltekci <andrew@peltekci.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Swap the Files right-rail tab glyph from FilePenLineIcon (pen-on-page) to
FilesIcon (stacked pages) to better convey the panel's contents.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(sessions): don't wipe local pins when UI upgrades before server
The one-time localStorage→server pin migration (#3189) trusted an
ambiguous success signal. A pre-upgrade server silently ignores the
unknown `?pinned=true` param and returns the normal (unfiltered) session
page, so the UI saw ~100 "server pins", computed an empty to-migrate set,
and cleared localStorage without ever writing a pin. After the server was
upgraded, its per-user key filter found nothing and every pin read as
unpinned — the reported data loss for UI-before-server upgrades.
Fix, entirely client-side:
- `fetchPinnedConversations` now returns `{ conversations, filterHonored }`.
It keeps only rows actually carrying the `omnigent.pinned` label and
reports `filterHonored: false` when the server returned unpinned rows —
the tell-tale of an old server that ignored the filter.
- The migration is gated on `filterHonored`: it stays inert (localStorage
untouched) against an old server and re-runs after the eventual upgrade.
A legacy id is dropped only after its write is confirmed.
- Pinned membership is the union of the server's pins and any leftover
localStorage pins, so a not-yet-migrated pin keeps rendering instead of
vanishing during the UI-before-server window.
Tests: new filter-honored detection cases, a migration-gate suite, and an
end-to-end backwards-compat test that drives the real hooks across an
old→new server upgrade and asserts the pin is never lost.
Co-authored-by: Isaac
* docs(sessions): address Polly review notes on pin migration
- Document the empty-page ambiguity in `filterHonored` and why it's safe
(an old empty page means a zero-session account; the migration PATCH to a
deleted session 404s and the pin is retained, not lost).
- Note the window-scoped caveat that a legacy-only pin outside the loaded
paginated window may not render a row until loaded.
- Add a regression test: a failed (404) migration write keeps the legacy
pin in localStorage for retry.
Co-authored-by: Isaac
* feat(automations): absolute next-run time + card rows
Change 1: the Automations list now shows the next run as an absolute
wall-clock time ("Next run Tomorrow at 8:00 AM" / "Today at 2:30 PM" /
"Jul 26, 8:00 AM") instead of a relative delta ("in 15h"). Adds
formatNextRunAtAbsolute() in scheduleText.ts, which only FORMATS the
server-authoritative next_run_at (rendered in the task timezone,
Today/Tomorrow bucketed in that same zone) and never recomputes which
instant is next on the client. The old relative formatNextRunAt() is
kept intact.
Change 2: each ScheduledTaskRow now renders as a card (rounded-xl
border bg-card, internal padding), and TasksPage stacks them with a
gap. All existing behavior and data-testids preserved; paused rows are
not dimmed.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* feat(automations): relative full-word next-run label
Reverses the earlier absolute-time next-run display back to a
server-sourced relative delta in full words ("Next run in 3 hours",
"Next run in 8 mins", "Next run in 2 days") per user feedback.
formatNextRunAt now emits full-word, pluralized buckets ('soon' /
'in N min(s)' / 'in N hour(s)' / 'in N day(s)'); the delta is still
computed only from the server's authoritative next_run_at, so the
"no client countdown" rule is unaffected. Removes the now-dead
formatNextRunAtAbsolute and its private helpers (safeFormat,
civilDayInZone). Card-row styling is unchanged.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* fix(automations): live-tick the relative next-run label
The relative next-run label was frozen at its first-render `now` and
only refreshed on remount. A shared 30s useNow() clock (a module-level
singleton via useSyncExternalStore) now drives live re-renders, so the
delta counts down while the page stays open. TasksPage owns the one
ticker and passes `now` to each row, keeping the row a pure function of
props.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* fix(automations): round next-run label to nearest unit
Flooring understated the relative next-run label near a unit boundary:
a task 1h49m away read "in 1 hour". formatNextRunAt now rounds to the
nearest minute/hour/day and promotes on carry (each threshold tests the
already-rounded value), so 1h49m reads "in 2 hours" and a delta that
rounds up to a full unit shows "in 1 hour"/"in 1 day" rather than
"in 60 mins"/"in 24 hours".
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* test(automations): e2e for live-ticking next-run countdown
Adds a Playwright test to the scheduled-tasks page suite proving the
relative next-run label re-renders on its own as time passes (the shared
useNow() ticker), with no navigation. Uses clock mocking for determinism:
pins the browser clock 40 min before the server's next_run_at, asserts
"Next run in 40 mins", fast-forwards 35 min past many 30s ticks, then
asserts the same row updated to "Next run in 5 mins". LLM-free.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
---------
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
## Related issue
N/A
## Summary
- When `omnigent server` binds a non-loopback interface, it auto-enables accounts (login) mode and prints a warning.
- The warning now explicitly names `OMNIGENT_AUTH_ENABLED=0` as the override to keep single-user mode.
- Kept the warning to the canonical env var; removed any mention of the deprecated alias.
- Improved the rendered indentation so the override sentence starts on its own line.
## Test Plan
- `uv run ruff check omnigent/cli.py`
- `uv run pytest tests/cli/test_bind_auth_defaults.py -q`
Both pass.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [x] 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
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
The existing `tests/cli/test_bind_auth_defaults.py` already exercises the non-loopback auto-enable path and the explicit `OMNIGENT_AUTH_ENABLED=0` override. This change only updates the warning copy.
## Changelog
`omnigent server` now tells users they can set `OMNIGENT_AUTH_ENABLED=0` to override automatic multi-user mode when binding to a non-loopback interface.
## Related issue
N/A
## Summary
- Remove the long-deprecated `OMNIGENT_ACCOUNTS_ENABLED` environment-variable alias for the multi-user auth enable switch. The canonical name `OMNIGENT_AUTH_ENABLED` has existed since the repository was open-sourced.
- Strip the alias logic from `omnigent/server/auth.py::_auth_enabled()`, the explicit-auth check in `omnigent/cli.py::_apply_bind_auth_defaults()`, and the runner env-propagation allowlist in `omnigent/host/connect.py`.
- Delete the tests that exercised the alias and the obsolete comment in `tests/conftest.py`.
## Test Plan
- `uv run ruff check omnigent/server/auth.py omnigent/cli.py omnigent/host/connect.py tests/conftest.py tests/cli/test_bind_auth_defaults.py tests/server/test_accounts.py tests/e2e/test_local_server_lifecycle_e2e.py` passed.
- `uv run pytest tests/cli/test_bind_auth_defaults.py tests/server/test_accounts.py -q --no-header` passed (95 items).
- `uv run pytest tests/server/test_accounts.py::test_resolve_auth_source_defaults_to_header tests/server/test_accounts.py::test_resolve_auth_source_opt_in_selects_accounts tests/server/test_accounts.py::test_factory_defaults_to_header_when_env_unset tests/cli/test_bind_auth_defaults.py -q --no-header` passed (15 items).
- Verified no remaining references with `grep -R "OMNIGENT_ACCOUNTS_ENABLED" . --exclude-dir=.git --exclude-dir=.venv`.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [x] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Removed the tests that specifically covered the deprecated alias; remaining tests continue to validate `OMNIGENT_AUTH_ENABLED` behavior. The refactor does not change the `OMNIGENT_AUTH_ENABLED=1 | =0` semantics.
## Changelog
[Breaking] The deprecated `OMNIGENT_ACCOUNTS_ENABLED` environment variable has been removed; use `OMNIGENT_AUTH_ENABLED` instead.
BREAKING CHANGE: Users and deploys still setting `OMNIGENT_ACCOUNTS_ENABLED` must rename the variable to `OMNIGENT_AUTH_ENABLED` before upgrading; the old name is no longer read or propagated.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Bring the Projects PRD implementation-status section in line with what has
shipped and what remains:
- Mark backend `config` hardening (size bound + non-dict coercion) as done —
both already landed in the project store.
- Move the completed Benchmark (#3094) and Phase 2 (project defaults) items out
of TODO into their own "Done" sections.
- Correct a stale claim that the new-session prefill machine still reads the
`omni_project` label — it was collapsed to config-only in Phase 2. The one
remaining UI label reader (the Settings archived-project picker) is folded
into the Phase 4 retire-label-path step instead.
- Postpone Phase 3 (memory & context) and Phase 4 (label consolidation) with
distinct triggers: Phase 3 waits for customer demand; Phase 4 waits until
telemetry shows most clients have migrated to a version that writes
`project_id`.
Co-authored-by: Isaac
## Related issue
N/A
## Summary
- Updated `inject_user_message()` in `omnigent/claude_native_bridge.py` so user messages that start with a Claude Code UI-only/unsupported slash command (`/help`, `/exit`, `/quit`, `/doctor`, `/cost`, etc.) are escaped before being pasted into the TUI.
- Escaping inserts an invisible zero-width no-break space before the leading `/`, causing Claude Code to treat the input as regular user text while the user still sees their slash.
- Supported slash commands (`/clear`, `/compact`, `/effort`, `/model`, `/ultrareview`, `/branch`, `/fork`) and unknown skill commands pass through unchanged.
## Test Plan
- Added parametrized unit test for `_escape_unsupported_slash_command`.
- Added payload test verifying `/help` gets the escape prefix and `/clear` does not.
- Ran targeted injection tests and pre-commit:
- `uv run pytest tests/test_claude_native_bridge.py::test_escape_unsupported_slash_command tests/test_claude_native_bridge.py::test_inject_user_message_escapes_unsupported_slash_command_payload -q`
- `uv run pytest tests/test_claude_native_bridge.py -k "inject_user_message" -q`
- `uv run ruff check omnigent/claude_native_bridge.py tests/test_claude_native_bridge.py`
- `uv run ruff format omnigent/claude_native_bridge.py tests/test_claude_native_bridge.py --check`
- `uv run pre-commit run --files omnigent/claude_native_bridge.py tests/test_claude_native_bridge.py`
## 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 — new unit tests directly cover the escaping decision and the payload path.
## Changelog
Unsupported Claude Code slash commands are now escaped and sent as regular user messages instead of leaving the native terminal in an undriveable state.
* fix(projects): disable settings inputs during config load; correct worktree doc
Two non-blocking follow-ups from the PR #3221 review:
- Gate the worktree toggle, workspace Browse trigger, and path input on
`isLoading`, matching the host Select. Previously an edit made in the load
window would be clobbered by the seeding effect once the fetch settled.
- Rewrite the `use_worktree` docstring to match the opt-in implementation
(only `true` is written; `false` is never stored and treated as unset).
Co-authored-by: Isaac
* docs(projects): mark backend config hardening as done in PRD
The two #3108 config-hardening follow-ups (size bound + non-dict coercion)
already landed in the project store; move them from "deferred" to a ✅ bullet
so the PRD status matches the code.
Co-authored-by: Isaac
* feat(projects): project settings editor + config-driven composer prefill (Phase 2)
Add a "Project settings" dialog to set a project's stored session defaults
(host, working directory, agent, opt-in random worktree) and wire the new-chat
composer to prefill from that stored config, retiring the newest-session
inference so stored config is the single source of truth.
- ProjectSettingsDialog: edit + persist config {host_id, workspace, agent_id,
use_worktree}; worktrees opt-in (default OFF, store true when on). Reuses the
composer's host/agent pickers and filesystem browser.
- projectPrefill: collapse to config-only seeding; unset fields fall through to
the composer's generic defaults. Honor a stored sandbox default via
selectSandbox (gated on managed sandboxes). Remove useNewestProjectSession.
- Extract the nested-dropdown dismiss guard into a dependency-free module shared
by the settings and scheduled-task dialogs.
Co-authored-by: Isaac
* fix(projects): repair CI — Sidebar test mocks, e2e rewrites, retire inference e2e
- Add useProjectConfig/useUpdateProjectConfig to all 10 Sidebar test mocks
(Sidebar now mounts ProjectSettingsDialog, which calls them).
- Rewrite the settings-dialog e2e to create the project via POST /v1/projects
instead of the flaky row-kebab move-to-project flow.
- Fix the composer-prefill e2e to stub GET /v1/sessions/projects (bare array),
the real endpoint useProjects hits.
- Remove test_start_session_project_prefill — it exercised the newest-session
inference path this PR retired; config-driven prefill replaces its coverage.
Co-authored-by: Isaac
* fix(projects): address review — no data-loss on failed config load; fresh prefill after save
Blocking issues from the PR review:
1. Data loss: saving the settings dialog after a failed config GET sent `{}`,
which the server reads as "clear stored defaults". Now `useProjectConfig`'s
isError is surfaced; a first-class project whose config failed to load blocks
Save (with a notice), the seed effect skips a blank draft, and onSubmit bails.
2. Stale prefill after save: useUpdateProjectConfig only invalidated, so the
composer's one-shot prefill could latch onto a stale cached config (30s
staleTime) and drop just-saved defaults. It now setQueryData's the fresh
config and upserts the projects list (so a promoted label-only folder
resolves to its new id immediately).
Tests: dialog load-error blocks Save; hook seeds config + upserts list on
success; useProjectConfig disabled on null id and surfaces isError.
Co-authored-by: Isaac
* fix(web): center sidebar header buttons and soften session row hover
## Related issue
N/A
## Summary
- Vertically center section header action buttons (Projects `+`, Sessions kebab, etc.) with their titles by using `top-1/2 -translate-y-1/2` instead of `top-0.5`.
- Remove the 1 px lift on session row hover (`motion-safe:hover:-translate-y-px`) so rows stay visually anchored.
- Calm the hover flash by dropping the bouncy Otto-token transition on rows and reducing the global `--sidebar-hover` tint from 5% to 3%. Rows now use the same plain `transition-colors` pattern as the rest of the sidebar hover surfaces.
- Make `SIDEBAR_ACTIVE_HIGHLIGHT` also specify `:hover` styles so active items (current page, selected session, drop target) keep their active background on hover instead of switching to the hover tint.
## Test Plan
- `cd web && npm install && npm run dev`
- Hover over Projects/Sessions headers and confirm action buttons are vertically centered with the title text.
- Hover over active items (e.g., current page in the top nav, selected session row, current Inbox) and confirm the background stays in the active state and does not flash.
- Hover over inactive session rows and confirm the row no longer shifts up and the background highlight is subtler.
## Demo
Subtle hover/positioning polish. Verify by hovering items in the sidebar — buttons align with title baselines, rows stay still on hover, and active items don't flash.
## Type of change
- [x] Bug fix
- [x] UI / frontend change
- [ ] Feature
- [ ] 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
Visually verified by inspecting the relevant Tailwind classes and CSS variables. No test coverage changes; the existing `Sidebar.projectHeaderChevron.test.tsx` covers header layout, and the hover behavior is primarily CSS.
## Changelog
Sidebar header action buttons are now vertically centered with section titles. Session row hover is smoother, and active items no longer flash when hovered.
* test(e2e-ui): regenerate visual baselines
---------
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
## Related issue
N/A
## Summary
- Replace the web-resolved-theme bridge with a single cross-shell contract, `setThemeSource(theme)`, so the web app only reports the user's chosen theme source and each shell drives its own OS-level dark mode.
- Android: `MainActivity` now extends `AppCompatActivity`; `OmnigentBridgeListener` maps `setColorScheme` to `AppCompatDelegate.setDefaultNightMode`; system-bar icon contrast is derived from `resources.configuration.uiMode`. Removes `ResolvedColorScheme.kt`, the root-class MutationObserver, and the top-level navigation reset on init.
- iOS: Add a `ThemeSource` enum and `ThemeController` singleton inside the existing `OmnigentWebView.swift` target file to avoid `.pbxproj` edits; wire `setColorScheme` through the JS bridge and apply it via `.preferredColorScheme(...)` and `window.overrideUserInterfaceStyle`.
- Web: Update `nativeBridge.setThemeSource`, remove the `omnigent-native-ready` queue, and update `ThemeProvider`/`nativeBridge` unit tests.
- Android and web unit tests are updated to match the new contract.
## Test Plan
- iOS: `cd web/ios && xcodebuild -project Omnigent.xcodeproj -scheme Omnigent -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -configuration Debug -only-testing:OmnigentTests test`
- Android: `cd web/android && ./gradlew :app:testDebugUnitTest`
- Web: `cd web && npm install && npm run type-check && npm run test -- ThemeProvider.test.tsx nativeBridge.test.ts`
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] 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 completed for iOS: the app builds and `OmnigentTests` passes in the iPhone 17 Pro simulator. Android and web test suites were not run end-to-end in this session, but the affected unit tests were updated in the same change.
## Related issue
N/A
## Summary
- Add a top-level `justfile` that groups common local dev tasks (`run-ios`, `run-android`, `dev`, `electron-dev`, `lint`, `normalize-locks`, etc.) with hidden `_ensure-*` / `_check-*` prerequisites.
- Add an iOS `simulator` Fastlane lane that builds the Debug .app, installs it on an already-created iOS Simulator, and launches it.
- Add Android Gradle tasks (`runDebug`, `reverseProxy`) for launching the debug APK and running `adb reverse`.
- Fix the Fastlane `xcodebuild` invocation to use camel-case `derivedDataPath` so the built `.app` is written where the lane expects it.
- Export `FASTLANE_SKIP_UPDATE_CHECK=1` in the justfile.
- Document the new `justfile` recipes concisely in `AGENTS.md`.
## Test Plan
- `just --list` shows grouped recipes.
- `just run-ios` built/launched the iOS app in the iPhone 17 Pro Simulator.
- `pre-commit` passes on the touched files.
## Demo
N/A
## Type of change
- [x] Feature
- [ ] Bug fix
- [ ] 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 by running `just run-ios` and watching the Omnigent app launch in the iOS Simulator.
## Changelog
Add a top-level `justfile` with recipes for launching the iOS Simulator, running the Android debug build, starting the omnigent dev pod, and running pre-commit/lockfile normalization.
* feat(web): 3D model preview for STL / 3MF / OBJ files
Selecting an .stl / .3mf / .obj file in the Files browser now renders an
interactive WebGL preview (orbit/zoom/pan) instead of the "Preview not
available for binary files" placeholder.
- Add `isModelFile()` to codeViewerHelpers (MIME-first, extension fallback),
scoped to exactly STL/3MF/OBJ.
- New lazy-loaded `ModelViewer` component (three.js STLLoader/3MFLoader/
OBJLoader) with camera + OrbitControls, lighting, auto-fit, loading/error
states, and full scene teardown on unmount.
- Dispatch models before the binary-rejection branch in CodeViewer; treat
them like images in FileViewer (diff/source-mode suppressed).
- three.js pinned at 0.185.1 and code-split into its own chunk so it stays
out of the main bundle.
Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(web): address model-viewer review — unified resolver, recovery, teardown
Resolve the four blocking issues from cross-vendor review of the 3D model
preview:
1. Unified format interface: add one shared `getModelFormat(path, contentType)`
resolver (MIME-first, extension fallback) used by BOTH `isModelFile`
dispatch and `ModelViewer`'s loader selection, so a MIME-matched file with
an unknown extension parses via the correct loader instead of erroring.
`isModelFile` is now `getModelFormat(...) !== null`.
2. Error state no longer unmounts the canvas: the container is always mounted
and the error is an overlay on top, keeping the ref alive so an
invalid→valid prop change recovers.
3. Single idempotent `teardownScene()` called from both the init failure path
and the effect cleanup, so a partial init (renderer/controls/context/RAF)
can't leak on failure.
4. Empty/degenerate models (e.g. comment-only OBJ) are validated for a
non-empty, finite bounding box before fitting; invalid bounds route to the
error UI instead of a blank canvas.
Adds ModelViewer.test.tsx (MIME-only loader selection, malformed/empty/NaN →
error, invalid→valid recovery, failure-path + unmount teardown) and
getModelFormat unit tests.
Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* feat(web): theme-aware 3D model preview (light/dark)
ModelViewer previously hardcoded a neutral STL material, fixed light
intensities, and a transparent canvas, so the 3D preview ignored the app
theme. Make it theme-aware off the SAME next-themes source Monaco and the
terminal use (`useTheme().resolvedTheme`), so it tracks light/dark and
updates live when the user toggles the theme with a model open.
- Add a pure `modelViewerTheme(resolved)` map in codeViewerHelpers (mirrors
`resolvedThemeToMonaco`): background clear color, STL default material, and
ambient/key light intensities per mode — brighter lights in dark so the
mesh stays legible. Shared across STL/3MF/OBJ in the one unified pipeline.
- ModelViewer seeds the scene from the active mode and keeps light/material
handles on its resource bag so a theme toggle recolors the live scene in
place (clear color + intensities + STL color) with no reload/reparse.
- Drop the transparent (alpha) canvas in favor of a theme-derived opaque
background so the preview sits flush with the panel in both themes.
- Tests: three theme-awareness cases (light build, dark build, live toggle
without rebuild) mirroring the next-themes mock pattern in
MonacoCodeEditor.test.tsx, plus modelViewerTheme unit tests.
Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* test(web): 3MF MIME-only dispatch + prune package-lock churn
Add MIME-only 3MF coverage mirroring the existing STL/OBJ tests: a file
with an absent/unrecognized extension but a `model/3mf` content type must
resolve to the 3MF loader in ModelViewer and route to <ModelViewer> in
CodeViewer, exercising the shared getModelFormat() resolver.
Regenerate web/package-lock.json so the diff vs origin/main is limited to
the `three` dependency subtree — dropping unrelated resolved-URL
normalization churn from an earlier regen.
Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(web): dispose material textures in ModelViewer teardown
disposeObject() freed each mesh's geometry and material but not the
textures the material references (map, normalMap, roughnessMap, …), so a
textured 3MF leaked its GPU textures every time the viewer unmounted.
three.js frees neither the material nor its textures automatically.
Add disposeMaterial(), which disposes every texture slot on a material
(detected via the three.js `isTexture` flag, robust to multiple three
copies) before disposing the material itself. Extend the ModelViewer
teardown unit test with a textured-material mesh and assert its textures
are released on unmount.
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* test(e2e): cover 3D model preview in the Files browser
Add a Playwright e2e test that seeds an ASCII STL and an OBJ file, opens
each in the Files browser, and asserts the ModelViewer mounts: the
`3D preview of …` canvas host renders a <canvas>, the "Unable to render
3D model" overlay never shows (so parsing and WebGL both succeeded), and
the flow does NOT fall through to the binary placeholder or a source
view. STL exercises MIME-based routing (application/vnd.ms-pki.stl); OBJ
exercises the extension fallback. Seeded via the filesystem PUT endpoint
(no agent run), mirroring the existing image/pdf rendering e2e tests.
This satisfies the E2E UI Required gate for the model-preview feature.
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(web): resolve the 3D-viewer deps from the public npm registry
The three.js stack this PR added (three, @types/three,
@dimforge/rapier3d-compat, @tweenjs/tween.js, @types/stats.js,
@types/webxr, fflate, meshoptimizer) was locked with `resolved` URLs
pointing at an internal mirror (npm-proxy.dev.databricks.com), while the
rest of package-lock.json resolves from registry.npmjs.org. Public CI
can't reach that mirror, so `npm ci` timed out fetching
three-0.185.1.tgz (ETIMEDOUT) and failed the install-dependent checks.
Repoint just those eight `resolved` URLs to the canonical
registry.npmjs.org form. Integrity hashes are unchanged (the mirror
served identical tarballs), so this only changes where the tarballs are
fetched from, not what is installed. `npm ci --legacy-peer-deps` now
succeeds from a clean node_modules, and `npm install --package-lock-only
--legacy-peer-deps` produces no further diff, so the lockfile-up-to-date
gate stays green.
Signed-off-by: Bryan Li <bryan.li@gmail.com>
---------
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(android): honor system dark mode
Signed-off-by: Bryan Li <15131870+btli@users.noreply.github.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(android): sync system bar contrast
Signed-off-by: Bryan Li <15131870+btli@users.noreply.github.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(android): harden resolved theme sync
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* refactor(android): decode theme at bridge boundary
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* test(android): tighten theme bridge coverage
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(android): drop WebView algorithmic darkening
Algorithmic darkening inverts the SPA when the user forces light mode
while the OS is dark: the page's root color-scheme is then 'light', so
WebView treats it as dark-unaware and darkens it algorithmically,
leaving dark status-bar icons over a darkened page. With targetSdk >= 33
the DayNight host theme alone makes prefers-color-scheme track the OS,
so the darkening flag added nothing for the system-mode path and only
broke the forced-light path. Verified on an API 34 emulator across the
OS-light/dark x app-System/Light/Dark matrix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(web): keep Electron on the selected theme, not the resolved one
Reporting only resolvedTheme regressed Electron system mode: an explicit
Light selection under a light OS changes no resolved value, so no report
fired and themeSource stayed 'system' — the shell chrome then flipped
dark with the OS while the app was forced light. Report the resolved
scheme first (Android system-bar contrast) and follow with 'system'
while that is the selection: Electron keeps the last report, so it
tracks the OS in system mode and pins to explicit selections, including
ones that leave resolvedTheme unchanged. Android drops 'system' at the
bridge, so its behavior is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix: route native themes by consumer
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(android): harden system bar theme sync
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* test(android): clean up theme bridge state
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(android): install theme bridge at document start
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(android): resync system bars on live theme changes
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* style(android): format theme test
Signed-off-by: Bryan Li <bryan.li@gmail.com>
---------
Signed-off-by: Bryan Li <15131870+btli@users.noreply.github.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
github-release.yml fired on every v[0-9]* tag push and created an
unpublished DRAFT release for rc/dev/alpha/beta tags. Nothing downstream
depended on those drafts — draft-release-notes.yml already skips rc,
finalize-release.yml refuses rc, and the Docker/homebrew/changelog
workflows fire on the tag push / release:published directly. The drafts
just accumulated (and rehearsal rcs had to be gh-release-deleted during
cleanup).
Add a guard that skips the draft-release job for rcN/devN/preN tags
(trailing digit required so a substring like 'dev' in a mistyped tag can't
trip it). Drop the now-dead alpha/beta arms — this repo only cuts rc
pre-releases — and align the same rc/dev/pre pattern + comments across
the other release-adjacent workflows for consistency. Update release.yml's
Next-steps text and RELEASING.md accordingly.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
Closes #[F-CR-7]
## Summary
- After a user consents to an unknown server (deep link) or types a server URL, `WorkspaceURLExpander.expandIfNeeded` issued a HEAD probe via `URLSession.shared` with no redirect policy, so the consented host could 3xx-redirect the probe to a different origin — including a local-network service — breaking the consent alert's promise that the app only talks to the host the user approved.
- The probe now defaults to a dedicated `URLSession` backed by `SameOriginRedirectHandler`, a `URLSessionTaskDelegate` that follows only same-origin redirects (scheme + host + port match) and blocks any cross-origin redirect by returning `nil` from `willPerformHTTPRedirection`.
- As defense in depth, `expandIfNeeded` additionally verifies `response.url`'s origin matches the approved origin, so a cross-origin response is never trusted even if a caller supplies a bare session without the redirect delegate.
- Rebased onto #3179 (F-CR-6) and deduped: removed my `--omnigent-deep-link` test hook (subsumed by #3179's `--omnigent-open-url` / `--omnigent-reset-state` seam), and consolidated the two `MockHTTPServer` copies into one shared file compiled into both test targets.
## Test Plan
- Unit: `WorkspaceURLExpanderTests.testRejectsResponseFromDifferentOrigin` returns a `server: databricks` 200 whose `url` is a different origin and asserts the URL is left unchanged.
- Integration (simulator, real local HTTP network): `WorkspaceURLExpanderRedirectTests.testBlocksCrossOriginRedirect` / `testFollowsSameOriginRedirect` assert a cross-origin redirect is blocked (response stays 302 on the approved port) and a same-origin redirect is followed. Confirmed meaningful: the cross-origin test fails when the delegate is reverted to follow-all-redirects (the vulnerable behavior).
- UI (simulator): `RedirectConsentUITests.testDeepLinkConsentOpensApprovedServer` drives the deep-link consent flow via #3179's `--omnigent-open-url` + `--omnigent-reset-state` seam and asserts the alert appears, "Open" loads the approved server's WebView.
- Ran on iPhone 17 simulator: all 8 expander/redirect tests + the UI smoke test + all 26 F-CR-6 deep-link tests pass; full project builds.
- Note: the UI test cannot exercise the redirect itself — a localhost deep link infers `http`, and the probe is https-only, so the probe never fires for loopback. The redirect policy is verified over a real local network by the integration test instead.
## Demo
N/A
## Type of change
- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [x] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [x] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Manual verification: built and ran the new unit, integration, and UI tests on the iPhone 17 simulator; confirmed all pass and that the integration test fails against the vulnerable (follow-all-redirects) baseline, proving it is a meaningful regression test. Also ran all F-CR-6 tests after the dedup to confirm no regression from #3179's shared seam.
## Changelog
The iOS app no longer follows cross-origin redirects when probing a newly approved server for the Databricks workspace mount, so a consented host can't redirect the probe to a different origin.
* test: stabilize two known flakes (dictation close, agent-info popover)
Two load-timing flakes that recur across PRs:
- Pytest (server-rest) test_dictation.py::test_stream_closes_take_on_
abrupt_disconnect: on an abrupt disconnect the route offloads
handle.close() to a thread. During teardown the loop's thread-pool
executor may already be shutting down, so the offload raises and the
old contextlib.suppress swallowed it — the take (and, for the remote
engine, a worker slot) leaks. Fall back to a direct close() on the
loop; it's a quick non-blocking free for every engine.
- E2E UI test_agent_info_popover.py: _open_popover single-clicked the
trigger, but the button hover-opens on the click's own pointer arrival
and the click's Radix toggle can flip it back shut past the
HOVER_CLICK_GRACE_MS window under load, so the panel never mounts.
Confirm the panel opened and retry the click from a closed state.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* test: stabilize scheduled-tasks time-picker flake
test_scheduled_task_create_edit_modal_and_time_picker had two coupled
races in the time-picker step (6/10 failures reproduced, no artificial
load needed):
- The picker is a Radix popover nested in the create-task dialog. The
dialog's focus management can fire an interaction-outside that closes
it the instant it mounts, so the minute cells unmount between the
visibility check and the click (element-not-found / click timeout).
- Selecting a minute leaves the popover open, and an open floating-ui
popover keeps recomputing its position — so the submit button (and,
later, the edit-phase time input) stays perpetually "not stable" and
detaches mid-click.
Extract a _pick_minute() helper that opens from a known-closed state and
retries until the cell is present, then dismisses the picker via a
click-outside (not Escape, which would bubble to the Radix Dialog and
close it) and waits for it to unmount so the layout settles before
submit. 0/12 clean + 0/8 under load after the fix.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
The in-session "Configure" model dropdown offered a "Smart Routing" option on
native terminal sessions (Claude Code, Codex, Pi, …). It's meaningless there:
a native CLI bakes its model into the launch argv once and can't per-turn
route, so picking it did nothing useful.
Add isNativeTerminalSession() (mirrors the server's
_native_coding_agent_for_session: native by omnigent.wrapper label OR resolved
harness) and exclude such sessions from costRoutingEligible in ChatPage, so the
Smart Routing option no longer appears in their Model dropdown. Brain-harness
sessions (claude-sdk / codex / pi, and the polly orchestrator) keep it.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(scheduled tasks): add windowed latest-run-status store query
Add ScheduledTaskStore.list_latest_run_status_for_tasks(ids) -> {id: status},
a single row_number()-windowed query (scheduled_at DESC, id DESC — same order
as list_runs) returning each task's most-recent run status. Powers the Tasks
list completion badge in one query instead of N per-row /runs fetches, and is
correct under overlapping run-now runs (unlike a denormalized last_run_status
column). Tasks with no runs are absent from the map.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* feat(scheduled tasks): run-now endpoint + status/next-run serializer fields
Backend for three Tasks-list run controls:
- last_run_status: _to_response now carries the task's most-recent run status
(from the windowed store query), populated on list/get/patch. Force-fail of
stale orphans runs BEFORE the status read so a dead run reports failed, not a
stuck running.
- next_run_at: _to_response carries the live scheduler's authoritative next-fire
ISO timestamp (scheduler.next_run_at) on list/get/create/patch — server-
sourced, never client-recomputed (paused/unarmed → null).
- POST /v1/scheduled-tasks/{id}/run: an immediate manual fire that REUSES the
shared fire path via build_run_now (same _run_fire_for_task body, dispatch/
preflight seams, and in-flight overlap guard as the scheduler). Paused tasks
are runnable (manual override); fire-and-forget → 202 Accepted. 409 when a
fire is already in flight, 404 for a non-owned task, 503 when the scheduler
subsystem is not running. Wired via app.state.scheduled_task_run_now.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* feat(scheduled tasks): status pill, run-now menu, next-run on rows
Wire the three run controls into the Tasks list UI:
- last_run_status → a completion pill on each row (Failed/Skipped/Running/
Queued). Succeeded and never-run render NO pill (success is not noise);
Failed is destructive, Skipped muted — matching the Paused pill styling.
- next_run_at → "Next: <time>" on the schedule subline, formatted in the
task timezone via a new formatNextRunAt() that only FORMATS the server's
ISO value (never client-recomputes; paused/unarmed → nothing).
- Run now → a "⋯ menu" item + useRunScheduledTaskNow mutation (POST
/{id}/run) that invalidates the list + that task's runs so the pill
updates. Runnable for paused tasks; row busy-disables while in flight.
scheduledTasksApi gains lastRunStatus + nextRunAt (interface + wire map)
and runScheduledTaskNow(). Unit tests: pill per status, no-pill cases,
next-run formatting (tz + calendar-day boundary), run-now mutation wiring.
e2e: new run-controls journey (Run now → recorded run + pill flips);
existing schedule-line assertions relaxed to to_contain_text now that the
server next-run renders on the same line.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* style(scheduled tasks): bump task title size/weight on rows
Make the scheduled-task row title slightly larger and bolder: text-sm →
text-base and font-semibold → font-bold. Subline, pills, and spacing are
unchanged. Updates the one TasksPage sort-order test that located the title
by its .font-semibold class to .font-bold.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* Revert "style(scheduled tasks): bump task title size/weight on rows"
This reverts commit e0195ce3c4977abdaeba5316426029f808edeef6.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* style(scheduled tasks): title 15px, metadata 13px on rows
Trim the row title to exactly 15px and the metadata subline to exactly 13px
using arbitrary-px classes (text-[15px] / text-[13px]) — the app root scales
rem ~1.125×, so the standard text-sm/text-xs would render 15.75/13.5px and
can't hit the exact target. Weights unchanged: title font-semibold (600),
subline no weight class (inherits 400). Pills, spacing, next-run text, and the
⋯ menu are untouched.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* style(scheduled tasks): lighten metadata subline on rows
Soften the row metadata subline one notch to a lighter gray via an opacity
step on the same theme token: text-muted-foreground → text-muted-foreground/80.
Theme-aware (works in light + dark), size unchanged (13px), and the next-run
<span> keeps inheriting the same color (no own color class). Title, pills,
spacing, and the ⋯ menu are untouched.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* style(scheduled tasks): tighten row spacing 2px, remove run-status pill
- Row vertical padding py-3 → py-[11px]: trims each side 1px so the gap
between adjacent rows drops from 24px to 22px (the list is flex-col with no
gap, so the row padding is the whole inter-row spacing).
- Remove the last-run status pill (Failed/Skipped/Running/Queued) entirely per
design: drop the render block, the RUN_STATUS_PILL map, the statusPill local,
and the now-unused ScheduledTaskRunStatus import. The Paused pill is kept
as-is. The lastRunStatus API/store field is left in place (harmless data;
only the visual is removed). Subline, next-run text, and the ⋯ menu unchanged.
Drops the per-status pill test cases in ScheduledTaskRow.test.tsx (that UI is
gone); keeps the paused-pill, next-run, and run-now tests.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* style(scheduled tasks): relative "Next run in Xh" on rows
Switch the row next-run display from an absolute label ("Next: Today 9:00 AM")
to a compact relative delta ("Next run in 15h" / "in 6d" / "soon"):
- formatNextRunAt now returns a delta (nextRunAt − now): <60m → "in Xm" (min
"in 1m"), <24h → "in Xh", else "in Xd", all floored; a delta below 1 min
(imminent / clock skew) → "soon"; null/unparseable iso → null. The `timezone`
param is dropped (a pure delta needs no zone) — call site + useMemo deps
updated. This only formats HOW FAR AWAY the server's authoritative next_run_at
is; it never recomputes WHICH instant is next on the client, so the old
"no client-recomputed countdown" rule still holds.
- Row prefix "Next: " → "Next run " so it reads "Next run in 15h".
Tests: rewrote the formatNextRunAt unit tests for the relative buckets +
boundaries + "soon" + null; updated the row test to the "Next run in …" prefix;
reconciled the e2e (the old count==0 "Next run" guard flips to positively
asserting the server-derived relative label — its real intent, no client
recompute, is unchanged).
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* style(scheduled tasks): darken row hover background
Bump the full-row hover tint one notch: hover:bg-muted/50 → hover:bg-muted/70
(same theme-aware `muted` token, higher opacity). The color-mix stays
`var(--muted) N% transparent`, so in light the effective tint goes ~2.9% → 4.1%
black and in dark the alpha goes 0.5 → 0.7 — visibly stronger but still subtle.
Comment updated to match. Nothing else changes.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
---------
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
Adds a 'databricks_cli' credential_proxy type so sandboxed tools can use
the Databricks CLI without the real OAuth/PAT token ever entering the
sandbox. The operator lists which ~/.databrickscfg profiles to proxy;
each is materialized into the sandbox as a placeholder-only .databrickscfg
(oa_cred_* token), and the L7 egress proxy swaps the placeholder for the
real token on the way out.
- Refreshing token provider (DatabricksProfileTokenProvider) re-mints
short-lived OAuth tokens via the databricks SDK for long sessions;
CredentialRewriteRule gains an optional secret_provider and the proxy
resolves secrets per-swap (offloaded via run_in_executor).
- Placeholder-only files are materialized into the sandbox scratch dir
and pointed at via DATABRICKS_CONFIG_FILE / DATABRICKS_CONFIG_PROFILE.
- Requires the 'databricks' extra and linux_bwrap (the Go CLI ignores
SSL_CERT_FILE on macOS, so darwin_seatbelt is rejected at parse time).
- Egress stays operator-listed: the workspace host must be named in
egress_rules, consistent with the other credential_proxy types.
Signed-off-by: mxatone <mxatone@gmail.com>
## Related issue
N/A
## Summary
- Bring `omnigent-slack` into the lockstep release cycle (now four packages, not three): its `[project].version` was stuck at `0.1.0` while the rest of the repo moved to `0.7.0.dev0`, so the extra pin and lockfile drifted.
- Pin `omnigent-slack==0.7.0.dev0` in the root `slack` optional-dependency extra, mirroring the existing `omnigent-client==` / `omnigent-ui-sdk==` sibling pins so a published `omnigent[slack]` always pairs with the matching `omnigent-slack` release.
- Teach `scripts/update_versions.py` (the engine behind `.github/workflows/bump-version.yml`) about the 4th package: rewrite the slack `[project].version` and the extra `==` pin on every bump, and scan `[project.optional-dependencies]` (not just `[project.dependencies]`) when verifying sibling pins. Regenerate `uv.lock`.
## Test Plan
- `uv run --no-project --python 3.12 --with packaging python scripts/update_versions.py check` → prints `0.7.0.dev0` (all four packages agree, all sibling `==` pins present).
- `uv lock` → "Updated omnigent-slack v0.1.0 -> v0.7.0.dev0".
- `uv run ... python -m pytest tests/scripts/test_update_versions.py` → 13 passed (updated the test fixture + assertions for the 4th package).
- `tests/test_version.py::test_version_matches_pyproject` still passes (root pyproject == `omnigent/version.py` at `0.7.0.dev0`).
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] 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
Updated `tests/scripts/test_update_versions.py` to include `integrations/slack/pyproject.toml` in the `repo_copy` fixture and adjusted the lockstep assertions (5 changed files, 4 `9.9.9` occurrences in root pyproject, 1 in slack). Verified the full suite (13 tests) passes. Also ran `update_versions.py check` and `uv lock` manually to confirm lockstep + lockfile consistency.
* feat(onboarding): report the installed-but-unconfigured harness state
Add the credential axis to the picker-facing readiness map so Claude, Codex,
and Pi report the yellow "installed but no credential" state — the signal the
web setup dialog needs to know when to offer an "Add credential" action
(the M2 keystone of Setup-From-the-UI). Purely additive: the values
("needs-auth" / "binary-missing") already exist in HarnessAvailability, and
the launch gate (harness_is_configured) stays binary-only, so a not-yet-authed
harness is never blocked from launching.
- New _family_provider_configured(): whether an omnigent-managed provider
(API key / gateway) serves the harness's family, reading the same config
omni setup's overview does. Subscription-kind is excluded (that lives in the
CLI's own login, judged by harness_cli_logged_in). Local, side-effect free,
never raises (fails to "no credential").
- Claude: ready when a provider is configured OR the CLI subscription login is
present (was CLI-login only — an API-key-only user wrongly showed yellow).
Checks config first to avoid the status subprocess on the common path.
- Pi: gains the axis it lacked entirely (was binary-only → always green once
installed). No CLI login, so binary + provider: installed-but-no-provider is
now "needs-auth".
Codex already had this (unchanged, the template). Qwen/OpenCode env-auth
unchanged.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* feat(onboarding): write a harness provider credential from the UI
Second PR of Setup-From-the-UI (M3, security-sensitive). Adds the path that
turns a yellow "installed but not configured" harness green from the browser
for Claude / Codex / Pi, host-agnostic (local or remote), reusing the
credential-write logic omni setup already uses.
Design: the server is an authz'd pass-through. It validates ownership + the
UI-auth allowlist and forwards the secret over the (TLS) tunnel; the host DAEMON
does the write on the runner. The server never persists the secret, and the
frame's secret_value field is redaction-named so it never lands on a telemetry
span. Gated behind OMNIGENT_HARNESS_INSTALL_ENABLED (default off) exactly like
the install route (404 when disabled).
- New non-interactive core omnigent/onboarding/harness_auth.py: store a key /
gateway (secret → keychain, else ~/.omnigent/secrets.json; a providers: entry
referencing keychain:<name>, never the raw key), adopt an existing host env
var by reference (env:<VAR>, value never read), and detect adoptable env
credentials (non-secret descriptors only). First provider on a family becomes
the default; unsupported families/kinds are refused.
- New host.store_secret / _result frame pair; host daemon handler resolves the
harness→family, calls the core, and re-reports readiness so the badge flips
without a reconnect. Pi maps to its preferred anthropic family.
- New route POST /v1/hosts/{id}/harnesses/{harness}/credential (owner-scoped,
allowlisted, flag-gated) + registry pending_secret_writes plumbing + tunnel
result resolution.
- Regenerated openapi.json.
Tests: core unit tests (incl. the no-raw-secret-in-config invariant), frame
round-trip + telemetry-redaction, host-handler unit tests, and a full route
integration test over a fake tunnel (ownership, flag-off, allowlist, failure
mapping). 243 pass across the affected suites.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* feat(onboarding): detect adoptable credentials on the host (adopt flow)
Adds the read side of the adopt flow: a host.detect_credentials frame pair +
GET /v1/hosts/{id}/credentials/detected that reports the credentials already
present on the host as NON-secret descriptors (family + source label + env var
name), so the UI can offer a one-click "adopt" instead of asking the user to
paste a key they already have. The value is never read or sent — adopt writes
an env:<VAR> reference via the existing store_secret path.
Owner-scoped + flag-gated like the credential-write route. Decode drops
malformed entries so a garbled payload can't inject a non-string field the UI
would trust. Adds frame round-trip (+ malformed-drop), host-handler, and route
integration (+ flag-off) tests; regenerated openapi.json.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(onboarding): tighten the credential route + adopt guard (Polly review)
Two review fixes on the credential-write path:
- The route gated on ui_installable_harnesses(), which includes the env-auth
opencode/qwen — the host handler then rejected them, turning a client/allowlist
problem into a confusing 502. Add ui_credential_configurable_harnesses() (the
Claude/Codex/Pi families the host can actually write) and gate on it, so
opencode/qwen get a clean 400 with no frame forwarded.
- adopt_env_credential now refuses an env var that isn't set on the host —
adopting an unset var would persist a provider entry that resolves to nothing
at the first turn. (Runs on the runner, so os.environ is the host's env.)
Tests: opencode/qwen added to the 400-rejection parametrize; an unset-env-var
adopt rejection case.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(server): serialize concurrent credential writes to one host (Polly review)
Polly non-blocking note: unlike the install route (which coalesces via
inflight_installs), the credential route had no guard against overlapping
writes. The daemon's write is a non-atomic load→merge→save of config.yaml
(twice — entry, then default), so two writes to one host in quick succession
(a double-click, or key + gateway) could interleave and clobber a sibling
providers: entry.
Add a per-connection credential_write_lock held around the store-secret
round-trip so writes to one host serialize. A gateway/local host still
processes different hosts concurrently (the lock is per HostConnection).
Adds an integration test that holds the first reply and asserts the second
frame only reaches the host after the first completes.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(onboarding): make Pi's auth step UI-authable and trackable
Pi's setup_steps auth descriptor was still the M1 shape (action="setup",
command="omnigent setup", status_key=None). Two consequences surfaced in
manual testing: (1) status_key=None made the step "unknown", so the setup
dialog dropped it and wrongly showed "Pi is ready" with no action even though
readiness reported needs-auth; (2) even rendered it was a CLI signpost, not
the credential form.
Pi is UI-authable now (PR A gave it the needs-auth readiness axis; the UI has
the credential form), so its auth step becomes action="auth" (opens the inline
form, keyed on kind=="auth"), command=None (Pi has no subscription CLI login),
status_key="authed" (trackable, so it's not dropped and the dialog reflects
the real state). Qwen stays the untracked env-auth signpost (not UI-authable).
Updates the pi test and adds a qwen-stays-signpost test.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* chore: use the `omni` CLI alias (omni setup) in setup guidance
Rename user-facing "omnigent setup" → "omni setup" in the harness setup-step
descriptors, the setup hint, and their doc-comments. `omni` is the installed
console entry point (pyproject: omni = omnigent.cli:main) and is already used
elsewhere in the codebase, so the shorter alias is correct and consistent.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* test: fix CI drift on the M3 backend branch (omni setup + auth action)
Two "Pytest (misc)" failures on this branch were stale test expectations, not
product bugs:
- tests/host/test_connect.py asserted the unconfigured-launch error names
"omnigent setup", but the earlier `omni` CLI-alias rename made the runtime
message say "omni setup". Update the positive assertion and the cursor
test's negative assertion (which guards that Cursor points at its own
installer, not the generic setup command) to the new spelling.
- tests/test_harness_capabilities.py restricted setup-step actions to
("install", "command", "setup"), but Pi's UI-authable step uses action
"auth" (added when Pi's credential step became a form). Add "auth" to the
allowed set; codex's own two-step assertion is unchanged.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* test: harden the install-flow e2e against a slow picker render
test_install_button_installs_missing_harness opened the agent picker and
immediately clicked the Codex row, but the picker mounts its rows only after
the /v1/agents fetch resolves. Under CI load that render lags, and a menu
opened before the data lands can render empty or re-close on the update — so
the bare open-then-click flaked with a 30s click timeout, the Codex row never
becoming actionable (seen across two different shards). Open the picker, wait
for the Codex row and re-open if the menu flapped, then click. No product
change; passes locally unchanged (the retry is a no-op on the fast path).
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* test: settle agent data before opening the picker in the install e2e
The install-flow e2e flaked (30s click timeout on the Codex row, then on the
picker trigger via an overlay pointer-interception when reopened). Root cause:
the picker opened before the /v1/agents fetch settled, racing the menu-open
against a re-render. Wait for the composer's "Set up Codex" notice (rendered
only once the Codex agent + its unconfigured host state load) BEFORE opening the
picker, then open once and click. Passes locally repeatedly.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* test: stop driving the agent picker in the install e2e (kill the flake)
The picker interaction was redundant — the single seeded Codex agent is already
auto-selected, so the composer's "Set up Codex" notice is present without
opening the dropdown. Driving the picker only added a menu-open-vs-async-render
race that flaked under CI load. Wait for the notice directly (generous 60s) and
click it to open the setup dialog.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* test: wait for network idle before asserting the setup notice (install e2e)
The "Set up Codex" notice depends on two async fetches re-rendering the
composer (/v1/agents auto-selecting the agent, /v1/hosts marking its harness
unconfigured). On loaded CI runners that chain lagged past the timeout and the
assertion raced the still-loading landing screen. Wait for network idle and the
host chip (readiness present) before asserting the notice.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* test: drop networkidle wait in install e2e (WS keeps network busy)
wait_for_load_state("networkidle") never fires in this app — the shell holds a
long-lived sessions/updates WebSocket, so the network is never idle. That wait
just burned its timeout and then raced the still-loading landing screen (the
"Set up Codex" notice was intermittently absent on CI). Replace it with plain
element waits (host chip, then the notice) at a generous 60s, matching every
other e2e_ui test. Passes locally repeatedly.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(host): adopt an env credential under its own family, not the harness's
Review (isaac, MAJOR): pi consumes both the anthropic and openai families, so
the UI can offer an OpenAI env var (e.g. $OPENAI_API_KEY) as adoptable for pi.
`_handle_store_secret` derived the family solely from the harness (pi→anthropic)
and passed that to `adopt_env_credential`, so adopting that var wrote an
anthropic-family provider whose api_key_ref is env:OPENAI_API_KEY — mis-routed
to the anthropic endpoint, failing at run time. For the adopt kind, look the env
var up in the host's detected credentials and use its OWN detected family
(falling back to the harness family if absent). Adds a pi-adopts-OpenAI
regression test.
Also carry the install-flow e2e fix onto this branch: explicitly select Codex
in the picker and stub the /v1/sessions?kind=any agent scan so the seeded-DB
agents don't leak in and leave Claude Code selected (a CI-only flake).
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(host): harden the UI credential-write path (review feedback)
Addresses Polly's blocking finding + hardening notes and Pat's nits on the
store_secret/adopt path:
- BLOCKING (adopt boundary): the daemon's adopt handler fell back to the
harness-derived family when an env var wasn't detected, and adopt_env_credential
only checked the var was *set*. An owner hitting the raw API could name any set
env var (a DB password, an unrelated secret) and have it persisted as a provider
credential sent to the vendor endpoint. Now the handler refuses an env_var that
isn't in detect_adoptable_credentials() (no fallback) — enforcing server-side the
same "only adopt what was detected" restriction the UI presents.
- secrets.py: create the file-backend secrets.json 0600 atomically via
os.open(O_CREAT, 0o600) instead of open()+chmod-after, which briefly left a
freshly-created file group/world-readable. Now network-triggerable, so worth
closing. Fixes the stale "0600 from the start" comment.
- adopt_env_credential: presence-only env check (`in os.environ`, not `.get`) so
the "never reads the value" contract stays literally true.
- gateway base_url: reject a non-http(s) scheme at write time rather than writing
a malformed provider entry that fails opaquely at the first turn.
- connect.py: hoist the harness_auth / provider_config imports to module top
(no circular import) to match the sibling onboarding imports.
Adds regression tests: adopt refuses an undetected env var, gateway rejects a
non-http base_url, and secrets.json is 0600 even under a permissive umask.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
---------
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
The Docker entrypoint's build_app() was constructing RuntimeCaps()
bare, so the llm:, policies:, and routing: blocks in a docker
deployment's config.yaml were silently ignored. This meant:
- Builtin policies that read event["llm_client"] (e.g.
deny_trivial_to_expensive_model) would always see None and abstain.
- default_policies declared under policies: would never fire.
- LLM-based and external routing clients were never built.
Mirror the logic from cli.py: parse_server_llm / parse_default_policies
/ routing client construction are now applied before RuntimeCaps is
passed to init_runtime, putting docker deployments on par with the
CLI-started server.
Fixes#3159
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Follow-up to #3189. Every session serialization path collapses per-user
`omnigent.pinned.<user>` keys via `_labels_for_viewer` except
`_child_session_summary_from_conversation`, which passed `conv.labels` through
raw. Child sessions aren't pinnable today (the pin affordance lives only on
top-level sidebar rows), so this is a latent gap rather than a live leak — but
if a shared child were ever pinned, its summary would expose another viewer's
pin key.
- Strip any `omnigent.pinned.<user>` key from a child summary's labels. No
collapse-to-canonical: there's no pin to surface, just the defensive strip.
- Test: a child carrying two users' pin keys yields a summary with no pin key,
while unrelated labels survive.
- Correct the stale `useMigrateLocalPinsToServer` docstring: the migration
patches the pinned-list cache (like `useTogglePinnedConversation`), it does
not invalidate the pinned query.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(pi-executor): add supportsUsageInStreaming:false to databricks-completions
Gemini, Qwen, inkling, and other non-OpenAI models in the databricks-completions
provider reject stream_options (which Pi sends with include_usage:true by default)
with 400 'unknown field'. Add supportsUsageInStreaming:false to suppress it,
matching what pi_native_credentials.py already does for omnigent-completions.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi-executor): use openai-responses for newer GPT models (gpt-5-5, gpt-5-6-*)
Newer GPT models reject function tool calls via /chat/completions with 400.
The Databricks Responses API (/ai-gateway/codex/v1/responses) now supports
tool-result chaining on subsequent turns (previously it did not).
- Add databricks-openai provider using openai-responses at /ai-gateway/codex/v1
for gpt-5-5, gpt-5-6-*, gpt-5-3-codex (matches pi_native_credentials routing)
- Keep databricks provider (openai-completions at /serving-endpoints) for
older GPT models (gpt-5-4, gpt-5-4-mini) that work fine with /chat/completions
- Add _pi_needs_responses_api() helper mirroring pi_native_credentials
- Update _pi_provider_for_model() to route to databricks-openai when needed
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi-executor): add kimi to reasoning model fragments
kimi-k2-7-code streams output on reasoning_content channel like GLM/DeepSeek.
Without reasoning:true in the model entry Pi ignores reasoning_content and
sees an empty stream, throwing 'Stream ended without finish_reason'.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(test): update kimi model entry to expect reasoning:true flag
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi-native): add reasoning:true to kimi/glm/deepseek model entries
These models stream output on reasoning_content channel. Pi's openai-completions
parser requires reasoning:true on the model entry to consume that channel;
without it the stream has no content and the turn ends with
'Stream ended without finish_reason'.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(pi-native): exclude qwen3 from completions provider
qwen3 models return content as a typed array [{type:'reasoning',...},{type:'text',...}]
when tools are present, causing Pi's streaming handler to produce [object Object].
Same root cause as gpt-oss; same fix.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: add inkling to reasoning model fragments and LLM detection
Both kimi and inkling stream output on reasoning_content channel with
content=null. Added inkling to _PI_REASONING_MODEL_FRAGMENTS (executor),
reasoning:true model entry condition (pi-native), and LLM name detection
tokens so it appears in the model list.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor: use allowlist for GPT completions-compatible models
Instead of a denylist of specific model ids that need the Responses API,
maintain an allowlist of GPT models known to work with /chat/completions.
Any GPT model not in the allowlist defaults to Responses API — safer
for new models not yet explicitly tested.
The executor's _pi_needs_responses_api now delegates to the same
implementation in pi_native_credentials for a single source of truth.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Smart routing was gated behind an OMNIGENT_SMART_ROUTING=1 opt-in on top of the
routing/llm config. The env is redundant: build the routing client whenever the
config supplies one — a server llm: block (built-in judge) or a
routing.provider=external block (external routes:select service). Remove the env
gate in cli.py and refresh the stale references in app.py, advise_models.py, and
web capabilities.ts. Server smart_routing_enabled already keyed on the resolved
client, so the /v1/info signal is unchanged.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The modular-registry proposal described the phases as thin numbered lists.
Turn them into a concrete, verified implementation plan reviewers can cost:
- Add a "Current state (verified 2026-07-24)" subsection grounding the plan
in the tree at main (59e6b70e): data model ready but no native_providers
field; run_<x>_native already near-uniform (only claude/codex/antigravity/
opencode carry extra kwargs); coverage uneven across hubs (resume 10,
chat-redirect 6, interrupt 9, stop 7); dead _HARNESS_MODULES literal still
present; harness_catalog() emits no native-agent rows.
- Phase 1 (core-only seam): 8 PRs (1.1–1.8) in a table with scope, key files,
dependencies, risk, and estimates. 1.1 provider model + resolver is the
additive foundation; 1.5 runner launch/terminal-route is the risk center.
- Phase 2 (community + web): 4 PRs (2.1–2.4).
- Add an effort summary: ~26–37 engineer-days across ~12 PRs, critical path
1.1 → 1.2 → 1.5 → 2.2 → 2.3. Refresh the Bottom line to match.
Docs-only; no code paths affected.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(web): remove "Create new project" from the project picker menu
Projects are created via the + icon next to the Projects header in the
sidebar, so the picker's own "Create new project" row was a redundant,
second entry point. Drop it (and the inline new-project input it toggled)
from ProjectPickerMenu, leaving search, the project list, and "Remove
from <project>".
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e_ui): file sessions via the + button after dropping picker create
The project picker no longer offers an inline "Create new project" row, so
the e2e helpers that drove that flow broke. Rewrite `_move_to_new_project`
to create the empty project from the Projects-header + button first, then
file the session via the kebab picker by name.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* feat(sessions): persist pinned sessions server-side as a per-user label
Pins were client-only (localStorage), so they didn't follow a user across
devices. Move them to a server-side per-user session label so a pin persists
and stays per-user even on shared sessions.
- Store: `omnigent.pinned.<user_id>` label (value = epoch-ms pin time);
`pinned_label_key()` hashes over-long user ids to fit the 128-char key
column. `list_conversations(pinned=True, pinned_owner=…)` filters to the
caller's own key.
- Route: `GET /v1/sessions?pinned=true` enumerates the caller's pins
(independent of the loaded window); PATCH rewrites the client's canonical
`omnigent.pinned` to the caller's per-user key, and `_labels_for_viewer`
collapses it back on read so the per-user dimension never crosses the API
and no viewer sees another user's pin key.
- Write-integrity: reject any client-supplied suffixed `omnigent.pinned.<user>`
key so a caller can't pin/unpin for someone else.
- Forks drop per-user pin keys by prefix (a clone must not inherit pins).
- Web: server-authoritative `usePinnedConversations` + optimistic
`useTogglePinnedConversation`; Pinned section ordered by pin timestamp;
one-time localStorage->server migration that retains pins whose write failed.
- Guard `relativeTime`/`absoluteTime` against non-finite input (no more "NaNy").
Co-authored-by: Isaac
* test(e2e-ui): drive visual-snapshot pins via ?pinned=true, not localStorage
The populated-sidebar visual baseline seeded the pinned session in localStorage,
but pins are now server-authoritative (GET /v1/sessions?pinned=true). Under the
new model the localStorage seed is ignored and the bare-list stub answered the
pinned query too, so every row rendered as pinned → baseline mismatch (the
non-blocking UI Snapshot job).
- Split a `?pinned=true` route out from the bare-list regex (which now also
excludes `pinned=`, mirroring the existing `project=` exclusion) and return
just the pinned row, carrying the canonical `omnigent.pinned` label.
- Drop the `omnigent:pinned-conversation-ids` localStorage seed.
- Apply the same fix to the pinned-project flyout baseline (it passed only by
luck — its bare-list stub happened to return exactly the one pinned row) and
give its row the pin label so it's explicit, not incidental.
Co-authored-by: Isaac
* fix(sessions): let read-only collaborators pin a shared session
Pinning moved server-side (per-user `omnigent.pinned.<user>` label) but the
session PATCH gated all label writes at LEVEL_EDIT, so a read-only collaborator
on a shared session could no longer pin it — a regression from the localStorage
model, which had no permission check.
- Gate a pin-only PATCH (labels == {omnigent.pinned}, no other field) at
LEVEL_READ: pinning is a personal per-viewer preference, not an edit to the
session, so anyone who can SEE it may pin it. Any other field keeps the
edit/owner requirement. Unpin ("" value) is still pin-only, so it downgrades
too. The `?pinned=true` list is already scoped `accessible_by`, so a shared
pin surfaces on "Shared with me".
- Tests: a LEVEL_READ grantee can pin AND unpin a shared session; the downgrade
stays narrow (a non-pin label, or a pin bundled with one, still 403s).
- Rework the access-tier comment to match the if/elif/else (READ / OWNER / EDIT).
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Co-authored-by: Isaac
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
PRs #3148 (extract native terminal orchestration) and #3149 (split the
native app-session test monolith) landed the two remaining Phase 0 file
splits. Update the proposal to reflect reality:
- §1 runner hub: app.py is now ~10.1k lines (was ~20.1k) plus the new
omnigent/runner/native/orchestration.py (~6.5k); drop the stale absolute
line-number anchors and clarify that the dispatch arms and interrupt/stop
closures stayed in app.py while the builders/mirrors moved out.
- Phase 0: mark both runner/app.py and the test monolith Done, noting the
single-orchestration.py outcome (vs the proposed three-way split) and the
nine concern-scoped test modules + shared conftest.py.
- Risk section: re-anchor the forwarder registry to _AUTO_FORWARDER_TASKS in
its new home and note the risk now shifts to Phase 1.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(server): split sessions.py into domain sub-modules
sessions.py (7799 lines) is split into 8 focused route files under
_sessions/:
routes_core.py — CRUD, list, WS updates, fork, switch-agent
routes_hooks.py — /hooks/* and /policies/evaluate
routes_items.py — /items and /child_sessions
routes_resources.py — /resources/* (terminals, files, environments)
routes_browser.py — /browser/*
routes_elicitations.py — /elicitations/*
routes_events.py — /events, /stream, DELETE /sessions/{id}
routes_permissions.py — /permissions/*, /owner
routes_agent.py — /agent, /agent/contents, /mcp
Each file exports a register_X_routes(router, ...) function.
create_sessions_router() becomes a thin delegator (~533 lines).
helpers.py gains proxy wrappers for _same_provider_family,
_agent_is_native, _agent_carries_native_fork_history,
_presentation_labels_for_agent, and _reset_runner_resources_after_switch
so existing test monkeypatches on sessions.<name> continue to work.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor(server): move sessions/ route sub-modules out of _sessions/
Convert sessions.py to a sessions/ package and move the 8 routes_*.py
files from _sessions/ into sessions/, so the public package layout is:
routes/sessions/__init__.py (facade, formerly sessions.py)
routes/sessions/routes_core.py
routes/sessions/routes_hooks.py
routes/sessions/routes_items.py
routes/sessions/routes_resources.py
routes/sessions/routes_browser.py
routes/sessions/routes_elicitations.py
routes/sessions/routes_events.py
routes/sessions/routes_permissions.py
routes/sessions/routes_agent.py
_sessions/ retains only the private internals (common, helpers,
orchestration) that do not need public names.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(server): use facade indirection for session_stream and get_agent_cache consistently
routes_browser, routes_events, and routes_hooks were still calling
session_stream.publish() and get_agent_cache() via the direct module
binding. Apply the same facade-indirection pattern already used in
routes_core so all call sites are consistent and test monkeypatches on
sessions.session_stream / sessions.get_agent_cache are honored.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(server): fix lint, _to_agent_object closure, and pyproject.toml exemptions
- Move _policy_type, _policy_description, _to_agent_object from inside
register_permissions_routes closure to module-level in routes_permissions.py
so routes_agent.py can import them directly. Fixes NameError crash on
GET /sessions/{id}/agent in server-approvals tests and E2E tests.
- Add missing 'return router' at end of register_permissions_routes (was
missing after the closure reorganization).
- Import the three helpers explicitly in routes_agent.py.
- Update pyproject.toml per-file-ignores to cover sessions/*.py and
sessions/__init__.py with the same exemptions the original sessions.py
had (ARG001, ARG002, BLE001, E501, F401, F403, F405) so pre-commit
ruff passes.
- Run ruff format on all sessions/ sub-modules.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(server): fix all proxy/monkeypatch misses and restore noqa directives
Route sub-modules were importing _X_impl directly instead of using the
facade proxy, causing monkeypatch(sessions, '_X', ...) to have no effect.
Fix by removing all '_X_impl as _X' imports from routes_*.py — the star-
imports from _sessions.helpers and _sessions.orchestration already bring
in the correct facade-delegating proxies.
Additional fixes:
- Access _SESSION_UPDATES_MAX_WATCHED, _SESSION_UPDATES_RESCAN_INTERVAL_S,
_SESSION_UPDATES_HEARTBEAT_INTERVAL_S through the facade in routes_core.py
so monkeypatch(sessions_routes, '_SESSION_UPDATES_*', N) works.
- Use _load_agent_spec_for_session proxy (not impl) in routes_resources.py.
- Access get_caps() through facade in routes_hooks.py evaluate_policy so
monkeypatch('omnigent.server.routes.sessions.get_caps', ...) fires.
- Restore noqa: BLE001 and F401 directives in _sessions/helpers.py and
_sessions/orchestration.py that were stripped by the RUF100 auto-fix.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(server): delete old sessions.py, fix remaining facade proxy misses
- Delete omnigent/server/routes/sessions.py (the file was rm'd in a prior
commit but never staged; CI was still linting it and seeing F403/F405).
- Route _HOST_BOUND_RUNNER_CONNECT_GRACE_S through the facade in
routes_events.py (3 call sites) so monkeypatch(sessions_module,
'_HOST_BOUND_RUNNER_CONNECT_GRACE_S', ...) is honored.
- Route _recover_subagent_status_forward_via_parent through facade
in routes_events.py.
- Route _registered_runner_id through facade in routes_core.py.
- Route _BROWSER_ACTION_AWAIT_S through facade in routes_browser.py.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(server): route patchable names in routes_hooks.py through facade
All five hook handlers and evaluate_policy use module-level timeout
constants (_CLAUDE_NATIVE_PERMISSION_HOOK_TIMEOUT_S etc.) and auth
helpers (_get_user_id, get_caps, get_agent_cache) that tests monkeypatch
on the sessions facade module. Access them through _sf (the facade) at
call time so monkeypatch(sessions_route, '_CLAUDE_NATIVE_PERMISSION_HOOK_TIMEOUT_S', 0.1)
and monkeypatch('omnigent.server.routes.sessions.get_caps', ...) are honored.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Re-running CI today means pushing an empty commit or rebasing, which fires a
push event and dismisses existing approvals (branch protection keeps
dismiss-stale-reviews on to block approve-then-swap). A `/rerun` comment
re-runs failed jobs on the existing head SHA instead -- no new commit, so
approvals survive.
Authorized to the PR author or a write-access commenter. Only re-runs the
mock-LLM `pull_request` test suites; the merge gates and Polly AI Review are
left alone. Single file (no privileged relay) because issue_comment gets a
writable base-repo token even for fork PRs.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pat.sukprasert@databricks.com>
--edit-last edits the most recent PR comment regardless of author or
content, so it was overwriting the UI preview comment when both workflows
ran on the same PR. Switch to the same find-by-marker + PATCH approach
used by ui-preview.yml so each workflow manages its own comment.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): make session rename optimistic so the new name shows instantly
Renaming a session left the stale name in the sidebar for the duration
of the PATCH round-trip: all cache patching happened in the mutation's
onSuccess, so the row only repainted once the server responded.
Move the cache overlay into onMutate so the new title paints on the next
frame, snapshot the old title for rollback, and restore it in onError.
onSuccess still reconciles with the server-confirmed title + updated_at
and keeps the deliberate no-refetch behavior (an immediate GET races the
search-index reindex). Also patch the ["project-sessions", name] caches
that project folders render from — the flat ["conversations"] overlay
never touched them, so a filed session's row stayed stale until the WS
reconcile.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): cancel in-flight list queries before optimistic rename overlay
Close the in-flight-reconcile clobber race flagged in review: an
already-running GET /v1/sessions reconcile poll (or a WS-triggered
fetch) could resolve after onMutate and overwrite the optimistic title
with the stale search-indexed name. Cancel the ["conversations"] and
["project-sessions"] queries in onMutate before overlaying so no
in-flight fetch can win.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Add an opt-in agent_name field to SessionCreatedEvent. Only polly and
debby are populated — all other agent names are withheld to avoid leaking
user-defined agent names in telemetry.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(sandbox): support OMNIGENT_CONTAINER_RUNTIME env var for container runtime selection
Allow switching the container runtime (e.g. from docker to podman) via the
OMNIGENT_CONTAINER_RUNTIME environment variable instead of requiring per-agent
YAML configuration. The per-agent container_runtime key still takes precedence
over the env var.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(test): add missing top-level `Any` import in test_local.py
Ruff flagged F821 (undefined name) because `Any` was used in a
runtime dict annotation but only imported inside a nested function.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(test): read version dynamically in crash handler test
The test hardcoded "0.6.0.dev0" which breaks when the installed
version diverges from the source (e.g. after a version bump).
Read omnigent.version.VERSION at runtime instead.
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* Revert "fix(test): read version dynamically in crash handler test"
This reverts commit 53855f5c10e3573e9d1ddbfd2afb0bd76abbc91e.
* fix: address review comments on container runtime PR
- Make container_runtime field explicitly Optional to avoid misleading
type annotation and unnecessary type-ignore
- Update parser docstring to mention OMNIGENT_CONTAINER_RUNTIME as an
additional default source
- Update shell script header comment to say "container runtime" instead
of "Docker"
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix(test): add autouse fixture to clear OMNIGENT_CONTAINER_RUNTIME
Prevents the host environment from leaking into tests that assume
the default runtime is "docker".
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* style: add missing blank line before autouse fixture
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* fix: address additional review comments on container runtime PR
- Rename _ALLOWED_RUNTIMES to ALLOWED_RUNTIMES (public API used
cross-module by the parser)
- Reject container_runtime: null in YAML instead of silently falling
back to the env var default
- Add test for container_runtime: null rejection
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
---------
Signed-off-by: Yuan Tang <terrytangyuan@gmail.com>
* feat(onboarding): report the installed-but-unconfigured harness state
Add the credential axis to the picker-facing readiness map so Claude, Codex,
and Pi report the yellow "installed but no credential" state — the signal the
web setup dialog needs to know when to offer an "Add credential" action
(the M2 keystone of Setup-From-the-UI). Purely additive: the values
("needs-auth" / "binary-missing") already exist in HarnessAvailability, and
the launch gate (harness_is_configured) stays binary-only, so a not-yet-authed
harness is never blocked from launching.
- New _family_provider_configured(): whether an omnigent-managed provider
(API key / gateway) serves the harness's family, reading the same config
omni setup's overview does. Subscription-kind is excluded (that lives in the
CLI's own login, judged by harness_cli_logged_in). Local, side-effect free,
never raises (fails to "no credential").
- Claude: ready when a provider is configured OR the CLI subscription login is
present (was CLI-login only — an API-key-only user wrongly showed yellow).
Checks config first to avoid the status subprocess on the common path.
- Pi: gains the axis it lacked entirely (was binary-only → always green once
installed). No CLI login, so binary + provider: installed-but-no-provider is
now "needs-auth".
Codex already had this (unchanged, the template). Qwen/OpenCode env-auth
unchanged.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* docs(onboarding): clarify _family_provider_configured checks entry presence
Polly review nit: the helper returns True when a non-subscription default
provider *entry* exists, not when its secret actually resolves — an entry
pointing at an unset env:/keychain ref still reads configured (matching the
secret-blind omnigent setup overview). Reword the docstring from "usable
credential" to "a default provider entry is present" and note the
secret-blind behavior + why it's safe (launch gate is binary-only; signal
only moves toward green). No behavior change.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* refactor(onboarding): address review nits on readiness detection
- Hoist the provider_config import in `_family_provider_configured` to the
module top (no circular import); update the test monkeypatch targets to the
now-module-bound name.
- Drop the internal milestone label from a test docstring.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
---------
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* feat(sandbox): parse and validate sandbox.kubernetes.pvc_mounts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* feat(sandbox): add pvc_mounts volumes to the runner Pod manifest
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* feat(sandbox): thread pvc_mounts through the kubernetes launcher
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* docs(deploy): document sandbox.kubernetes.pvc_mounts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* feat(sandbox): fail loud on unknown sandbox.kubernetes keys
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* test(sandbox): lock in pvc_mounts collision-order, null read_only, and claim-reuse semantics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* test(sandbox): pin the reserved-mount HOME prefix to the launcher's _HOME_DIR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* refactor(sandbox): reuse shared validators in the pvc_mounts parser
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(sandbox): close pvc_mounts reserved-path gaps from review
Reject mount_paths with exactly two leading slashes — POSIX normpath
preserves them so '//home/omnigent' passed both validation gates while
the kernel collapses '//' to '/' at mount time, shadowing HOME. Add
/opt to the reserved prefixes: the host image's omnigent venv lives at
/opt/venv and was shadowable. Both cases now covered in the fail-loud
parametrization.
Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
* fix(sandbox): reject pvc_mounts paths that mount over reserved prefixes
The reserved-path check only caught mount_paths at or under a reserved
prefix, so an ancestor like /home or /var passed validation while
mounting over the HOME emptyDir mountpoint or the Secret projections.
Reject ancestors too, and reserve /run, /var/run, and /var/lock in full
so the Debian image's /var/run -> /run and /var/lock -> /run/lock
symlinks can't alias around the lexical check.
Co-authored-by: Isaac
Signed-off-by: Bryan Li <bryan.li@gmail.com>
---------
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Related issue
Closes F-CR-6
## Summary
- `DeepLink.parse` validated the `/c/<id>` segment with only `!contains("/")`, but Foundation's `URL.path` is percent-DECODED — so `omnigent://host/c/id%3Fview=terminal` exposes `?` as a literal in the path and smuggles a query (and `%23` a fragment, `%2e%2e` a `..`, `%00` a control char) past the intended "/c/<id> only" shape. Added a denylist that rejects `?`, `#`, `/`, `.`, `%`, and control chars in the decoded id, so an encoded separator that `URL.path` decoded into one of those is dropped.
- The denylist deliberately does NOT assume the id's exact format (the server emits 32-hex uuids today, but the SPA's `/c/:id` route accepts any non-slash segment); the SPA stays the authority on id validity, and a future id scheme (ULID, nanoid, base64) won't be silently rejected. Benign non-canonical ids like `conv_abc` are accepted; only structure-smuggling is blocked.
- Documented the custom-scheme hijack risk in `DeepLink.swift`: iOS doesn't verify single ownership of `omnigent://`, so a co-installed app can read the link's host + id (metadata disclosure). For managed Databricks domains that can serve an `apple-app-site-association`, prefer verified Universal Links; the custom scheme is retained for BYO/OSS servers that can't host AASA, with the interception risk documented.
## Test Plan
- Unit tests (`OmnigentTests/DeepLinkTests`): 19 cases, all pass — including `testRejectsSmuggledQueryViaEncodedQuestionMark` (`%3F`→`?`), `testRejectsSmuggledFragmentViaEncodedHash` (`%23`→`#`), `testRejectsEncodedDotAndDotDot` (`%2e%2e`), `testRejectsControlCharacters` (`%00`/`%0A`/`%7F`), `testRejectsMalformedPercentEscape` (`%zz`), and `testAcceptsBenignNonCanonicalIds` (`conv_abc`/`x`/`not-a-uuid` are accepted — no smuggled structure).
- UI tests (`OmnigentUITests`): 6 cases via a DEBUG-only `--omnigent-open-url` launch-argument seam that routes the link through the real `handleDeepLink`/`DeepLink.parse` (XCUITest can't reliably deliver custom-scheme URLs on this toolchain). `testValidDeepLinkShowsConsent` (valid link → consent alert), `testBenignNonCanonicalIdIsAccepted` (`conv_abc` → consent alert), and rejection tests for smuggled `?`/`#`/`..`/control-char (no alert). A `--omnigent-reset-state` flag wipes persisted server state so each case starts with no known server. All pass on the iOS simulator.
- Manual simulator verification: drove `xcrun simctl openurl` against the running app with `OMNIGENT_DEEPLINK_TRACE` set; NSLog trace confirmed `ACCEPTED` for the valid link and `REJECTED` for all 5 smuggling/malformed links (smuggled `?`/`#`, `..`, control char, non-id).
## Demo
N/A
## Type of change
- [x] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [x] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [x] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Manually verified end-to-end on the iOS simulator: launched the app with `OMNIGENT_DEEPLINK_TRACE=1` and sent six real `omnigent://` links via `xcrun simctl openurl`. The NSLog trace showed `ACCEPTED` for the valid link and `REJECTED` for all smuggling/malformed links, proving the fix through the real `DeepLink.parse` → `handleDeepLink` path. The DEBUG-only `--omnigent-open-url` / `--omnigent-reset-state` launch-argument seam and `OMNIGENT_DEEPLINK_TRACE` NSLog logging are compiled out of Release builds (gated by `#if DEBUG`), so there is no production behavior change from the test infrastructure.
* fix(cli): normalize Azure Databricks custom-URL workspaces to their canonical host
omni login (and omni host) failed for Azure Databricks workspaces with a custom
(vanity) URL like https://mydomain.azuredatabricks.net/?o=<workspace_id>: the
vanity edge 303-redirects the unauthenticated probe to /login instead of
answering, so _databricks_workspace_login_target does not recognize the
Databricks posture and login fails. The canonical host
adb-{workspace_id}.{workspace_id % 20}.azuredatabricks.net does answer, and the
?o=<workspace_id> selector already carries the id.
Rewrite the custom host to the canonical adb- form in _resolve_server_url (the
shared normalization every --server entry point uses, so omni host is covered
too). Only *.azuredatabricks.net hosts that are not already the adb- form and
carry a numeric ?o= are touched; AWS/GCP hosts, canonical URLs, and URLs without
a selector are left unchanged.
Closes#2781
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
* fix(cli): probe before adopting the canonical Azure Databricks host
The custom-URL fix landed the canonical adb- host rewrite unconditionally in
_resolve_server_url, so a wrong synthesis could strand the user on a host they
never typed, and the unit tests only re-asserted the implementation's own
arithmetic (123 % 20 == 3), which would pass under any modulus.
Try the URL as the user gave it first. Only when that fails to resolve, and only
for an Azure vanity workspace URL carrying a numeric ?o=, synthesize the
canonical host, probe it, and adopt it if it answers. A dead synthesis now falls
back to the user's URL instead of replacing it.
The shard rule remains an observed regularity rather than a documented contract
(Microsoft calls the segment a random number and treats properties.workspaceUrl
from the ARM API as authoritative), so the probe keeps it off the load-bearing
path. Docstrings say so plainly.
Also:
- _canonicalize_azure_databricks_url is now _canonical_azure_databricks_url and
returns None to decline, so a caller can tell "not applicable" from "no change".
- Guard the selector with isascii() as well as isdecimal(): str.isdecimal()
accepts non-ASCII digits that int() also parses, which synthesized a
nonsensical host.
- _probe_root reduces a URL the way _workspace_api_server_url does before
probing. Without it the comparison against the expansion's result never
matched (it drops the ?o= selector first, and that selector is what makes a
URL a candidate), and the new probe requested /?o=123/v1/me.
- Replace the tautological shard assertions with five real observed
workspace/host pairs, and drive the resolver tests through the real expansion
with only httpx scripted, since a stubbed expander cannot catch the above.
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
* docs(cli): drop issue-number refs from Azure canonical-host comments
The repo's comment convention says code comments should describe the
scenario, not reference issue/PR numbers. Remove the (#2781) tags from
the _canonical_azure_databricks_url / _resolve_server_url docstrings and
the vanity-URL fallback test; the surrounding prose already explains the
Azure vanity-host case without needing the external link.
Co-authored-by: Isaac
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>
Co-authored-by: Pat Sukprasert <pat.sukprasert@databricks.com>
* fix(runner): resolve and re-materialize file attachments on remote-runner history reload
Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>
* fix(runner): seed the native-session compaction anchor; tolerate malformed file metadata
Native-harness sessions skip the history reload entirely, which also
skipped seeding the last server item ID that harness compaction
persistence anchors on — compactions then silently stopped persisting.
Session create now fetches just the newest item ID (newest-first, single
item, no attachment downloads) for native harnesses.
A 200 metadata response with an unparseable body no longer aborts
attachment resolution: both resolvers (the runner's message-content
resolver and the claude-native transcript rebuild) fall back to the
content response's Content-Type for the media-type hint.
Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>
* refactor(attachments): centralize file_id resolution and reference-line emission in native_attachments
The transcript rebuild and the runner each carried a full copy of the
file_id fetch-and-inline pipeline, and nine native executors repeated
the same materialize-or-marker block. Both now live in
native_attachments: resolve_file_id_block() serves the runner and the
transcript rebuild, attachment_reference_line() serves the executors,
and ATTACHMENT_MARKER_STRIP_PATTERN replaces four hand-copied forwarder
regexes. Materialized filenames are sanitized the same way as marker
names so a bracketed filename cannot break the marker consumers, and
the resume dedupe short-circuits on file size before reading bytes.
Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>
* fix(attachments): replay resolved history attachments as structured content
Cold-started claude-sdk sessions flattened prior turns into a text
prefix, so a resolved historical image reached the model as the marker
[image: name, media_type, N base64 chars]. The bytes never arrived, which
leaves the #882 symptom in place for that harness: the model describes an
attachment it cannot see.
Prior-turn attachments now replay as real Anthropic image/document blocks
via the existing converter, interleaved in transcript order. Text-only
history still takes the plain-string path and renders byte-identically,
unresolved attachments keep their existing marker, and base64 still never
enters prompt text.
Materialization also derives its collision suffix from a content hash
rather than a random one, so a history carrying two distinct uploads of
the same filename keeps one file per payload instead of gaining a copy on
every transcript rebuild.
The two tests that asserted the compact-placeholder shape are replaced by
cold-reload tests: that shape is the behavior being corrected, but the
invariant those tests protected (no base64 in prompt text) is asserted
against the prompt's text blocks.
Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>
* refactor(attachments): collapse duplicated prompt-shape branches
The structured and plain-text arms of _build_prompt returned the same
value whenever the latest message was multimodal, and re-scanned the
block list to decide which arm to take. Coalescing already leaves an
all-text history as one block, so the block count answers that.
Materialization's second identity check was a no-op guarding a write
that produces the same bytes, so the collision path flattens to one
branch.
Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>
* refactor(tests): keep runner conftest identical to upstream
Move the file-server fake's items/failure/malformed-meta behaviors out of
the shared _FakeFileServerClient into local subclasses in the one file
that uses them, so conftest.py stays in sync with upstream and per-test
modes stay next to their tests.
Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>
---------
Signed-off-by: Sunny Yang <sunnyadn1130@gmail.com>
Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
The before-quit handler defers the quit until serverManager.shutdown()
finishes, then re-issues app.quit() as the *only* way the quit ever
proceeds. Re-issuing app.quit() after before-quit's preventDefault() is a
known intermittently-unreliable Electron behavior (electron/electron#4994,
#33643, #39094); when it no-ops, or shutdown hangs (a stuck
'omnigent server stop'), the app stays up with its window still open —
matching 'sometimes the app is still running and refuses to quit'.
- Hard safety cap: app.exit(0) after quitCleanupTimeoutMs (unref'd) if
graceful cleanup + the re-issued quit haven't terminated. Normal cleanup
(<6s) completes well under the 10s cap; it only trips when stuck.
- Evaluate resolvedCliPath() inside an async IIFE so a future throw becomes
a rejection caught by .catch, never stranding the quit.
- Install fallback: when quitAndInstallIfPending() returns true but
quitAndInstall() doesn't actually quit (staged update gone), a short
app.exit(0) fallback still quits.
- unref() the periodic update-check setInterval so it can't keep the event
loop alive at quit.
Adds two regression tests (install-fallback and cleanup-cap) via an
injectable setQuitTimeouts; harness exposes setTimeout/clearTimeout/app.exit.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
The codex-native launch read the spec model only from
executor.config["model"], a key the single-file agent loader never
populates, so a custom agent's declared model: was silently replaced by
the provider default. Read the canonical executor.model first — the same
field the in-process harness and the claude/cursor native launches
consume — and keep config["model"] as a fallback for bundle specs that
pin the model inside the harness config block.
Co-authored-by: Isaac
* fix(loader): reject the bundle type:/config: nesting in single-file executor blocks
A single-file agent YAML written with the bundle config.yaml shape
(executor: {type: omnigent, config: {harness: ...}}) loaded without
complaint: the unknown keys were silently dropped, the declared harness
with them, and a different harness was inferred from the model prefix —
databricks-gpt-* landing on openai-agents instead of the declared
codex-native, with no diagnostics. Reject exactly type:/config: with an
error that shows the flat spelling. Other extra executor keys
(use_responses, extra, ...) keep loading — the compat loader reads them
from the raw YAML.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* test: spell e2e fixture executors flat instead of the bundle config: nesting
Six runtime-generated single-file agent YAMLs in the e2e/e2e_ui/server
fixtures nested the harness under executor.config — the exact trap the
loader now rejects. They only worked because the dropped harness was
re-inferred from the gpt-* model prefix as the same openai-agents value.
Spell them flat so the declared harness actually flows. The two
spec_version bundle specs (approval agent, elicitation supervisor) keep
the nesting — config.harness is the correct spelling on the strict
parser path.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
The background server spawned by bare `omni` (`_spawn_local_server`)
launched `omnigent.cli server` without `--config`, so the server's
loader returned an empty config and never read `~/.omnigent/config.yaml`.
Its `llm:` (and `policies:`) block was invisible to the detached server,
so self-hosted smart routing silently stayed off (`sys_advise_models` ->
`router_on: false`; `/v1/info` -> `smart_routing_enabled: false`).
Forward `--config <global_config_path()>` when the file exists. Same bug
class as #2386/#2763 (Docker entrypoint dropped `policies:`); this is the
local-spawn instance.
Co-authored-by: Isaac
Signed-off-by: Pranav Setlur <psetlur@gmail.com>
PR #3148 extracted _session_labels_for_runner_spawn into
omnigent.runner.native.orchestration, but _SESSION_STREAM_HEARTBEAT_S
and the stream loop that reads it remained in omnigent.runner.app.
test_session_stream_emits_heartbeat_on_idle located the module to patch
via _session_labels_for_runner_spawn.__module__, which now resolves to
omnigent.runner.native.orchestration — a module that has no
_SESSION_STREAM_HEARTBEAT_S attribute — so the test raised
AttributeError and failed CI on main.
Patch omnigent.runner.app directly, which is where the heartbeat cadence
constant and its consumer actually live.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
When the launching process sets CLAUDE_CODE_USE_GATEWAY=1, that
gateway-aware mode keeps tool search enabled so MCP schemas load on
demand. Setting CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS alongside it
would override that mode, disabling all betas and inflating startup
token usage.
Only set CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS when gateway-aware
mode was NOT selected.
Ported from databricks-eng/universe#2298829.
Co-authored-by: harry-yao_data <harry.yao@databricks.com>
Introduce no-op extension points on the conversation store so a subclass
can transform conversation_items.data and control search_text, without
changing OSS behavior:
- _encode_item_data(data_json): identity by default; append's data write is
routed through it so a subclass may compress or encrypt the payload.
- _decode_item_data_batch(stored_list): identity by default; the read paths
(list_items, list_latest_message_items_for_conversations, the FTS-ranked
read) decode a whole page of rows through it before building entities, and
_to_item now takes the already-decoded data. Making the read seam a batch
(not a per-row hook) lets a subclass decode a page in one pass — e.g. a
single bulk decrypt — instead of once per row.
- _item_search_text(item): extracts the search text as before by default;
may return None to skip persisting search_text (and its FTS row) on a
schema that omits the column.
Every default preserves current behavior exactly: the column stays plaintext
Text, and search/FTS are unchanged. This lets a downstream store (Databricks'
MySQL-homed conversation store) envelope-encrypt item payloads at the column
boundary while reusing append/list_items unchanged.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* docs(deploy): correct docker admin bootstrap flow (no auto-generated password)
Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
* docs(deploy): correct remaining generated-password and /data-persistence claims
Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
* docs(deploy): scrub generated-password flow from remaining platform guides
The Docker docs were corrected earlier, but fly / railway / render / modal /
hf-spaces still told operators to read a generated admin password out of the
logs / /data/admin-credentials — a flow that no longer exists (bootstrap never
auto-generates a password; the first admin is claimed via the web Create-admin
form or a pre-seeded OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD).
- Rewrite the first-admin step in each guide to the real flow, and drop the
fake "Created initial admin ... password: <generated>" log block.
- Add a first-visitor security note (unauthenticated /auth/setup while no
password-bearing account exists) to every public-facing guide; fold it into
hf-spaces' "make the Space Public" step where the exposure is most direct.
- render: correct the disk bullet (hashes live in Postgres, not on /data) and
the render.yaml comment that called the anchor path a password file.
Co-authored-by: Isaac <isaac@example.com>
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
Co-authored-by: Pat Sukprasert <pattara.sk127@gmail.com>
Co-authored-by: Isaac <isaac@example.com>
* feat(cli): add `omnigent session import` (inverse of session export)
`session export` writes a portable JSONL but there was no way to load it
back — inspecting a shared/exported session meant hand-writing items into
the store. Add `session import` to close the round-trip: it reads the
session_meta + item lines and recreates the conversation on the target
server as a new session (fresh id each time) via POST /v1/sessions with
the history passed as initial_items.
Details:
- De-aliases the `model` serialization alias back to `agent` per item and
validates each with parse_item_data() client-side before the request.
- Agent binding: reuse the exported agent_id when it exists on the target
server; else fall back to the built-in native agent for the export's
harness (mirrors /v1/imports); else fail with a clear message.
- Creates history-only (host_type=external, no host_id) so no runner
launches. Carries over title/workspace/harness/model/effort overrides.
Known limitation (documented in --help): the server seeds initial_items
under a single synthetic response_id, so exact per-turn grouping is not
preserved. Fine for viewing/debugging; a follow-up server route could
preserve it if needed.
Verified end-to-end: imported the real 260-item export, re-exported, and
diffed — identical item counts and types, agent bound, model<->agent
alias round-trips.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(cli): scope agent→model de-alias to alias-bearing item types
Polly review caught that the import de-alias applied `model`→`agent` to
every item type, corrupting the two types where `model` is a genuine
field: `compaction.model` (silently dropped) and `routing_decision.model`
(required + collides with its own `agent` field → hard import failure for
any smart-routed session).
Derive the alias-bearing types from the data-model field definitions
(serialization_alias == "model") so the reverse map only fires for
message/function_call/reasoning/slash_command and can't drift. Add
regression tests for compaction and routing_decision.
Also address non-blocking review notes:
- Wrap non-404 create errors in a clean ClickException instead of a raw
httpx traceback.
- Document created_by re-attribution in --help alongside the response_id
caveat.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The hermes-native forwarder's messages SELECT omitted the reasoning
columns Hermes persists, so thinking shown in the TUI never reached the
web conversation. Read reasoning_content/reasoning and emit a one-shot
external_output_reasoning_delta before the assistant message (started=True),
matching the codex- and opencode-native transient reasoning contract. The
structured codex_reasoning_items column is left alone.
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
The codex harness wrap read only HARNESS_CODEX_CWD, so when the spawn env
omits that var the executor fell through to os.getcwd(). Seven sibling
harnesses (acp, claude-sdk, goose, hermes, kimi, pi, qwen) already fall
back to OMNIGENT_RUNNER_WORKSPACE first.
tests/runtime/test_spawn_env_cwd.py::test_builder_omits_cwd_when_none
documents that the builder omits the CWD var precisely so the harness can
apply its own OMNIGENT_RUNNER_WORKSPACE fallback. codex is in that test's
builder list but never held up the harness half of the contract.
Every current caller threads a cwd, so this changes no observed behavior
today. It closes the contract gap and covers a caller that omits it.
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
* ✨ feat(cli): add `omni usage` cost report
Summarize LLM spend across a user's sessions: rolling 24h / 7d / 30d
cost totals plus a per-session breakdown of model and cost.
- server: `GET /v1/usage` aggregates each top-level session's subtree
usage (via `load_session_usage`), scoped to the caller, bucketing
cost by last-activity time; normalizes the primary model per session.
- cli: `omni usage` (`--limit`, `--server`, `--json`) renders the
report through the shared `omnigent.inner.ui` palette.
Co-authored-by: Isaac
Signed-off-by: Jakub Majorek <majorek.jakub@gmail.com>
* ✨ feat(usage): address review — separate router, per-model breakdown, daily-rollup windows
Addresses the four review comments on the `omni usage` cost report:
1. Move the report to its own user-scoped router (omnigent/server/routes/
usage.py) instead of the session-scoped sessions router.
2. Rename the schema UsageSession -> SessionUsage.
3. Show a per-model cost breakdown per session, mirroring the web session
sidebar: authoritative session total on the id line, each model's
recorded cost beneath (shown faithfully, not forced to sum). Single-model
sessions stay on one line.
4. Source the cost summary (Today / Last 7 days / Last 30 days / All time)
from the per-user daily-cost rollup (user_daily_cost) via a new
sum_daily_cost range read, so windows reflect when spend occurred rather
than a session's last-activity time. Labels relabeled to calendar-day
truthful wording.
Regenerates openapi.json; updates unit + e2e tests.
Co-authored-by: Isaac
Signed-off-by: Jakub Majorek <majorek.jakub@gmail.com>
---------
Signed-off-by: Jakub Majorek <majorek.jakub@gmail.com>
* test(ui-snapshot): add sidebar pinned-project flyout baseline
The populated-sidebar baseline covers every sidebar row type but not the
hover flyout that surfaces a pinned session's originating project — the
card is portalled and only mounts on hover, so a restyle of it (recently
aligned to a compact HoverCard: clamped title + folder icon + project
name) sails through that gate.
Add a visual test that hovers a pinned, project-owned row and captures
`PinnedProjectFlyoutContent`. Mirrors the populated-sidebar fixture's
determinism (pinned clock, silenced updates socket, seeded localStorage);
the flyout's 150ms openDelay fires under set_fixed_time since only Date.now
is pinned, so a plain hover opens it.
Baseline PNG intentionally omitted — generated in CI's pinned image via the
`update-ui-snapshot` label so it matches the gate byte-for-byte.
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>
The ChatGPT desktop app writes model_reasoning_effort = "ultra" into
~/.codex/config.toml; the codex CLI forwards it as the retired "max"
wire value, which the OpenAI Responses API rejects with
invalid_value: 'max' (its ladder tops out at xhigh). Because the codex
harness copies the user config verbatim into every per-session
CODEX_HOME, every codex turn fails on such machines — including debby's
gpt sub-agents.
Two-part fix:
- validate_effort() coerces a deprecated alias (ultra/max -> xhigh) when
the raw value is unsupported but the canonical one is. Providers that
genuinely support max (Anthropic) are unaffected. This also stops the
server rejecting external_reasoning_effort_change events from
ChatGPT-app-configured codex terminals that report effort ultra.
- _populate_codex_home_config() normalizes a deprecated top-level
model_reasoning_effort in the session's private config.toml copy;
keys inside tables and supported values are left untouched, and the
user's real ~/.codex/config.toml is never modified.
_normalize_copied_codex_effort() now tracks array bracket depth so a
top-level multiline array's continuation lines (which can themselves
start with "[") are never mistaken for a table header — otherwise a
still-top-level model_reasoning_effort key after such an array would be
skipped. Also updates the two reasoning-effort-validation tests that
asserted "max" was rejected outright: since max/ultra now coerce to
xhigh for codex and the OpenAI Agents SDK, those tests now assert the
coercion instead.
Fixes#2696
Signed-off-by: Bryan Chua <me@bryanchua.com>
* fix(runtime): strip base64 image data from stored history on replay
The native-ingest strip only helps images read *after* that fix landed.
Sessions already in the conversation store still hold full base64 images
in their function_call_output items, so they keep overflowing the context
window on resume — replaying the stored output as prompt text wedges
compaction (loads over-window history to summarize, fails "prompt is too
long", writes no boundary, re-overflows).
Strip inline base64 image blocks at the replay boundary in
history_to_input_items, where every harness's stored history is converted
to LLM input. This fixes already-stored large-image sessions without a
store migration. A base64 image tool result (JSON list of
{"type":"image","source":{"type":"base64",...}} blocks) is rewritten to a
"[<media> image omitted from history …]" placeholder that points back at
the originating tool call so the image stays recoverable on demand.
Plain-text and non-image JSON outputs (the common case) pass through
unchanged via a cheap guard before any JSON parse.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runtime): strip base64 from truncated (invalid-JSON) image outputs
Testing against the real wedged session's export revealed the JSON-only
strip was a no-op on exactly the data that matters: stored image outputs
are clipped at the conversation-store 245760B cap, leaving the base64
string unterminated, so json.loads raises and the original (base64-laden)
output was returned unchanged.
Add a linear regex fallback that rewrites an image source block in place
when the output is not parseable JSON. The pattern uses fixed optional
key groups and a base64-alphabet char class disjoint from the quote
terminator, so it cannot backtrack catastrophically against a
multi-hundred-KB payload (an earlier lazy-quantifier attempt hung).
Verified on the real 3440987444542977 export: all 4 truncated image
items strip, 982,448 -> 832 chars (99.92%), sub-ms. New test covers the
truncated/invalid-JSON shape.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(claude-native): strip truncated base64 images on cold resume
Native Claude Code resumes from its own local transcript, which the
wrapper rebuilds from Omnigent items before `claude --resume`. Intact
image tool results are intentionally rehydrated into real image blocks
(cheap ~1.5K tokens). But an output clipped at the conversation-store
byte cap holds corrupt/partial base64 that no longer parses: rehydration
fails, so the raw ~250K-char string was sent as tool_result text AND
stashed in toolUseResult — re-overflowing the resumed context and
wedging compaction (the exact native failure users hit).
Collapse only that truncated/unparseable-image case to a recoverable
placeholder before building the record, so both the tool_result content
and the toolUseResult metadata stay small. Intact images still resume as
images.
Verified on the real 3440987444542977 export: full transcript rebuild
drops from 1,549,700 to 563,994 chars with zero base64 leak, while a
valid image still rehydrates to an image block.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Merge caller-supplied headers threaded through connection_params so MAS
can route CP serving-endpoint calls through the Barnacle forward proxy
(host + s2s auth headers). Also log the upstream error body on 4xx/5xx
for both non-streaming and streaming requests, which raise_for_status()
otherwise omits — essential for debugging CP serving/gateway failures.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The in-session config gear PR left comments that narrated the change
(a now-deleted IntelligentModelControl reference, "moved OUT of the picker
trigger", "no longer a standalone toggle", "old/pre-gear picker") and named
a "picker trigger"/"Agent picker" that no longer exists. Rewrite them to
describe current behavior — where the Smart Routing toggle, harness label,
and model/effort label live — per the repo's "describe the scenario, not
the change history" guidance.
Comment-only; no behavior change.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* feat(databricks-adapter): use SDK Config for OAuth token refresh
Cache a databricks.sdk.config.Config per profile and call authenticate()
on every request so OAuth tokens are refreshed transparently instead of
expiring after ~1 hour. Falls back to resolve_databricks_workspace when
the SDK is unavailable.
This addresses the v1 limitation documented in credentials/databricks.py.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): hide per-turn Smart Routing toggle when Auto harness is selected
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(new-chat): hide Smart Routing checkbox in favour of Auto harness
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(auto-harness): propagate routing error to UI via routing card
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(smart-routing): route harness+model for child sessions via sys_session_send
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(smart-routing): force auto-harness for sub-agents when parent routing is on
When the parent session has smart routing enabled, a sub-agent created via
sys_session_send is now routed regardless of the harness/model the
orchestrator chose — the server forces the "auto" sentinel at child-session
create time, ignoring the tool call's agent/model args. The first-message
routing path then picks both harness and model.
Skips native-terminal wrapper labeling for forced-auto children so the
harness isn't prematurely fixed (routing may pick a non-native SDK harness);
the child takes the SDK routing path where auto-resolution runs.
Only applies to omnigent-executor agents (auto needs a swappable brain
harness); non-omnigent children keep the orchestrator's choice.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): persist cost_control=on for Auto sessions, hide composer routing toggle
- New-chat create body sends cost_control_mode_override="on" when harness=auto
so the persisted state matches the routing that always runs for auto sessions.
- Hide the per-turn composer routing icon entirely — it's superseded by the
Auto harness (routes at session start), and its "off" state was misleading.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): exclude databricks-claude-haiku-4-5 from pi routing candidates
pi routes Claude models through the Anthropic Messages gateway, whose request
path adds an eager_input_streaming field the Databricks serving endpoint
rejects with a 400 when tools are present. Filter the model out of pi's
candidate list in route_session_harness (both live-catalog and static paths)
so Claude work routes to claude-sdk instead. Keeps pi's GPT models.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): prevent double-routing on forced-auto child sessions
The auto-harness resolution block and the per-turn routing block both called
route_session_harness on a forced-auto child's first message (parent routing
on + harness_override="auto"), causing two judge calls, two routing cards, and
a possible harness/model mismatch between the two picks. Track whether the auto
block routed this turn and skip the per-turn block when it did. Also fixes the
failure-path card duplication (auto emits an applied=False card, then no longer
falls through to a second card).
Cleanup: except (ImportError, Exception) -> except Exception in the databricks
adapter (Exception already subsumes ImportError).
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(auto-harness): mirror routing card into parent session for sub-agents
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): map live-catalog worker names to harness ids for routing
The live runner catalog (fetch_runner_models) keys rows by worker name —
sub-agent names like "claude_code" plus "self" — not by harness id. So
route_session_harness found no matches for _AUTO_ROUTING_HARNESSES and
returned "No routable harnesses are available", especially for child
(sub-agent) sessions.
Normalize worker names to harness ids via _WORKER_NAME_TO_HARNESS
(claude_code -> claude-sdk, codex, pi), and fall back to the static
infer_models table when the live catalog yields no routable candidates
(e.g. a catalog with only an unrecognized "self" worker).
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(ci): remove dead _ROUTABLE_HARNESSES and effectiveHarness (noUnusedLocals)
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test: update child-session routing test for forced-auto (route_session_harness)
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test: remove dead Smart Routing dialog tests (superseded by Auto harness)
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): exclude gpt-5.5/5.6 reasoning models from pi routing
pi routes GPT models through the openai-completions (/chat/completions) path.
Databricks applies a default reasoning_effort for the gpt-5.5/5.6 reasoning
models there and rejects tool calls with "Function tools with reasoning_effort
are not supported for gpt-5.5 ... use /v1/responses or set reasoning_effort to
'none'." pi's provider can't send that override, so every tool turn 400s.
Exclude databricks-gpt-5-5, -5-5-pro, and the -5-6 family from pi's routing
candidates (same pattern as pi+claude-haiku). The gpt-5.4 family works on pi
and stays; codex serves gpt-5.5+ via the Responses API natively.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): redirect incompatible router verdicts off pi
Some external routers ignore the filtered candidate set we send and still
return an excluded (harness, model) pair — e.g. pi + gpt-5-5. Since we can't
stop the router choosing it, post-process the verdict: redirect a Claude model
on pi to claude-sdk and a gpt-5.5/5.6 reasoning model on pi to codex (which
serves them via the Responses API). The chosen model is preserved; only the
harness is corrected to one that can actually run it with tools.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* style: ruff format test_sessions_model_override
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): order codex before pi so GPT models default to codex
_AUTO_ROUTING_HARNESSES order is both the candidate-set insertion order and
the tiebreak when a model is served by multiple harnesses (the external
router's id-only fallback and our own model-ownership fallback both pick the
first harness owning the model). With pi before codex, a GPT model with no/
ambiguous harness resolved to pi — whose openai-completions path 400s on
gpt-5.5+ reasoning models with tools. Reorder to codex, pi so GPT defaults to
codex (Responses API, handles reasoning+tools).
Complements _redirect_incompatible_pick, which handles the separate case of a
router returning an explicit pi+gpt-5.5 pair despite our filtered candidates.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): stop filtering candidates; router requires full model set
The external task_v0 router enforces a required model set (e.g. must include
gpt-5-6-luna) and returns 400 "task_v0 requires [...] models" when any is
missing. Our _filter_excluded_models pruning stripped gpt-5.5/5.6 and Claude
models from pi's candidates, making the required set incomplete and 400-ing
every route call.
Send the full candidate set unfiltered and rely solely on
_redirect_incompatible_pick to correct an incompatible (harness, model)
verdict after the router responds. Removes the now-unused _filter_excluded_models.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): emit routing card after input.consumed so it renders
The auto-harness routing card (success and failure) was published to the live
SSE stream at resolution time — before the runner forward and before
input.consumed. The user-message bubble hadn't been delivered yet, so the
reducer dropped/misordered the card and it never appeared live (only on
reload). Defer the card emission to after input.consumed, matching the
per-turn routing path's ordering. Now the "router unavailable" failure card
shows in the UI.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): refresh external router OAuth token per call
ExternalRoutingClient captured its bearer once at server startup (from the
routing profile), so after ~1h the token expired and the router 401'd
("Credential was not sent or was of an unsupported type"), which surfaced as
"router returned no verdict". Pass the Databricks profile through and mint a
fresh bearer per route() call via the SDK Config (same OAuth-refresh pattern
as the DatabricksAdapter fix). An explicit api_key still uses a static bearer.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(auto-harness): surface the router's actual error in the failure card
The auto-harness failure card showed a generic "router returned no verdict".
ExternalRoutingClient swallowed the real reason (401, task_v0 required-model-set,
etc.) — only logging it. Record it on client.last_error and have
route_session_harness surface it, so the UI card reads e.g. "Routing
unavailable: router returned HTTP 401: Credential was not sent or was of an
unsupported type". _router_error_detail unwraps the gateway's nested JSON
error envelope to a clean message.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): route sub-agents against the parent's catalog
A sub-agent's own runner catalog is "self"-only (it's a leaf spec with no
sub-agents), so _WORKER_NAME_TO_HARNESS didn't recognize it and routing fell
back to the small static infer_models lists — a different, incomplete candidate
set than the top agent sees (which broke the external router's required-model
check, e.g. missing glm-5-2/gpt-5-6-luna).
Add catalog_session_id to route_session_harness and pass the parent session id
for sub-agent routing (parent + child share a runner). The parent's catalog
enumerates the full spawnable-worker map (claude_code/codex/pi with complete
model lists), so a sub-agent now routes against the same stable candidate set
as the orchestrator — regardless that we route both harness and model for it.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test(routing): assert external client defers profile auth to per-call
_build_external_routing_client no longer resolves a Databricks profile
token at build time — the client mints a fresh bearer per request (OAuth
refresh) so it survives ~1h token expiry. Update the test to assert the
profile is threaded through (no eager resolve, no static _auth) instead
of the old build-time resolution contract.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): align sidebar session flyout and row padding
The session hover flyout and the sidebar rows were visually inconsistent
with the pinned-project flyout and project folder rows:
- The plain session tooltip used a wide card (w-72, bg-card-solid) while
the pinned-project flyout used a compact HoverCard look. Restyle the
tooltip to mirror it (w-64, bg-popover, clamped title, muted metadata).
- Both flyout titles used rem-based `text-sm`, which scaled with the UI
font-size setting and rendered larger than the fixed-px sidebar rows.
Size both to `sidebar-compact-text` so they match the row name exactly.
- Session rows used `w-[calc(100%+1rem)]`, bleeding ~8px past the right
edge so their highlight didn't align with the project/folder rows.
Switch to `w-full` and shift the trailing pin/kebab controls inward
(right-[30px] / right-1) so they stay inside the row edge.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): drop reserved scrollbar gutter so sidebar rows sit flush right
The sidebar scroll container reserved a stable scrollbar gutter
(`scrollbar-gutter: stable`), which on overlay-scrollbar platforms
(macOS) leaves ~15px of empty space on the right of every row. That made
rows look uncentered — 8px inset on the left vs. 8px + 15px on the right —
and misaligned the project-folder header actions with the session-row
controls. It's also why session rows previously used `w-[calc(100%+1rem)]`
to paint over the gutter (the workaround this series already removed).
Drop the reserved gutter so the right inset collapses to the same 8px
`px-2` as the left. On overlay scrollbars there's no layout shift; the
rows and folder-header actions now line up on both edges.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): match project-folder header controls to compact session kebab
The project-folder header pencil + kebab used `icon-sm` (size-7, 28px)
while the session-row kebab uses `icon-xs` (size-6, 24px). Both anchor at
`right-1` with a centered `size-3.5` glyph, so the 4px width difference
put their glyph centers in different columns — the folder ⋯ sat ~2px left
of the row ⋯ and read as misaligned.
Drop the folder-header controls to `icon-xs` so they share the compact
size (and glyph column) with the session-row kebab.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): match folder-header icon spacing to session row
The folder-header pencil + kebab sat in a gapless flex, while the session
row's pin↔kebab pair has a 2px (right-1 vs right-[30px]) gap. That put the
folder pencil 2px right of the session pin, so the leading-icon columns
didn't line up across row types.
Add `gap-0.5` to the folder-actions flex so the pencil lands in the same
column as the session pin; the kebabs already share the trailing column.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): shrink Projects group-header controls to compact icon
The "New project", "Expand all", and "Collapse to previous" controls in
the Projects group header were still `icon-sm` (size-7, 28px) while every
other right-gutter control — folder-row and session-row pin/kebab — is now
`icon-xs` (size-6, 24px). The larger buttons broke the shared icon column.
Drop all three to `icon-xs` so the whole sidebar right-gutter shares one
compact size.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* refactor(web): share one flex container for sidebar row trailing controls
The session row's pin + kebab were two separately absolute-positioned
buttons, so their spacing was hand-tuned per button and drifted from the
project-folder header actions at non-default font scales. Wrap both in a
single `absolute right-1 flex items-center gap-0.5` container — the same
pattern the folder header already uses — so the spacing is defined once
and stays aligned across every right-gutter control at any scale. Also add
the matching gap-0.5 to the Projects group-header controls.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): regenerate visual baselines
* fix(web): reserve scrollbar gutter symmetrically instead of removing it
Removing `scrollbar-gutter: stable` fixed the right-edge asymmetry on
macOS overlay scrollbars but reintroduced horizontal reflow on classic-
scrollbar platforms (Windows/Linux) when the scrollbar appears/disappears.
Use `stable both-edges` instead: the gutter is reserved symmetrically on
both sides, so rows stay centered against the left `px-2` inset and never
reflow — a no-op on overlay scrollbars, correct on classic ones.
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>
The Phase 0 section listed pre-split line counts and framed the cli.py and
sessions.py extractions as to-do, but both have shipped. Update it to reflect
actual state: correct the counts, mark cli.py (#3047) and sessions.py (#3097)
done, and leave runner/app.py and test_app_sessions_native.py as the two
remaining >10k files (which can proceed in parallel). Move chat.py to a
deferred bucket since it is already under the 10k target.
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* test(runner-init): guard fork-history directives survive the reconnect envelope
Adds an integration test across the exact seam that regressed in #2793 and
was fixed in #3116: a forked claude-native session's fork directives
(carry-history, source-external-session) must survive from the store's
by-runner-id reconnect lookup into the session-init envelope the runner
reads to decide whether to clone/rebuild the vendor transcript.
Unlike the existing envelope tests (which hand-build an envelope with the
label already present) and the store unit test (which checks one method in
isolation), this drives the real store end to end — create a native source
with a captured external_session_id + workspace, fork it with
carry_history_into_native, bind it to a runner, then run
list_conversations_by_runner_id -> build_runner_session_init_payload ->
parse -> _claude_launch_metadata_from_envelope and assert the fork
directives land as launch metadata. It fails if any layer on that path
stops carrying labels (verified: reverting #3116's hydration makes it fail
with an empty label set).
Runs in CI (no vendor Claude login), unlike the opt-in
tests/e2e/test_host_claude_native_fork_e2e.py that would otherwise be the
only coverage of this path — which is why the original regression slipped
through.
Co-authored-by: Isaac
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: repair test docstring indentation broken by suggested edit
A GitHub-suggested "Potential fix for pull request finding" commit
(b48c50b3) rewrote the test docstring flush-left, leaving the function
with no indented body -> IndentationError, which failed ruff-format,
ruff-check, and pytest collection (server-rest).
Restore a properly-indented docstring and switch the em-dashes/arrows in
comments to ASCII so the file is unambiguously parseable everywhere. Test
behavior is unchanged: still passes with #3116's label hydration and fails
without it (verified by reverting the fix).
Co-authored-by: Isaac
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* refactor(server): split sessions route into facade + impl package
The sessions route had grown to ~15k lines in a single file, well past
the 10k-line ceiling we want for maintainability and ahead of the
native-harness pluggability work that will touch this module heavily.
Split it into a facade over an implementation package:
- sessions.py (7.7k) stays the public entry point, keeps
create_sessions_router, and re-exports the impl modules via `import *`.
- _sessions/common.py, helpers.py, orchestration.py hold the
implementation, layered common -> helpers -> orchestration, each
star-importing the ones below it.
No behavior change. Symbols that tests patch on the facade are exposed
through call-time proxies that delegate back to the facade, so a
`monkeypatch.setattr(sessions_mod, ...)` is honored no matter which impl
module resolves the name. F403/F405 are waived for these files in
pyproject since star re-export is the point of the facade.
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(sessions): honor facade monkeypatch across _sessions impl modules
The facade/_sessions split re-exports symbols via `import *`, so each impl
module holds its own binding of every name. A test's
`monkeypatch.setattr(sessions, "_kick_managed_wake", ...)` rebound only the
facade attribute; sibling impl callers kept their stale star-import binding and
ran the real path, breaking managed-wake and compact single-flight tests.
Route the patched symbols (`_kick_managed_wake`, `_compact_lock`) through
call-time facade proxies with the real body renamed `*_impl`, and add explicit
facade override imports so the patch is honored no matter which module resolves
the name.
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(sessions): route impl-module get_agent_cache/session_stream through facade proxy
Drop the function-local `from omnigent.runtime import get_agent_cache`
and `from omnigent.runtime import session_stream` imports in the impl
modules. Those locals shadowed the module-level facade-delegating
proxies (bound via the `# noqa: F401` import block from
`_sessions.common`), so a `monkeypatch.setattr` on the facade was not
honored at those call sites.
Removing the shadowing imports lets the already-bound module-level
proxies resolve the names, keeping facade patches effective while
behaving identically when unpatched (the proxy forwards to the real
runtime symbol). Addresses Copilot review on the sessions split.
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(sessions): repair cross-module seams from the facade split
The _sessions split moved code behind an explicit __all__ per impl module
and a star-import facade, which introduced three latent seams:
- _validated_harness_override_executor_type was omitted from
helpers.__all__, so the harness_override == "auto" gate in
orchestration (which sees it only via star-import) hit NameError at
session creation. Add it to __all__.
- _query_host_runner_status read _HOST_RUNNER_STATUS_TIMEOUT_S off its
own star-import binding, so a facade-level monkeypatch was dropped.
Read the constant off the facade module instead; strengthen the
timeout test to assert the wait actually bails early.
- _wait_for_managed_runner_tunnel and _run_managed_wake read
_HOST_RELAUNCH_RUNNER_CONNECT_TIMEOUT_S bare; qualify both through the
facade for the same reason.
Add test_sessions_facade_exports.py to pin these re-export seams so a
dropped __all__ entry or un-re-exported constant fails at import time.
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(sessions): restore call-time get_agent_cache import in resolvers
The split dropped the call-time `from omnigent.runtime import
get_agent_cache` local import from the four harness/model resolver
functions. Without it the name resolved to the module-level facade
proxy, which forwards to a snapshot binding taken at import time, so a
test patching `omnigent.runtime.get_agent_cache` was no longer honored
and the call hit the real uninitialized runtime.
Restore the local import in _resolve_llm_model, _resolve_harness_impl,
_validated_harness_override, and _validated_harness_override_executor_type
to match pre-split behavior.
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* feat(web): in-session composer config gear modal
Bring the new-session gear-config affordance (#3050) into the in-session
composer. A gear icon left of the send button shows the session's live
run-config on hover and opens a config modal on click, consolidating the
mid-session switchable knobs — Model, Effort, and Smart Routing — behind one
control. Permission/approval/cursor modes stay launch-time only and are
intentionally absent.
What changed:
- New ComposerConfigGear + SessionConfigModal: draft Model/Effort/Smart Routing
and apply on Save (Cancel discards), mirroring HarnessConfigModal. Save
commits SEQUENTIALLY (awaiting each PATCH) because claude-native applies
model/effort by typing separate /model and /effort slash commands into its
terminal — firing them concurrently interleaves the injections into one bad
line. Unchanged knobs are skipped.
- The <Model> <Effort> control is now a read-only status label, not a dropdown
(the gear owns config); bare /model opens the modal. The label reads "Smart
Routing" when routing is on, and falls back to the harness identity
("Polly (Pi)") for SDK/bundle agents that surface no model/effort.
- Harness identity moved out of the status-line tray into the gear tooltip.
- The gear is soft-disabled (aria-disabled + click guard, tooltip preserved)
when the session isn't live, since a config PATCH can't wake a sleeping
runner and those states never load the model catalog.
- Extracted ConfigRow / DescribedSelect / MODEL_SELECT_* sentinels from
NewChatDialog into web/src/components/HarnessConfigControls.tsx for reuse.
- Removed the standalone IntelligentModelControl and its per-turn verdict chip;
Smart Routing now folds into the Claude Model dropdown (a Switch for other
routable agents).
Smart Routing eligibility is unchanged (same isCostRoutingSession gate the
prior control used); a KNOWN GAP note documents that the in-session gate is
stricter than the new-session dialog's routable-harness rule, to be aligned in
a follow-up.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): restore host/context tray + fold Smart Routing into Codex model dropdown
Two follow-up fixes on the in-session composer gear modal:
- Restore the composer status-line tray (host badge + context ring) for
host-bound sessions that have no worktree branch and no context ring yet
(e.g. codex). Removing the harness label from the tray also dropped it from
the render guard, which had been the de-facto "always render for a bound
session" trigger — so the whole shelf vanished. Gate on a `showHostBadge`
(host-bound + non-sub-agent) signal instead. Fixes the failing
test_host_badge / test_hosts_changed_push e2e specs.
- Fold Smart Routing into the Model dropdown for ANY agent that has one
(Claude and Codex), not just Claude. Previously Codex got both a standalone
Smart Routing switch AND a Model dropdown whose selected value could become
the routing sentinel with no matching option (empty trigger). The rule is now
"has a Model dropdown" (showModels): fold in when it does, standalone Switch
only for routable agents without one (e.g. Polly).
Both covered by regression tests (host-bound tray renders with no branch/ring;
Codex folds routing into its dropdown with no standalone switch). Verified the
previously-failing host-badge e2e specs and the gear-modal e2e specs pass
locally.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(ui-snapshot): update visual baselines for composer gear modal
The composer now shows a read-only model/effort label + config gear (and
the harness label moved into the gear tooltip), which changes the chat
conversation render. Regenerate the three drifting visual baselines from
the PR's CI-rendered artifact (byte-identical to the pinned Playwright
image the UI Snapshot gate compares against) so the gate passes.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* refactor(web): drop orphaned IntelligentModelControl + verdict exports
This PR relocated the standalone Smart Routing control into the composer
gear modal and removed its only app-code usage, leaving
IntelligentModelControl, parseCostRoutingVerdict, CostRoutingVerdict,
verdictRelativeTime, ModelTierPill, and COST_CONTROL_PLAN_LABEL with no
remaining consumers (only their own tests). Delete them and their tests.
Keep the still-used exports: isCostRoutingSession (ChatPage eligibility
gate), CostControlMode (NewChatDialog), and shortModelName (StatusBlocks
+ SmartRoutingCard). Fix the stale {@link ModelTierPill} JSDoc reference
in SmartRoutingCard.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(ui-snapshot): exercise the composer config gear in the chat baseline
The chat visual-snapshot fixture served a bare session (no omnigent.wrapper
label, no model_options), so modelPickerKind was null and the composer's
config gear + read-only model/effort label never rendered — the baseline
couldn't guard them. Patch the mocked session into a claude-native wrapper
(labels + harness + llm_model + model_options, mirroring the model-picker
e2e), and wait for the gear + model/effort label before capture, so the
baseline now covers the new composer surface.
The committed [linux] baseline PNG is regenerated separately from the CI
render (no Docker locally); verified on a throwaway [darwin] render that the
gear + "Sonnet 5" label now appear.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(ui-snapshot): regenerate chat baseline capturing the composer gear
Adopt the CI-rendered [linux] baseline (byte-identical to the pinned
Playwright image the gate compares against) now that the fixture renders
a claude-native session: the composer shows the config gear + "Sonnet 5"
model/effort label.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): don't re-pin a leaked sticky on routing-off; use effort sentinel
Two non-blocking review notes:
- Routing-off on a no-dropdown routable agent (e.g. Polly) entered the
model-commit branch and could setModel(resolvedModelId) where
resolvedModelId resolves the leftover cross-session sticky
(sessionModelOverride ?? selectedModel) — pinning a model the user never
chose. Gate the routing-off re-pin on showModels: only agents with a Model
dropdown re-pin; no-dropdown agents clear via setModel(null).
- The Effort select reused MODEL_SELECT_DEFAULT as its "none" sentinel;
switch to the purpose-built EFFORT_SELECT_NONE for consistency with the
new-session dialog.
Adds a regression test proving a leaked "gpt-5.5" sticky is not pinned when
turning routing off on an SDK/bundle agent.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(claude-native): strip base64 image data from tool-result history
Reading an image file via Claude Code's Read tool returns the image as
a list of {"type":"image","source":{"type":"base64",...}} blocks. The
transcript mirror serialized that content verbatim into the stored
function_call_output, so a single image cost ~245KB (~70K+ tokens) of
literal text. On resume the native harness replays these items as prompt
text, and a handful of image reads overflows even a 1M context window —
which then wedges compaction (it must load the same over-window history
to summarize, fails with "prompt is too long", writes no compaction
boundary, and re-overflows on the next resume). The base64 is useless to
the model as text anyway.
Strip inline base64 image blocks to a "[image omitted from history]"
placeholder before serializing the tool-result output. Observed on a
real wedged session: 245,080 -> 55 chars per image (99.98% reduction),
eliminating the ~281K-token replay overrun.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(claude-native): make stripped-image placeholder recoverable
The base64-strip placeholder was a dead "[image omitted from history]"
marker. Since a stripped image always comes from a tool call (e.g. Read
of a file path) that is preserved intact right before the output, the
agent can view the image again by re-running that call. Name the media
type and say so in the placeholder, so the image is recoverable on
demand rather than appearing silently lost.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Non-streaming chat_response_to_response stored message.content raw, so
for Claude via Databricks (and Kimi, etc.) — which return content as a
list of typed blocks — OutputText.text became a list instead of a str.
This broke prompt_policy (fail-closed DENY on .strip() of a list) and
any non-streaming consumer of databricks-claude-* models.
Reuse the existing _extract_delta_content helper (already used by the
streaming path) to flatten list-of-blocks content into a string; it
returns the plain string unchanged for existing providers.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Forked claude-native (and other native) sessions launched the vendor
TUI with no prior conversation history, even though the fork copied the
history into the store (the web UI showed it). The runner never received
the fork directives that drive transcript seeding.
Root cause: list_conversations_by_runner_id built its Conversation
entities without fetching labels, so they carried labels={}. The runner
reconnect path (_on_runner_connect) sources conversations from this
lookup and builds the session-init envelope from conversation.labels;
with an empty label set the fork directives (omnigent.fork.carry_history,
omnigent.fork.source_external_session_id) were dropped in transit. The
init-envelope initializer then caches and shares that label-less envelope
with the first-turn path, so even the label-hydrated get_conversation
result was never used for the envelope. The runner saw no fork labels,
skipped the clone/rebuild branches, and launched the TUI fresh.
This dropped labels for every consumer of the reconnect path, not just
claude-native forks — any label-driven behavior on reconnect (codex / pi
/ qwen fork history, presentation ui/wrapper labels) was equally
affected and is fixed by the same hydration.
Fix: fetch labels via the existing batched _fetch_labels_bulk inside the
same _conv_session and thread them into _to_conversation. One extra
query, no N+1, correct under the split-DB topology (labels live in the
conversation DB).
Co-authored-by: Isaac
`create_conversation` already accepts an optional `conversation_id` (falling back
to `generate_conversation_id()` when omitted). This extends the same capability to
the other two session-creating methods via protected `_..._with_id` seams:
- `create_session_with_agent(...)` -> `_create_session_with_agent_with_id(conversation_id, ...)`
- `fork_conversation(...)` -> `_fork_conversation_with_id(conversation_id, ...)`
The public methods stay unchanged thin wrappers that pass `generate_conversation_id()`,
and the `ConversationStore` ABC is untouched, so this is a behavior-preserving refactor
for all existing callers. It lets a subclass mint the id externally and inject it as the
row id (e.g. a store that keys conversations by an identity-service node id) — which
`create_conversation` already permits but these two methods did not.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* docs(projects): mark the benchmark TODO done (#3094)
The list_projects / list_project_sessions journeys, project corpus seeding, and
the dev/benchmarks PR-benchmark trigger all landed in #3094. Update the PRD
status so the roadmap points at Phase 2 (project defaults) as the next item.
Co-authored-by: Isaac
* feat(projects): add a config column for project-level session defaults (Phase 2)
Phase 2 (P4a) of the projects feature — the backend half. Gives a project a
place to store default session settings (host, workspace, harness, model,
reasoning effort, git base-branch, …) so a new session created in the project
can pre-fill them, replacing the inference-based prefill (#2133) in a follow-up.
- Migration b3c4d5e6f7a8: add a nullable `config` TEXT column to `projects`
(additive; clean downgrade). NULL = no stored defaults.
- The column is an OPAQUE JSON object: the backend persists it whole and never
filters on it, so the key vocabulary is owned by the client (the new-chat
dialog) and can grow without a schema change. Values are hints, not enforced.
- Plumb config through the stack: SqlProject model, Project entity (decoded
dict, empty when unset), ProjectStore.create/update (encode/decode helpers
mirroring session_overrides), and the /v1/projects schemas + routes.
- update() semantics: config=None leaves it unchanged; config={} clears it —
distinct, so a rename never wipes stored defaults.
- Tests: store round-trip + None-vs-{} update semantics, route create/get/patch
round-trip, entity default_factory isolation, migration up/down verified.
- Regenerated openapi.json (config on ProjectObject/Create/Update).
- PRD: mark the backend config column done; the dialog wiring and #2133
retirement remain as follow-up sub-items of Phase 2.
Co-authored-by: Isaac
## Related issue
N/A
## Summary
- Adds `omnidev omnigent <args…>`, which forwards any omnigent command to
`uv run omnigent …` with the current checkout's pod env applied
(`OMNIGENT_DATA_DIR`, `OMNIGENT_DATABASE_URI`, `OMNIGENT_CONFIG_HOME`,
`OMNIGENT_URL`), so a CLI command talks to the same pod the supervisor runs
and coexists with a running supervisor (no lock acquired).
- Resolves the repo root → pod dir (same as the supervisor), ensures the pod
tree, and reads persisted ports so `OMNIGENT_URL` targets a live server. Runs
in the foreground inheriting stdio and exits with omnigent's status code;
omits the supervisor's log-mirror env so omnigent's own TTY detection wins.
- The `omnigent` subcommand is a named gate with `trailing_var_arg` +
`allow_hyphen_values`, so the existing install subcommands
(`install`/`update`/`check`/`refresh`/`shell-hook`) keep their top-level
surface and clap's typo-suggestion guardrail. New `src/omnigent_cmd.rs` holds
the pure `build` + `run` split for testability.
## Test Plan
- `cargo build` and `cargo clippy` clean (no warnings).
- `cargo test` — 60 tests pass (36 unit + 7 install-mgmt + 17 pod-setup),
including 4 new `omnigent_cmd` unit tests: args forwarded after
`uv run omnigent`, empty passthrough, pod-isolation env applied, and
log-mirror env omitted.
- `omnidev --help` shows the flat subcommand surface; `omnidev omnigent …`
outside a checkout fails at repo-root discovery (not at clap); `omnidev
isntall` still suggests `install`.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [x] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Manual verification: confirmed `--help` renders the new `omnigent` subcommand,
the passthrough routes outside a checkout (repo-root error, not a clap error),
and the typo guardrail survives (`omnidev isntall` suggests `install`).
## Changelog
`omnidev omnigent <args…>` runs an omnigent command against the current checkout's pod via `uv run omnigent`, with the pod's isolated env applied
## Related issue
N/A
## Summary
- A bare `omnigent server --host 0.0.0.0` used to stay in header mode and fail-close (401 on every request) with no warning and no path forward, because an end user has no realistic way to inject an identity header. The existing first-admin terminal prompt also never fired, since it no-ops when `account_store is None` (header mode).
- Now a non-loopback bind with no explicit auth config auto-enables accounts (login) mode, mirroring the Docker/Cloudflare/k8s entrypoints. The server boots and serves; first-admin setup happens via the web Create-admin form. A stderr warning is emitted at startup naming the host and the mode change.
- Removed the `_maybe_prompt_first_admin` TUI prompt path entirely — the server should just be a server, and the web Create-admin form (which is fully self-sufficient) is now the only interactive setup route. Explicit operator choices (`OMNIGENT_AUTH_PROVIDER`, `OMNIGENT_AUTH_ENABLED`, deprecated `OMNIGENT_ACCOUNTS_ENABLED`) always win; the loopback default is unchanged.
## Test Plan
- `uv run python -m pytest tests/cli/test_bind_auth_defaults.py -v` — 13 new unit tests covering the loopback/non-loopback/explicit-override matrix (accounts auto-enabled + warning on non-loopback; explicit provider/auth-enabled respected; empty `AUTH_PROVIDER` treated as unset; OIDC resolves downstream).
- `uv run python -m pytest tests/cli/test_server_lifecycle.py tests/cli/test_cli_auth.py tests/server/test_accounts.py -q` — existing tests still pass (131 total).
## 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
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
The new `_apply_bind_auth_defaults` helper is unit-tested directly across all matrix corners; existing server-lifecycle / accounts / CLI-auth suites confirm no regressions.
## Changelog
`omnigent server --host 0.0.0.0` now enables accounts (login) mode automatically instead of silently 401-ing every request
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Stack 1 of 3 for the Scheduled Tasks page (UI-1). Pure lib/hooks, not
rendered yet, so it type-checks standalone.
- scheduledTasksApi.ts: hand-written client for all 6 /v1/scheduled-tasks
endpoints (mirrors sessionsApi.ts).
- useScheduledTasks.ts: React-Query list query (page-scoped 60s poll, with
a guard-rail comment) + create/patch/delete mutations with invalidation.
- scheduleText.ts: client-side RRULE → "Weekdays at 8:00 AM · Next run in Xh".
- scheduleBuilder.ts + timezones.ts: RRULE construction + IANA tz helpers.
- Adds the rrule@^2.8.1 dependency (the only new dep).
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
## Related issue
N/A
## Summary
- Removes the `omni server start` subcommand. `omni server` already starts
the server (in the foreground), so `start` was a redundant way to launch it;
the only thing it added was the detached/background mode.
- Adds a `--background` flag to `omni server` that reproduces the former
`start` behavior: spawn (or reuse) the managed detached local server instead
of running uvicorn in the foreground. `omni server stop` / `omni server
status` are unchanged.
- Updates the desktop app's CLI shell-out, docs, skill files, and tests to
the new invocation.
## Test Plan
- `omni server start` now exits `2` with "No such command 'start'" (verified
via `CliRunner`).
- `omni server --background` routes to `ensure_local_omnigent_server()` and
short-circuits before the foreground port-bind check; prints the URL and
captured log path on spawn, "already running" on reuse, and omits the log
line when `log_path` is unknown (3 renamed tests pass).
- `omni server stop` / `omni server status` behave as before (verified via
CliRunner with stubbed registry).
- `server --help` lists `--background` and only the `stop`/`status`
subcommands; bare `omni server` still reaches the foreground port-bind
check.
- `node --check web/electron/src/omnigent_cli.js` passes; the spawn primitive
in `host/local_server.py` invokes the bare `omnigent.cli server` foreground
command, so it is unaffected by the `start` removal.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [x] Breaking change
## Test coverage
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Renamed the three `test_server_start_*` tests in `tests/cli/test_server_lifecycle.py`
to `test_server_background_*` (invoking `server --background`); updated
comments in `tests/host/test_local_server.py`. Manually verified routing,
help output, and the desktop CLI arg via ad-hoc CliRunner/node checks.
## Changelog
`omni server start` is removed; use `omni server --background` to launch the
detached managed server instead.
* perf(benchmarks): add list_projects + list_project_sessions read journeys
The web sidebar now hammers two project read paths that had no benchmark
coverage: GET /v1/sessions/projects (the project list, a dual-read union of
first-class projects and legacy omni_project label-projects) and
GET /v1/sessions?project= (a project folder's sessions, the dual-read filter
behind clicking a folder).
Add both as latency journeys mirroring the existing list_sessions hot-read
path. Each is a single-request read (1 HTTP/op). list_project_sessions'
setup reads a representative project from the seeded corpus, self-seeding a
first-class project + one filed session when the DB is empty (smoke path) so
the ?project= filter resolves a real member instead of an empty match.
Wire both into the smoke test's curated HTTP-journey list and document them
in the README journey table.
Co-authored-by: Isaac
* perf(benchmarks): seed first-class projects so the project journeys measure real work
The list_projects / list_project_sessions journeys added earlier had no project
data to read: the corpus seeder never filed a session into a project, so against
a real corpus list_projects timed an empty union and list_project_sessions read
a degenerate 1-row folder (self-seeded fallback) — testing nothing about scale.
Seed first-class projects into the corpus and file a configurable fraction of
sessions into them (round-robin), across both write paths:
- new --projects N (default 20) and --filed-fraction F (default 0.5) knobs;
- projects owned by the reserved "local" user the loopback server resolves to,
so the owner-scoped project reads see them;
- membership set on conversation_metadata.project_id (store path via
set_conversation_project, core fast path via the bulk metadata insert);
- deterministic project ids (derived from the index) so both paths produce
byte-identical project rows and a re-seed at the same config is stable;
- project knobs folded into the reuse marker so a pre-existing corpus without
projects is reseeded once.
Now list_projects unions a realistic folder count and list_project_sessions
reads a populated folder (~sessions×fraction/projects members).
Tests: extend the fast-path row-count + byte-stability tests to cover the
projects table and per-folder membership; the smoke seed test asserts projects
are created and filed sessions are listable via the owner-scoped ?project=
filter.
Co-authored-by: Isaac
* ci(benchmarks): run the PR benchmark check when the benchmark harness changes
The PR benchmark regression check only triggered on migration/store changes, so
a change to the benchmark harness itself (journeys, seeder) — like adding the
project read journeys and project seeding — never ran the benchmark it defines.
Add dev/benchmarks/** to the trigger paths so harness changes are exercised
against the nightly baseline on the PR that makes them.
Co-authored-by: Isaac
The Subagents panel list view and graph/tree view kept separate,
duplicated status->color maps that had drifted: the quiet connected
states (launching, idle, done) rendered a blue --session-active dot in
the list but a grey --muted-foreground dot in the graph, so the same
agent showed a blue dot in list and a grey dot in graph.
Extract a single shared subagentStatus module (activity classification +
dot palette) and have both StatusIndicator (list) and NodeStatusDot
(graph) color their dot from it, so a given status renders an identical
dot in both views. The graph keeps its own per-activity border/background
tint, but the dot color is now the shared source of truth.
Also align the graph's activity classification with the list's: the
graph now honors the 'disconnected' state (a runner disconnect renders a
quiet grey dot in both views, not the red 'Failed'), and the root/main
node uses sessionStatus so launching and disconnected are reflected
there too.
Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com>
* feat(projects): polish project-folder header actions
Refine the hover-revealed controls on a project-folder header:
- Swap order so the new-session (pencil) sits left of the "..." kebab,
mirroring how the two buttons read left-to-right.
- Align a session row's quick-pin with the kebab (right-8) so the pin/kebab
pair lines up with the project row's pencil/kebab pair.
- Add a "New session in project" tooltip on the pencil.
- On mobile, hide the pencil (max-md:hidden) and fold the action into the
kebab as a md:hidden "New session" item linking to the same pre-filed
composer.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e): cover project new-session mobile fold
Add a Playwright e2e asserting the folder header's new-session pencil is
hidden below the md breakpoint (max-md:hidden) and the same action is offered
as a md:hidden "New session" kebab item linking to the pre-filed composer.
Satisfies the E2E UI Required gate for the mobile behavior change.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e): scope mobile-fold locators to the test's project
The bare project-new-session / project-actions test-ids match every project
folder on the shared e2e server, so the mobile-fold test hit a strict-mode
violation (2+ pencils) once another test seeded a second folder — passing in
isolation but failing in the CI shard. Scope the pencil and kebab locators by
their per-project accessible names ("New session in <project>", "Project
actions for <project>") so only this test's folder is matched.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* feat(projects): first-class projects in the web sidebar
Wires the web app to the first-class projects entity (#2765/#3053), keeping
the legacy omni_project label path working via dual-read so no migration is
forced. Folders are keyed by name (the union key that merges a first-class
project and a like-named label-project into one folder), carrying the
first-class id when one exists.
Backend
- GET /v1/sessions/projects now dual-reads: unions first-class projects
(project_store.list — incl. empty, with id) and legacy label-projects
(id=None), merged by name and sorted. Response shape list[str] →
list[{id, name}]; still owner-scoped. openapi.json regenerated.
Frontend
- projectsApi.ts: typed /v1/projects CRUD client (list/create/rename/delete).
- Hooks: useProjects → ProjectSummary[] ({id, name}); new useCreateProject,
useRenameProject; reworked useDeleteProject (archive + unfile every member,
then delete the container). Filing/moving files via project_id, resolving
the picked name to an id and creating the first-class row on demand for a
label-only folder; "" unfiles. Conversation.project_id added.
- Sidebar: folders keyed by {id, name}, members matched by project_id OR the
legacy label; always-visible Projects section with a "New project"
(create-empty) control extracted to NewProjectButton.tsx; Rename dialog;
delete threads id; a row's current-project dual-reads project_id→name so a
pinned first-class member keeps its project flyout; "Remove from project"
unfiles silently (a first-class project persists when emptied); empty
folders read "No sessions".
- NewChatDialog: composer files new sessions via project_id.
Tests
- projectsApi unit tests; reworked hook tests (resolve→file, create-on-demand,
archive+unfile+delete); sidebar/composer suites updated; server union test;
e2e_ui docstrings + fixtures updated for the project_id membership flow.
Deferred (kept on the label path via dual-read): the new-session prefill state
machine and the Settings archived-only project picker; retiring label reads is
gated on the Phase 4 backfill.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(projects): rename-dialog Enter, checked promote PATCH, typed projects schema
Addresses the review on #3061:
- Rename-project dialog: wrap the body in a <form> so Enter submits natively
(Radix Dialog doesn't provide one, and the prior manual key handler looked
for the confirm button inside the <input> and never fired).
- useRenameProject label-only promote: check res.ok on each re-file PATCH and
throw on failure, so a 4xx/5xx no longer reports success with members left
unfiled.
- GET /v1/sessions/projects: return a typed SessionProjectSummary list instead
of list[dict] + response_model=None, which produced an empty ("schema": {})
OpenAPI response and broke client generation. openapi.json regenerated.
- Drop the stale test comment describing the removed last-session remove-confirm
gate.
Copilot #2 (recreate missing metadata row) and #4 (...->NotImplementedError in
the abstract method) intentionally declined, consistent with prior rounds.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(projects): keep dual-read membership coherent on move/rename; lift row lookup
Addresses the second web-UI review round on #3061:
- moveConversationToProject now clears the legacy omni_project label in the same
PATCH as it sets project_id. The sidebar groups a folder by project_id OR the
label during the dual-read transition, so a stale label would keep a moved
session in its old label-folder (and match two folders at once). project_id is
the single source of truth after a move.
- useRenameProject reconciles members for BOTH paths (first-class rename and
label-only promote): sweep the folder's members via ?project=<oldName>, re-file
each onto the target project_id, and clear the legacy label — so a first-class
rename no longer strands label-matched members in an oldName folder.
- resolveOrCreateProjectId tolerates the create-on-demand race: a concurrent
move to the same new name can 409 on the second POST; re-list and use the
winner's id instead of failing.
- ConversationRow no longer calls useProjects() per row. A list-level
id->name map is provided via context (ProjectNamesContext), so row renders are
O(1) with no per-row query observer.
Test PATCH-body assertions updated for the added labels field.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(projects): preserve the original error when create-on-demand truly fails
resolveOrCreateProjectId caught the create error to tolerate the 409 race
(a concurrent move created the same name), but a genuine 500/network failure
was indistinguishable and surfaced as a generic "Could not resolve or create"
message. Re-list to disambiguate: if the row now exists a racer won — use it;
otherwise rethrow the ORIGINAL error so the true cause isn't masked.
Addresses a non-blocking note on #3061.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): stub /v1/sessions/projects with the {id,name} shape in prefill test
The project-prefill e2e test stubbed GET /v1/sessions/projects with the old
bare-string body, but this PR changed the endpoint to return
SessionProjectSummary objects. The sidebar parsed no folder, so the project
header never rendered and header.hover() timed out.
Return the dual-read union shape ({id: None, name} for the label-only project
the test seeds), matching the endpoint contract and the sibling sidebar tests.
Co-authored-by: Isaac
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* ✨ feat(claude): Load Databricks models live
- Refresh the gateway catalog once per new native session and share the launch snapshot with the UI.
- Keep provider-neutral aliases, cached fallback behavior, and authoritative model removals.
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
* 🐛 fix(claude): Handle delayed model catalogs
- Retry sticky model handoff after live options arrive, including bind races
- Map provider model ids and defaults to friendly active picker rows
- Tighten model option contracts and cover backend/UI edge cases
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
* fix(api): regenerate OpenAPI schema
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
* 🐛 fix(claude): Mirror managed model catalog
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
* 🐛 fix(ui): Resolve launch models from host
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
* test: fix model discovery CI coverage
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
* test: stub host model discovery in e2e
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
* fix(claude): preserve live catalog routing
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
* fix(claude): don't treat a failed-primary empty catalog as authoritative
Addresses the outstanding review round:
- discover_databricks_claude_models: when the UC listing fails and the
legacy gateway answers with no Claude routes, re-raise the primary
error instead of returning {} — callers now fall back to cached ucode
models rather than hard-failing the launch on a transient UC outage.
- Warn when model-services pagination is truncated at the page budget.
- Runner claude-model-options: answer ClickException config failures
with 424 instead of the retryable 503, so the picker path stops
conflating "no models configured" with "still booting".
- chatStore bind race: a preserved raced-catalog selection must still
exist in that catalog — a removed sticky alias no longer lingers
visually selected.
- Document that the pre-launch host catalog is an ambient-default
preview; launch re-resolves with the session's agent spec.
Co-authored-by: Isaac
* test(e2e): pick the live catalog label in the model/effort scenario
The config modal's Model rows now carry the host catalog's display
names ("Opus 4.8"), not the static alias labels, so the exact-match
click must use the mocked catalog's label.
Co-authored-by: Isaac
* chore: revert accidental uv.lock churn from the merge
Co-authored-by: Isaac
* fix(api): sync openapi.json with the host model-options docstring
Co-authored-by: Isaac
* fix(api): tolerate provider model rows without displayName
Polly review: the shared NativeModelOption schema made displayName
required and _model_options_from_wire validated all-or-nothing, so one
Codex model/list or OpenCode /api/model row lacking displayName blanked
the whole picker for the session. Restore displayName as optional (the
UI already falls back to the id) and skip malformed rows individually
instead of discarding the catalog.
Co-authored-by: Isaac
---------
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
The Configure <agent> modal's Cancel/Save footer used the shared
DialogFooter's muted tray background and top divider, which read as a
distinct gray band. Override it to blend into the modal body so the
footer matches the rest of the surface.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Fold ix_scheduled_tasks_created_at and ix_scheduled_tasks_user_id into a
single ix_scheduled_tasks_user_scope (workspace_id, user_id, created_at, id).
The per-user GET /scheduled-tasks listing (store.list(owner_user_id=...):
WHERE workspace_id AND user_id ORDER BY created_at, id) becomes an ordered
index seek with no filesort, instead of a user_id seek that must sort or a
created_at scan of every owner's rows.
The scheduler-boot read (list_active_all_workspaces) uses neither index for
its state filter and its ordering only feeds independent per-task timer
arming, so dropping the created_at-ordered scan costs nothing.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* feat(web): add ⌘⌥V hotkey to toggle voice dictation
Add a WhisperFlow-style global hotkey (⌘⌥V / Ctrl+Alt+V) that toggles the
composer's voice dictation from anywhere in the app — the same action as
clicking the mic button.
- New useVoiceDictationHotkey hook, mirroring useCommandPaletteHotkey: a
global keydown listener that bails inside terminals / the Monaco editor,
ignores auto-repeat, and matches on the physical KeyV code (⌥ rewrites the
character on macOS). Uses the browser-safe ⌘⌥ chord shared by the
sidebar-toggle and pinned-session hotkeys — plain ⌘M minimizes the window
on macOS and most ⌘⇧-letter combos are browser shortcuts.
- ComposerMicButton gains an opt-in enableHotkey prop plus onVoiceStart /
onVoiceDiscard callbacks. While listening, Enter commits (stop, keep the
text) and Esc cancels (stop, revert to the pre-dictation snapshot); a
discard guard drops a trailing transcript that races in after Esc.
- Wire the hotkey + snapshot/restore into both composers (ChatPage and the
New Chat landing screen); the two never mount at once, so the chord never
double-fires.
- Document the shortcut in the keyboard-shortcuts dialog.
Co-authored-by: Isaac
Signed-off-by: kerryspchang <kerryspchang@users.noreply.github.com>
* fix(web): skip the doomed Web Speech take in Electron dictation
In Electron the SpeechRecognition constructor exists but has no backend, so
the first take always fails with a "network" error and only then falls back
to the server path — a visible ~1s "fail then recover" on every take. Real
browsers don't hit this because Web Speech genuinely works there.
When the server advertises dictation and we're in the Electron shell, go
straight to the server path and skip the Web Speech attempt entirely. The
existing "network" fallback stays as a safety net for other environments.
Co-authored-by: Isaac
Signed-off-by: kerryspchang <kerryspchang@users.noreply.github.com>
* test(e2e): cover the voice-dictation hotkey and Enter/Esc commit/discard
The E2E UI gate flagged the new keyboard-driven dictation behavior as
user-facing and unit-tested only. Extend the existing server-dictation
Playwright test with three cases driving a real browser + live server +
fake engine:
- the ⌘⌥V / Ctrl+Alt+V hotkey starts and stops a take (window keydown
path, matched on the physical KeyV code — not the mic button onClick),
- Enter while listening ends the take and keeps the dictated text (and,
via the capture-phase handler, does not send the draft),
- Esc while listening ends the take and reverts to the pre-dictation text.
Extract the server-mode page setup (mic permission grant + stripping the
SpeechRecognition constructors) into a shared helper the four tests share.
Co-authored-by: Isaac
Signed-off-by: kerryspchang <kerryspchang@users.noreply.github.com>
---------
Signed-off-by: kerryspchang <kerryspchang@users.noreply.github.com>
Co-authored-by: kerryspchang <kerryspchang@users.noreply.github.com>
* fix(egress): don't inject credentials on TRACE/OPTIONS + honor Max-Forwards
TRACE is a loopback diagnostic whose final recipient reflects the request
back to the caller, so the credential proxy attaching a bound-host secret
on TRACE would echo it straight back into the sandbox. Refuse credential
injection/swap on TRACE and OPTIONS regardless of the allowlist.
Also make the proxy a conformant intermediary for Max-Forwards
(RFC 7231 §5.1.2): answer TRACE/OPTIONS as the final recipient when the
hop budget reaches 0 (never forwarding into the injection path), and
decrement a positive budget before forwarding.
Signed-off-by: mxatone <6202935+mxatone@users.noreply.github.com>
* refactor(egress): address Polly review notes on Max-Forwards handling
Non-blocking follow-ups from the automated review:
- Normalize the method with .upper() inside _apply_max_forwards so the
guard holds even if a future caller forgets to upper-case the verb.
- Document that the OPTIONS Allow list is intentionally static and
proxy-scoped (the proxy's own final-recipient capabilities, not the
origin's).
- Note that a request body on the terminate path is intentionally left
undrained since the reply is Connection: close.
Signed-off-by: mxatone <6202935+mxatone@users.noreply.github.com>
---------
Signed-off-by: mxatone <6202935+mxatone@users.noreply.github.com>
* feat(web): set up a missing harness from the New Chat dialog
Turn the dead-end "binary missing" / "needs auth" warning in the New
Chat harness picker into a working setup flow, gated behind the
server's harness_install_enabled capability (flag off → the picker is
byte-for-byte the pre-feature UI).
- A "Set up →" affordance on an unready harness opens HarnessSetupDialog,
a server-driven checklist that reflects the harness's real setup steps
and per-step status from /v1/harnesses and /v1/info.
- One-click install drives POST /v1/hosts/{id}/harnesses/{harness}/install,
scoped per-harness so concurrent installs of different harnesses track
independently; the dialog reads live host readiness so the badge flips
without a reconnect.
- Steps we can't yet detect (API-key / gateway auth) point at
`omnigent setup` rather than showing an untrackable checkbox.
Frontend-only; the backend for this flow landed in #2912.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(web): address review on the harness setup dialog
- Wire the harnessInstallableOnHost guard into the Install button so the
UI never offers a one-click install the server's allowlist would
reject (defence in depth against catalog/allowlist drift); it was
exported and tested but never called. Fix the stale
canInstallHarnessFromUI doc reference.
- Key the post-install toast on the refreshed readiness the install
returns: "ready" only when the harness is actually launchable,
otherwise "installed — one more step" so it can't contradict a
still-showing sign-in row (e.g. Codex).
- Add a fallback message when the server published no setup steps for a
spelling, instead of an empty dead-end dialog.
Adds tests for the guard, both toast wordings, and the empty-steps
fallback.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
---------
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(onboarding): judge install success with the readiness resolver
try_install_harness_cli judged install success with a bare
shutil.which(spec.binary), but readiness (harness_cli_installed) uses
resolve_cli_binary — the full ladder that also probes the
nvm/npm-global/homebrew bin dirs the host daemon's frozen PATH omits.
On a host whose npm prefix is off PATH, npm lands the binary in a
fallback dir: the install verdict returned "not on PATH" (→ 502 → red
"failed" toast) while readiness resolved it via the ladder (→ green
"ready" tick). One install, two contradicting verdicts, surfaced by the
UI setup dialog.
Judge success with the same resolve_cli_binary the readiness badge uses
so the two can't disagree, while keeping the ~/.local/bin PATH-prepend
the setup wizard's later harness_login relies on. Adds a regression test
pinning that an off-PATH-but-on-ladder binary reads installed from both
try_install_harness_cli and harness_cli_installed.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* docs(onboarding): clarify HarnessInstallResult resolves off PATH too
Polly review nit: after unifying the install verdict on resolve_cli_binary,
the "on PATH after the attempt" phrasing on HarnessInstallResult.installed
and in try_install_harness_cli's docstring was stale — success can now also
come from a binary resolved via the fallback ladder (off bare PATH). Reword
both to say "resolves via resolve_cli_binary". No behavior change.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(onboarding): put the resolved install dir on PATH for later login
Polly review follow-up on the install-verdict fix: judging install
success via resolve_cli_binary's full ladder fixed install-vs-readiness,
but the wizard's *later* steps (harness_login / harness_cli_logged_in /
harness_logout) still shell out with the bare binary name and only bare
shutil.which. The prior remediation only prepended ~/.local/bin, so an
install that succeeded via a different fallback dir (nvm / npm-global /
homebrew) could be followed by a login step that couldn't locate the
binary just installed.
Prepend the dir the binary actually resolved from (Path(resolved).parent)
to PATH, so install, readiness, and login all converge on the same
binary. Adds a test pinning that a bare shutil.which (what login uses)
finds the CLI after an off-PATH install, and updates the ~/.local/bin
refresh test for the resolver-based mechanism.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
---------
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(runner): quiet idle-reaper shutdown instead of a scary error banner
When the runner idle monitor reaps an inactive runner after
`runner.idle_timeout_s` (default 1h), the runner exits cleanly (code 0),
but the UI rendered the same loud red `ErrorBanner` a genuine crash would
— even though the session is fully reactivatable (host-bound sessions
relaunch the runner on the next message). A clean idle shutdown tripped
two banner-producing server paths:
1. Relay path (durable / reload banner): the runner's `GET /stream`
dropped abruptly, so the SSE relay published `failed` +
`runner_disconnected` and persisted it as a `last_task_error` label.
2. Host exit-report path (live): the host's `_watch_runner` reported
`host.runner_exited`, which became `failed` + `runner_failed_to_start`.
This treats a clean idle exit as benign (a genuine crash still shows the
banner):
- Runner drains its session streams before the idle shutdown: enqueues the
`[DONE]` sentinel to each `GET /stream` so the relay returns cleanly
(no `runner_disconnected`, no durable label). `serve_tunnel` now takes a
`shutdown_event` + `on_graceful_shutdown` hook; on signal it waits for
in-flight dispatch tasks to emit their end frames, then closes the socket
with a normal close handshake (the handshake completing is the delivery
confirmation — robust over a remote connection, not a timing nudge), and
stops reconnecting.
- Host suppresses the exit report for a clean (code-0) exit; a non-zero
exit still reports its cause.
Co-authored-by: Isaac
* refactor(runner): address PR review nits on graceful-shutdown loop
- Use asyncio.create_task instead of ensure_future in the graceful-shutdown
read loop, matching the module convention (Copilot).
- Make the graceful-shutdown serve test deterministic: pre-arm the shutdown
event so the first recv() race resolves to it, dropping the real-time
sleep(0.01) that could flake under load (Copilot).
- Give the flagged bare `await task` an explicit effect via
`assert task.result() is None` (CodeQL "statement has no effect").
Co-authored-by: Isaac
* docs(runner): note the same-tick frame drop in graceful shutdown
Polly/Copilot review flagged that if a frame and the shutdown signal
complete in the same asyncio.wait tick, the shutdown branch wins and the
frame is dropped. That is acceptable on the idle-reaper teardown path (a
host-bound session replays/relaunches on the next message); document it so
the trade-off is explicit for future readers.
Co-authored-by: Isaac
* refactor(runner): snapshot drain queues; create_task in tests
Follow-up PR review nits (Copilot):
- `_drain_session_streams` now iterates `list(_session_event_queues.values())`.
The loop is synchronous (no await, so nothing interleaves on the event loop
today), but snapshotting keeps the drain robust if a queue mutation ever
moves off this atomic path — matching the `list(...)` idiom already used by
the timer-cleanup / pane-reaper paths.
- Switched the two remaining `asyncio.ensure_future(...)` test helpers to
`asyncio.create_task(...)` for consistency with the module convention.
Co-authored-by: Isaac
* fix(runner): log recv failure while settling cancelled read on shutdown
PR review (Copilot): the graceful-shutdown branch swallowed
WebSocketException while awaiting the cancelled recv_task. If recv() had
already failed with an abnormal close on the same tick the shutdown fired,
the socket may be dead — so the drain's [DONE] frames won't reach the
server and it will see a disconnect — yet there was no trace of why.
Keep suppressing the exception (letting it propagate would skip
_graceful_drain and reintroduce the abrupt drop this PR removes), but split
the handling: silent on CancelledError (normal cancellation), debug-log on
WebSocketException so the rare same-tick failure is diagnosable without
disturbing the quiet UX.
Co-authored-by: Isaac
* perf(scheduled-tasks): fix unbounded queries in scheduled-task store
Three unbounded DB reads could cause excessive load as the task table grows:
- Issue #5: `list()` fetched all workspace tasks then filtered in Python.
Add `owner_user_id` parameter to `list()` (ABC + SQLAlchemy) so the
WHERE clause uses the existing `ix_scheduled_tasks_owner_user_id` index.
Update the route to pass `owner_id` directly instead of post-filtering.
- Issue #6: `list_runs()` returned every historical run for a task with no
LIMIT. Add a `limit: int = 100` keyword parameter (ABC + SQLAlchemy) and
apply `.limit(limit)` to the query.
- Issue #10: `list_active_all_workspaces()` had no cap on rows returned at
scheduler boot. Apply a hard `.limit(10_000)` to prevent unbounded load.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(scheduled-tasks): paginate list_runs and arm all tasks at boot instead of silent caps
Problem A: GET /scheduled-tasks/{id}/runs silently truncated run history at
100 rows with no pagination. Replace the bare limit with cursor pagination:
list_runs now returns (runs, next_cursor) and takes after_id; the endpoint
accepts limit (1-1000) and after, and returns {runs, next_cursor}. Run ids are
random UUIDs, so the keyset resolves the cursor row's scheduled_at and compares
the full (scheduled_at, id) tuple under the DESC order — an id-only cursor
would skip/repeat rows on scheduled_at ties.
Problem B: scheduler boot (list_active_all_workspaces) capped at 10k rows, so
tasks beyond the cap silently never armed. Chose the complete-pagination
approach over a loud-warning cap: the method now keyset-pages internally by
(workspace_id, created_at, id) in 10k batches and returns ALL active tasks, so
every task is armed at boot. Full pagination is strictly correct (no task ever
left un-armed) and the boot scan is a rare, one-shot cost.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(permission-store): add query limits and reduce session opens
Unbounded queries on list_for_user, list_for_session, and list_users
could fetch unlimited rows from the DB. Add limit: int = 1000 to each
with .limit(limit) applied to the query; update the abstract base class
to match.
check_access opened 2 separate sessions for 2 PK lookups.
get_permission_level opened 3 sessions (is_admin + 2 get calls).
Consolidate each into a single `with self._session()` block following
the same pattern used by resolve_access.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* revert(permission-store): restore separate sessions in check_access and get_permission_level
The consolidation of check_access and get_permission_level into single
sessions changed the timing characteristics of permission reads. Under
xdist parallel test execution the CI integration suite (Integration
openai-agents) saw test_share_and_second_user_continues fail: a
concurrent reset from another worker cleared the mock LLM queue between
configure_mock_llm and the owner's first turn, causing the second turn to
receive no LLM response.
Revert check_access and get_permission_level to their original
multi-session implementations to restore the original execution timing.
The resolve_access consolidation (used by the hot GET /v1/sessions path)
is retained as it was already present on main and is not implicated in
the failure.
Issue #15 (reducing session opens in check_access/get_permission_level)
remains open and can be addressed with a more targeted fix that also
addresses test isolation.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(permissions): add cursor pagination to GET /sessions/{id}/permissions
list_for_session now returns (grants, next_cursor) with user_id-ordered
keyset pagination. The API endpoint accepts limit (1–1000, default 100)
and after (cursor = user_id) query params and returns
{"permissions": [...], "next_cursor": str|null}.
GET /users gains a limit query param (1–1000, default 100) wired through
to list_users(). list_for_user keeps its silent 1000-row cap (internal
only).
All callers of list_for_session updated to unpack the tuple.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test(permissions): cover cursor pagination and dict response shape
Add a store-level pagination test and update the session permissions
integration tests to unwrap the new {permissions, next_cursor} response
shape. Fix list_for_session cursor to return the last returned user_id
so the exclusive user_id > after_user_id filter does not skip a row.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test(permissions): update e2e/server tests for paginated permissions response
GET /v1/sessions/{id}/permissions now returns {permissions, next_cursor}
instead of a bare list. Update the e2e sharing test and the e2e_ui
permissions-modal helper to read the permissions array.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): parse paginated permissions response in listPermissions
GET /v1/sessions/{id}/permissions now returns {permissions, next_cursor}
instead of a bare array. listPermissions follows the cursor and
concatenates all pages, returning Permission[] so callers
(isSessionSharedWithOthers, AgentInfo, usePermissions) are unaffected.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(web): move host-offline reconnect prompt into the composer host badge
When a session's host went offline, the "Host is offline — click to
reconnect" affordance rendered as a banner below the composer, separate
from where the host is already named. Fold it into the composer's host
badge: when a session is `host_offline`, the badge becomes a clickable
red "Host is offline — click to reconnect" control in place of the
passive host name + status dot.
ConnectionIndicator now suppresses its banner for `host_offline` whenever
the composer (and its badge) is on screen — i.e. everywhere except the
terminal-first *terminal* view, where the PTY owns the surface and the
banner still carries the affordance. `local_stranded` keeps the banner
everywhere (no host, so no badge to host it).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): keep host-offline banner for sub-agent sessions
A sub-agent session's composer hides the host badge (the header's child
slot owns that row), so the badge can't carry the host-offline reconnect
affordance. The banner suppression keyed only on the terminal view, so a
non-terminal-first sub-agent `host_offline` session lost the affordance
entirely. Thread `isSubAgentSession` into ConnectionIndicator and only
suppress the banner when the badge will actually render it.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): give sub-agent sessions the same host-offline reconnect path
The previous fix special-cased sub-agents by keeping the banner for them.
Instead, treat them like normal sessions: the composer's host badge carries
the reconnect affordance for a host_offline sub-agent too (only the passive
name badge stays hidden for a child). ConnectionIndicator goes back to
uniform suppression whenever the composer is on screen.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* refactor(web): drop unreachable sub-agent host_offline handling
Sub-agent sessions are never host-bound — sys_session_send creates the
child with host_id null and the server inherits only runner_id, so a
stranded child is always local_stranded, never host_offline. The badge's
reconnect affordance therefore never needs to render for a sub-agent;
gate showReconnect back on showHost and drop the dead sub-agent test.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* feat(auto-harness): use live runner catalog to filter available harnesses
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: restore Auto harness option and routing icon after merge
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: remove leftover comment placeholder
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: restore auto-harness session create intercept and first-message resolution after merge
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix: restore route_session_harness lost in merge
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(auto-harness): always clear 'auto' sentinel after first-message resolution
Add _unset_harness_override to update_conversation so the 'auto' sentinel
is cleared even when routing returns harness=None (unavailable/failed).
Without this, the resolution block re-ran on every turn and emitted
a routing card each time.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
test_first_message_schedules_background_semantic_title wrote its own seed
title via store.update_conversation after posting the first user turn. The
events endpoint already seeds the title synchronously before returning, so
that manual write raced the background coordinator's rename and clobbered it
when it landed late — the source of the flaky
"assert 'please investigate...' == 'Debug authentication timeout'" failure.
Drop the redundant manual seed (and the now-unused db_uri fixture) so the
test relies on the endpoint's seed, matching the passing sibling tests.
Co-authored-by: Isaac
- Route accumulated conversations to the latest matching turn queue
- Keep native mock credentials active and refresh the Claude mock model
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
On Windows, `omnigent setup` could crash as soon as it reached the interactive
harness picker because the TTY menu path imported the POSIX-only termios/tty
modules. The user-visible failure was `ModuleNotFoundError: No module named
'termios'`, after the setup banner and preflight warning had already printed.
Route Windows setup menus through the existing numbered fallback instead of the
raw termios path, including the legacy wizard helpers and their back-navigation
behavior. Also remove the remaining POSIX os.getuid() assumptions from native
bridge temp-root setup so Windows installs do not fail while importing those
bridge modules.
Tested with the focused Windows startup regressions:
python -m pytest tests/onboarding/test_interactive.py tests/onboarding/test_wizard.py tests/test_claude_native_bridge.py::test_ensure_secure_dir_succeeds_without_getuid tests/test_qwen_native_bridge.py -q -k "not rejects_symlinked_ancestor"
Signed-off-by: scwf <wangfei_hello@126.com>
* 🐛 fix(history): Hide Claude task notifications
- Mark Claude task notification transcript rows as meta context
- Hide legacy task-notification rows during history hydration
* 🐛 fix(history): Handle monitor task notifications
---------
Co-authored-by: Anthony Ivan <anthony.ivan@example.com>
Co-authored-by: Serena Ruan <82044803+serena-ruan@users.noreply.github.com>
* feat(slash-menu): substring-match slash commands by name
The slash-command suggestion menu matched a query as a prefix of the
full, namespaced command name, so typing `/using-superpowers` surfaced
nothing — the name starts with `superpowers:`. Match the query as a
case-insensitive substring of the command name instead, so
`/using-superpowers` surfaces `/superpowers:using-superpowers`.
A single shared helper `slashCommandMatches(name, query)` in
SlashCommandMenu.tsx backs all three web filter sites (the menu render
filter, ChatPage `menuMatches`, and NewChatDialog `slashMenuMatches`) so
the visible list and the keyboard-nav index can't drift apart. The
omnigent REPL completer (`_SlashCommandCompleter`) mirrors the same rule
in Python so the CLI and web UI behave alike; parallel unit tests keep
the two implementations from diverging.
Matching is name-only, not description: the web menu never shows
descriptions inline, so a description-driven match would look
unexplained. Insertion order is preserved (no relevance ranking) to keep
the menu's Commands/Skills section split contiguous, and submit routing
is unchanged — menu completion still fills the canonical name first.
Co-authored-by: Isaac
Signed-off-by: Colin Reynolds <colin.reynolds@databricks.com>
* style(slash-menu): prettier-format merged import lines
Rewrap the import statements combined during the ap-web -> web rebase so
they satisfy `prettier --check` (they exceeded the print width). No
behavior change.
Co-authored-by: Isaac
Signed-off-by: Colin Reynolds <colin.reynolds@databricks.com>
* style(repl-test): drop explicit `return None` from _noop_handler
Ruff (RET501) flags an explicit `return None` in a `-> None` function.
The bare `return` is equivalent; keeps `pre-commit run --all-files`
green. No behavior change.
Co-authored-by: Isaac
Signed-off-by: Colin Reynolds <colin.reynolds@databricks.com>
* test(e2e-ui): cover slash-command substring matching in both composers
Adds the Playwright coverage the e2e_ui gate requires for this
user-facing change. Two tests drive the new substring behavior in a real
browser against a spawned server:
- In-session composer: `/ontext` (mid-name substring of `/context`,
prefix of nothing) surfaces the row AND highlights it — proving the
render filter and `menuMatches` keyboard-nav filter substring-match in
lockstep.
- New-chat landing composer: a stubbed non-native agent bundling a
`code-review` skill; `/review` surfaces the row and Tab completes it to
`/code-review ` — covering keyboard completion.
Co-authored-by: Isaac
Signed-off-by: Colin Reynolds <colin.reynolds@databricks.com>
* fix(slash-menu): rank prefix matches ahead of mid-string matches
Substring matching combined with auto-highlight (setMenuIndex(0)) and
immediate execution of no-arg built-ins let a short query execute the
wrong command. Built-ins are ordered /compact, /context, /effort,
/model, /help, so typing `/e` highlighted `/context` first (it contains
"e") and Enter/Tab ran it immediately instead of filling `/effort `;
`/m` similarly hit `/compact` ahead of `/model`. The REPL completer had
the same ordering.
Rank matches for display: built-ins before skills (so the Commands
section stays above Skills and the flat keyboard index walks the same
order that's rendered), and within each group prefix matches before
mid-string matches. The sort is stable, so ties keep insertion order and
an empty query (lone `/`) still lists everything unchanged.
A new shared helper `rankedSlashCommandNames` backs all three web filter
sites (menu render, ChatPage `menuMatches`, NewChatDialog
`slashMenuMatches`) so the visible order and keyboard index stay aligned;
the REPL completer mirrors the rule (prefix tier before substring tier,
insertion order within each). Tests pin the ordering on both sides,
including a real-registry REPL assertion.
Co-authored-by: Isaac
Signed-off-by: Colin Reynolds <colin.reynolds@databricks.com>
---------
Signed-off-by: Colin Reynolds <colin.reynolds@databricks.com>
* feat(web): move new-session harness config into a gear-icon modal
The new-session composer's agent picker did double duty — selecting the
agent/harness AND exposing every run-config knob (model, effort, permission
mode, Codex approval + dangerous bypass, Cursor exec mode, bundle brain
harness) via desktop hover-flyout submenus and a bespoke mobile drill-in.
This overloaded one control and made the submenu machinery complex.
Split the concerns: the picker dropdown now only selects the agent, and a
gear icon beside it opens a "Configure {agent}" modal that adapts to the
selected agent's capabilities. The modal edits a local draft and commits on
Save (Cancel discards).
Also in this pass:
- Picker dropdown groups: "needs setup" harnesses fold into a "More" flyout;
custom (user-registered) agents fold into a "Custom agents" flyout. On
touch, both drill in-place with a Back row instead of hover flyouts.
- Gear tooltip summarizes the current settings on hover.
- Config Selects anchor below the trigger, pinned to trigger width; option
descriptions (permission/approval/cursor) show in a footer that tracks the
hovered row.
- Codex bypass toggle simplified to a plain switch (no typed-phrase gate),
still behind Save with the danger banners.
- Smart routing folds into the Model dropdown as a "Smart Routing" option
(when the server enables it and the harness is routable); picking it
freezes Effort to Default. Removes the standalone composer toggle here
(unchanged in the in-session composer).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): regenerate visual baselines
* fix(web): surface Smart Routing for all routable agents; address review
Polly AI review flagged that Smart Routing lived only in Claude's Model
dropdown while _ROUTABLE_HARNESSES still advertised Codex/Pi/bundle agents —
a silent UI regression (server still routes them). Fixes:
- Add a standalone "Smart Routing" toggle row in the gear modal for routable
agents that have no Model dropdown to fold it into (Codex, bundle agents).
Claude keeps offering it as a Model option.
- Commit costControlMode in save() for every eligible agent, not just the
Claude branch.
- Reset costControlMode on agent change (alongside the bypass reset), so an
armed routing can't carry to an agent whose modal can't clear it.
- Picking "Default" in the Model dropdown while routing was on now defers
(null → omitted) instead of emitting an explicit "off".
- Refresh the stale reset-effect comment (the typed bypass phrase is gone).
Adds tests for the Codex standalone toggle, its create-flow wiring, and the
reset-on-agent-change behavior.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): make gear tooltip consistent with the modal for effort/routing
Address Copilot review (PR #3050): the tooltip's Effort summary showed the
"—" sentinel while the modal's unset option is "Default", and it didn't
reflect Smart Routing (which freezes effort) for non-Claude agents.
- Effort now reads "Default" when unset or when Smart Routing is on,
mirroring the modal.
- Non-Claude routable agents show a "Smart Routing: On" tooltip row when
armed (Claude folds it into the Model row).
Adds tooltip tests for the Default-effort label and the Smart Routing case.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): keep the gear visible for routing-eligible agents
Address Copilot review (PR #3050): the gear was hidden when the selected
agent had no permission/approval/cursor knob and wasn't a brain-harness
agent — which would also hide Smart Routing, since it lives only in the gear
modal now. Fold smartRoutingEligible into selectedAgentHasKnobs so any
routing-eligible agent keeps its gear.
In practice every routable selectable agent already has another knob (Claude
permission, Codex approval, bundle Agent Harness), so this is defensive —
but it makes the visibility gate provably correct rather than reliant on that
overlap. Adds tests for the bundle-agent routing+harness case and the
knob-less non-routable case (gear hidden).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): gate Smart Routing UI on eligibility to avoid stale-on states
Address Copilot review (PR #3050): a stale costControlMode="on" combined with
smartRoutingEligible=false (server later disabled the flag, or a non-routable
agent) could (a) leave the Model Select on the __smart__ sentinel with no
matching item, and (b) show misleading "Smart Routing" rows in the gear
tooltip. Gate both smartRoutingOn (modal) and routingOn (tooltip) on
smartRoutingEligible so the UI only reflects routing when it's actually
offered for the current agent.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e-ui): update picker interactions for the grouped/gear-modal picker
The gear-modal refactor moved custom agents into a "Custom agents" submenu,
needs-setup harnesses into a "More" submenu, and the bundle brain-harness
picker into the config modal's Agent Harness select. Update the e2e drivers
that still assumed the old flat picker:
- test_create_custom_agent: reach "Create custom agent" via the Custom agents
submenu; on a sandbox the whole group is omitted (assert both absent).
- test_hide_unconfigured_harnesses: Goose (unconfigured) now folds into "More"
when the toggle is off — drill in to find it.
- test_agent_picker_version: the custom upload lives in the Custom agents
submenu; the built-in stays inline.
- test_codex_auth_availability: the bundle harness badge is in the config
modal's Agent Harness select now (open gear → open select).
- test_start_session (fork-of-fork dedup): top level is now Claude + the
Custom agents submenu trigger (2 menuitems); the custom agent survives
inside the submenu.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): make the new-session picker and config modal mobile-friendly
- Agent picker dropdown ran off the top of short mobile viewports (clipped
under the status bar). Add collisionPadding so Radix's available-height cap
leaves a safe margin and the menu flips/scrolls instead of overflowing.
- Config modal rows squeezed the label into a narrow column beside a fixed
w-52 control, forcing heavy wrapping on mobile. Stack label-over-control
full-width on mobile; keep the side-by-side layout from sm+.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): badge unconfigured brain harnesses in the config modal; fix e2e
Two follow-ups from the E2E run:
- The config modal's Agent Harness select showed a plain "(needs setup)" text
for unconfigured harnesses, dropping the reason-specific badge (and its
new-chat-landing-harness-warning-<id> testid) the old picker had. Restore the
amber badge with the reason text ("needs auth", etc.) so bundle agents like
Polly surface Codex auth state again.
- test_create_custom_agent sandbox check: the "Custom agents" submenu can
legitimately render on a sandbox when a session-scan surfaces a discovered
custom agent; only the create action is gated. Assert just that "Create
custom agent" is absent, not the whole submenu.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): fold Codex bypass into Approval dropdown; a11y + review fixes
UI/UX:
- Codex "Bypass approvals & sandbox" is now the most-permissive option in the
Approval dropdown (it's conceptually an approval stance) instead of a
separate toggle. The persistent danger banner stays when it's selected.
- Smart Routing toggle for non-Claude routable agents moves to the FIRST row
and right-aligns the switch.
Accessibility (Copilot review): the config-modal Select triggers had no
accessible name (the ConfigRow label is visual-only). Add aria-label to the
Model / Effort / Agent Harness triggers and an ariaLabel prop on
DescribedSelect (Permissions / Approval / Mode).
Logic (Copilot review):
- The effectiveAgentId reset effect (bypass + smart routing) now fires only on
an actual agent change, not initial resolution — so a costControlMode/bypass
restored from the landing draft isn't wiped on mount.
- Picking Model "Default" always defers routing to the spec default (null),
never emitting an explicit "off".
Tests: unit + e2e updated for the folded bypass option and the codex
needs-auth badge (now in the Agent Harness select; .first for Radix's
trigger mirror).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): surface "Create custom agent" when no custom agents exist
On a fresh non-sandbox host with no custom agents, "Create custom agent"
was buried inside a lazily-mounted "Custom agents" submenu — non-obvious,
and it left the sandbox-gating e2e assertion vacuous (the item was never
in the DOM after opening the top-level dropdown regardless of target).
Only fold into the "Custom agents" submenu once custom/pending agents
exist; otherwise surface the create action as a top-level picker row.
This restores discoverability on a fresh server and makes the sandbox
`to_have_count(0)` assertion meaningful.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): compute Smart Routing eligibility from the effective harness
A bundle agent (Polly/Debby) on a routable brain harness shows both the
Smart Routing toggle and the Agent Harness override in the config modal.
Arming routing and then overriding to a non-routable harness (e.g. Cursor)
left eligibility computed from the spec harness, so Save still committed
cost_control_mode_override and the create sent routing "on" for a harness
that can't route — with no visible control to clear it.
Compute eligibility from the effective harness (brain-harness override wins
over the spec harness), and gate cost_control_mode_override on eligibility
at create time as a safety net (also covers a stale "on" left after the
server flag flips off). Add a test for the override -> ineligible path.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e): neutralize agent discovery in create-custom-agent tests
With "Create custom agent" now a top-level picker row only when no custom
agents exist, these tests began failing on the shared e2e_ui server:
sessions left behind by other tests leaked in via the kind=any discovery
scan as discovered custom agents, flipping on the "Custom agents" group and
folding the create action back into a submenu — so the top-level create row
the helper clicks was absent.
Stub the kind=any scan to return no agents (same approach as
test_codex_auth_availability.py) so only the stubbed Claude agent feeds the
picker and the create row renders deterministically at the top level.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): show armed Codex bypass as the Approval value in the gear tooltip
Bypass is now an Approval dropdown option, and the modal's Approval trigger
shows "Bypass approvals & sandbox" when armed. The gear tooltip still split
it into `Approval: <preset>` (often "Default") plus a separate `Bypass: On`
row, implying approvals were still at the preset. Mirror the modal: when
bypass is armed the single Approval row reads "Bypass approvals & sandbox".
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>
* feat(benchmarks): add simulated network delay + per-journey request counts
The benchmark harness runs everything over loopback, so it can't tell a
chatty journey (many round-trips) from a lean one on wall-clock alone, nor
model what those round-trips cost over a real network. Two related knobs
close that gap.
- --network-delay-ms (default 0) injects an httpx request-hook sleep before
every client->server request, modelling a real network hop. benchmark.yml
gains a network_delay_ms dispatch input (0 on the nightly schedule for
stable trend data).
- Every run now reports http_requests / http_requests_per_op: the server-side
HTTP request count over the timed region (schema v4->5). For runner journeys
this captures the cross-process runner->server / host->server traffic a
client hook can't see; for HTTP journeys it's known by construction.
The counter is the server's existing ServerPerformanceMetrics.total_started,
which lives in the server subprocess and is only pushed to OTel. A CI-only
router (dev/benchmarks/omnigent/debug_router.py) exposes it at
GET /debug/server-metrics. It never ships in production: it lives under dev/
(excluded from the wheel), is mounted only via the new debug_router_modules
config key (mirroring the policy_modules load-by-dotted-path seam) that prod
config never sets, and a failed import is logged-and-skipped.
compare.py surfaces a Req/op column so an added/removed round-trip shows up
in the PR comparison. README documents both features and their v1 scope
(client<->server hop only; tunnel frames and LLM hop are follow-ups).
Co-authored-by: Isaac
* docs(benchmarks): note CI time-budget limit for high network delays
A CI dispatch at network_delay_ms=100 over the full journey set hit the
workflow's 30-min per-leg timeout: the delay multiplies across the full-turn
journeys' round-trips (cold start ~12 requests/op; turn journeys poll every
0.2s). Document the empirical budget (10ms finishes in ~6 min; 100ms times
out) and steer high-delay experiments toward an HTTP-journey subset.
Co-authored-by: Isaac
* feat(benchmarks): per-route request appendix + full-width CI table
Two follow-ups from reviewing the request-count output:
- The printed table truncated wide headers ("HTTP/op" -> "HTTP…") in CI logs,
because rich falls back to 80 columns when stdout is not a TTY. Give the
non-interactive console a 160-col floor so every header renders in full;
real terminals keep auto-detection.
- Add a per-journey network appendix so the request count is actionable, not
just a single number. ServerPerformanceMetrics now tallies requests by
low-cardinality route template (record_route, exposed via the debug
endpoint's route_counts); the harness diffs it per journey and the report
gains per-run route_requests plus a summary network_routes breakdown
({route, requests, per_op}, sorted per_op desc, grouped across runs). This
names which endpoints a journey's requests hit — e.g. session_cold_start's
~12 requests/op spread across the cross-process runner->server / host->server
calls — not just the total. The harness's own counter-poll route is filtered
out. Schema v5 -> v6; sample_output.json + README updated.
Co-authored-by: Isaac
* perf(benchmarks): drive warm turns over SSE instead of polling to idle
drive_turn polled GET /v1/sessions/{id} every 0.2s until the session status
returned to idle. That inflated the per-journey request count — normally
~2 GET/op, but ~800/op (124/op averaged) when a turn stalled and the loop
polled out the full 180s timeout, which is what made warm_turn's
GET /v1/sessions/{id} count balloon on the postgres leg.
Switch drive_turn to the SSE completion path the real Web UI uses: subscribe
to GET .../stream, post the message, and return on the session.status -> idle
event (guarded by seen_running so a prior turn's trailing idle can't end the
wait early). One subscription instead of an unbounded poll loop.
Result for warm_turn: a flat 3 requests/op (stream + events + policies/evaluate),
no ballooning when a turn is slow, and it mirrors production client behavior.
Latency is also more accurate — SSE observes completion immediately rather than
at the next 200ms poll tick, so p50 is no longer quantized upward.
_sse_session_status parses both the nested ({"data":{"status"}}) and flat
({"status"}) session.status shapes. Unit test + runner-journeys e2e cover it.
README CI-budget note corrected (turn journeys no longer poll).
Co-authored-by: Isaac
_resolve_harness() routes through _globals._agent_store, which is only
populated when the server starts via the CLI (runtime.init()). In other
deployment paths the global is None, so _resolve_harness silently returns
None and SessionCreatedEvent emits harness: null for SDK sessions.
Fix: in create_session, resolve the harness directly from the in-scope
agent and agent_cache (dependency-injected into every request handler),
which are always populated regardless of how the server starts. This
mirrors the native_agent path for native harnesses and uses the existing
_spec_harness() helper for SDK executor types.
Also adds unit tests for _resolve_harness covering:
- None conv / uninitialized store / agent not found → None
- harness_override wins before any store lookup
- executor config["harness"] key → resolved harness name
- executor.type fallback → resolved harness name
- unexpected exception → None (never raises)
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(credentials): stop mislabeling OAuth Databricks profiles as malformed
The configparser fallback in resolve_databricks_workspace treated any
profile without a static `token` as malformed and told the user to "fix
or remove it". OAuth profiles (auth_type = databricks-cli) legitimately
have no token — only the databricks-sdk path can mint one for them — so
the message was actively misleading, steering users to break a valid
profile.
Distinguish a well-formed OAuth profile (non-`pat` auth_type, no token)
from a genuinely malformed one via a new `_SectionNeedsSdk` signal, and
raise an actionable OSError instead. The message now branches on why the
SDK path failed: if databricks-sdk isn't installed (it ships in the
`databricks` extra, not the base install), it tells the user to install
`omnigent[databricks]`; if the SDK is present but auth failed, it points
at the CLI / OAuth session.
The PAT fail-loud guard (missing token on a token-auth profile) is
unchanged.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(credentials): harden SDK-import check and tailor non-CLI remediation
Address PR review:
- `_databricks_sdk_importable` now does a real `import databricks.sdk.config`
in a try/except instead of `importlib.util.find_spec`. find_spec can return
a spec for an SDK whose transitive deps are missing, and can even raise on a
partial install — both would misroute or escape the error-message branch.
- The `_SectionNeedsSdk` remediation is no longer hard-coded to OAuth. The
signal now carries the section's `auth_type`, and the resolver only suggests
`databricks auth login` for `auth_type = databricks-cli` (OAuth-U2M). Other
SDK-only auth types (azure-cli, metadata-service, oauth-m2m, …) get neutral
wording naming the actual auth_type. The profile is now described as
"token-less ... that only the databricks-sdk can resolve" rather than
unconditionally "OAuth".
Adds a test for the non-databricks-cli branch (azure-cli) asserting the
message names the auth_type and does not misdirect to `databricks auth login`.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* refactor(cli): extract native TUI subcommands into cli_native.py
Phase 0 of making native harnesses pluggable: carve the 11 native
coding-agent subcommands (claude, codex, opencode, pi, cursor, kiro,
goose, hermes, antigravity, qwen, kimi) out of cli.py into a dedicated
cli_native.py so the follow-up registry-driven seam lands in a small,
focused module instead of a 14k-line file. Behavior-preserving.
- New omnigent/cli_common.py holds the decorator-time constants
(RESUME_PICKER_SENTINEL, CLAUDE_STARTUP_PROFILE_ENV_VAR) and
reject_native_on_windows. It is a leaf module (imports nothing from
omnigent.cli), so both cli.py and cli_native.py can import it without a
cycle — required because Click evaluates command decorators at import
time.
- omnigent/cli_native.py exposes register_native_commands(cli), which
cli.py calls at module bottom (after the group and shared launch
helpers exist). Command bodies reach shared cli.py helpers through thin
call-time proxies on the omnigent.cli module, which keeps this module
free of a top-level omnigent.cli import (no cycle) and lets tests that
monkeypatch omnigent.cli.<helper> still take effect.
- polly/debby (bundled example agents, not native TUIs) stay in cli.py,
along with the shared helpers they and the native commands use.
Also drafts designs/harness-modular-registry-proposal.md (the doc the
harness_plugins.py comment already references), which lays out the full
NativeHarnessProvider plan and the phasing this commit begins.
Test plan: tests/cli/test_cli.py (244), test_chat.py/test_import.py/
test_runner_startup.py (137) all pass; ruff format+check and the
pre-commit file hooks pass; `omnigent <tool> --help` renders for all 11.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(cli): extract config/onboarding subsystem into cli_config.py
Gets cli.py under the 10k-line-per-file budget (13,248 → 9,664). The native
subcommand extraction alone left cli.py well over budget, so move the second
large cohesive block: the interactive harness/credential configuration
subsystem behind `omnigent config` / `omnigent setup` and the first-run
`configure harnesses` picker.
- New omnigent/cli_config.py (~3,650 lines) holds the 63 config helpers:
_configure_harness_add, every _manage_*_harness / _prompt_install_* / _set_*,
the ambient-credential adoption path, node-dependency preflight, and
_run_configure_harnesses_interactive. _CLI_LOGIN_BRAND moves with them (it had
no other user). The config/setup/integration Click commands stay in cli.py.
- The 3 config-load helpers the block needs (_load_global_config /
_save_global_config / _load_effective_config) stay in cli.py (used ~20x each
there); cli_config reaches them through call-time proxies, so importing
cli_config never imports omnigent.cli (no cycle) and monkeypatching
omnigent.cli.<helper> is still honoured.
- cli.py re-imports the 7 config entry points its commands call, so they remain
omnigent.cli attributes (patchable, importable) for callers and tests.
- Tests: repoint references for helpers that are called *intra*-cli_config to
omnigent.cli_config (where patching now takes effect) — the _manage_* dispatch
test, _adopt_detected_providers / _promote_global_auth_to_provider /
_launch_*_configure / _qwen_auth_configured patches, and the opencode / promote
imports. Helpers cli.py itself calls stay patched on omnigent.cli.
Behavior-preserving; no command, flag, or prompt changed.
Test plan: tests/cli/{test_cli,test_configure_models,test_opencode_setup,
test_chat,test_import,test_backend,test_runner_startup}.py all pass; ruff
format+check and pre-commit file hooks clean; cli.py is 9,664 lines.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(cli): address bot review on native/config extraction
Follow-ups from the PR #3047 bot reviews (Copilot, github-code-quality,
Polly), all behavior-preserving:
- cli_native.py: drop the duplicated --session/--resume validation block in
the codex command (Copilot) — it validated twice; the single pre-backend
check is kept, ordering unchanged.
- cli_native.py: fix the claude --host help text (Copilot) — the flag is a
no-op (del register_host), so the old "Requires --server" help was
misleading. Now marked [DEPRECATED] no-op.
- test_opencode_setup.py: use one import style for omnigent.cli_config
(github-code-quality) — drop the `from ... import` line and qualify the
two calls with the cli_config alias the file already uses.
- cli.py: drop the "(#334)" ticket id from the _run_bundled_agent comment
(Polly / CLAUDE.md "no ticket IDs in comments").
Test plan: tests/cli/{test_opencode_setup,test_cli,test_configure_models}.py
(362) pass; ruff check + format clean; claude/codex --help render.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* feat(projects): session→project membership over HTTP (Phase 1b)
Completes Phase 1 of the projects feature (see designs/PROJECTS_PRD.md) by
linking sessions to first-class projects and exposing it over HTTP. Phase 1a
(#2765) shipped the empty container; this adds the membership pointer and the
move/list surfaces that read it, so no column or store method ships unused.
- Migration c2d3e4f5a6b7 (chained after b1c2d3e4f5a6): nullable project_id
(Uuid16) on omnigent_conversation_metadata + ix_conversation_metadata_project_id.
Additive, no backfill, no DB FK (Rule R032). NULL = unfiled.
- Conversation.project_id on the entity; mapped in _to_conversation.
- ConversationStore.set_conversation_project() (file/move/unfile by id).
- list_conversations(project=<name>) is now a name-based dual-read: a session
is "in <name>" if it has EITHER the first-class membership (metadata.project_id
→ the owner's project of that name) OR the legacy omni_project label. "" =
unfiled. Backward-compatible: with no first-class members the filter collapses
to the prior label-only behaviour. The first-class prefetch is intersected
with the caller's permission-scoped ids so the IN/NOT IN list can't grow past
their own sessions.
- PATCH /v1/sessions/{id} files/unfiles by id (owner-only; target-project
ownership validated → 404, no existence leak); GET /v1/sessions?project=<name>
lists owner-scoped; project_id surfaced on SessionResponse / SessionListItem;
project_store wired into the sessions router; openapi.json regenerated.
- Tests: store membership ops + dual-read (incl. unfiled + cross-DB split-DB);
route move/unfile/list with single- and multi-user ownership boundaries.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(projects): reject null project_id; push unfiled exclusion down in single-DB
Addresses review on #3053:
- PATCH /v1/sessions/{id}: an explicit JSON ``null`` for project_id used to
coerce to "" and silently unfile the session, contradicting the contract
(omit = unchanged, "" = unfile). Reject null with 400 so only "" unfiles.
- list_conversations(project=""): in single-DB mode (metadata colocated with
conversations) push the first-class exclusion down as a NOT IN subquery
instead of materializing every filed id into Python. Split-DB keeps the
bounded prefetch. Caps memory for single-user / unscoped callers.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(projects): unfile-path 404 parity, single-DB IN subquery, doc null vs omit
Addresses the second review pass on #3053:
- PATCH /v1/sessions/{id}: the unfile branch (project_id == "") ignored
set_conversation_project()'s return, so unfiling a session with no metadata
row reported 200 while the file path returns 404. Check the result and raise
404 for parity.
- list_conversations(project=<name>): mirror the unfiled-branch optimization —
in single-DB mode use the member SELECT as an IN subquery instead of
materializing member ids into Python; split-DB keeps the bounded prefetch.
- UpdateSessionRequest.project_id docstring: distinguish omit (unchanged) vs
null (rejected 400) vs "" (unfile); regenerate openapi.json.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
The coverage-report job used `!cancelled()`, so it ran even when one or
more pytest shards failed. A failed shard drops its covered lines from the
`coverage combine`, so the resulting total is computed off partial data and
compared against main's baseline — misleading. A red pytest run gets re-run
anyway, which re-triggers coverage, so there's no value in computing it now.
Gate on `success()` so coverage-report only runs when every pytest shard is
green. The draft guard stays: on drafts pytest is skipped, and a skipped
dependency doesn't make `success()` false.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* feat(projects): first-class projects entity + CRUD container
Promote "projects" from the implicit ``omni_project`` conversation label to a
first-class, owner-private container that groups sessions and exists
independently of its members — so it can be empty, renamed, and (later) carry
its own config. See designs/PROJECTS_PRD.md.
This is Phase 1a — the container only: create / list / rename / delete empty
projects. Session->project membership (the conversation_metadata.project_id
column, conversation-store plumbing, dual-read listing) and the session-move
HTTP surfaces are Phase 1b (a follow-up), so this PR ships no column or store
method that nothing consumes yet.
- projects table (SqlProject): Uuid16 id, name, owner_user_id, created_at,
updated_at. ix_projects_owner_user_id (workspace_id, owner_user_id,
created_at, id) serves the owner-scoped list ordered by created_at as a pure
index scan; UNIQUE (workspace_id, owner_user_id, name) enforces per-owner
name uniqueness at the DB layer for non-NULL owners (the store's _name_taken
check guards NULL-owner / single-user rows).
- Migration b1c2d3e4f5a6 creates the table only; additive, no backfill,
no DB foreign keys (Rule R032).
- Project entity; ProjectStore + SqlAlchemyProjectStore (owner-scoped CRUD;
IntegrityError -> ALREADY_EXISTS as the uniqueness-race backstop).
- POST/GET/PATCH/DELETE /v1/projects, owner-scoped; wired into create_app +
CLI; schemas + openapi.json regenerated.
- Tests: store CRUD + owner isolation + name uniqueness (incl. DB backstop);
route CRUD (single- + multi-user header auth); entity.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(projects): discriminate name-UNIQUE violation before mapping to ALREADY_EXISTS
The create()/update() IntegrityError handlers translated *any* integrity
failure into an ALREADY_EXISTS name collision, which could hide unrelated
problems (a PK collision on id, a NOT NULL violation) behind a misleading
409/"already exists". Add _is_name_conflict() to translate only when the
per-owner name-UNIQUE index was hit and re-raise everything else. It matches
both dialect signatures: Postgres names the index (ix_projects_name), SQLite
lists the columns (projects.name).
Also add a regression test proving a non-name integrity failure (PK reuse)
re-raises as IntegrityError, and tidy the list-order assertion to a set
membership check.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
An abrupt browser disconnect tears the dictation WebSocket's ASGI task
down via cancellation. The cleanup in the finally block awaited
handle.close() inside the already-cancelled scope, so the cancellation
fired at the await before the close ran — leaking the take. For the
remote engine this leaks a worker capacity slot until the connection
dies. contextlib.suppress(Exception) did not help: anyio cancellation is
a BaseException, and suppressing it only hides the traceback while the
close is still skipped.
Wrap the close in a shielded anyio.CancelScope so cleanup always
completes before the outer cancellation resumes.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* feat(host): add install-harness tunnel frame pair + registry plumbing
Adds the HostInstallHarnessFrame / HostInstallHarnessResultFrame pair to
the host tunnel protocol, mirroring the existing HostCreateDirFrame
request/result pattern, plus the pending_installs future map on
HostConnection. This is the vocabulary the server and a connected host
use to negotiate a UI-driven harness install (later PRs add the host
handler, the route, and the frontend button).
Additive only: no frame is sent or received yet, so behavior is
unchanged. The result frame carries a freshly-recomputed readiness map
(configured_harnesses, reusing _optional_str_availability_map) so the UI
can flip the harness badge without waiting for a reconnect.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* refactor(onboarding): surface install failure reason from install_harness_cli
Extracts install_harness_cli_with_reason(key) -> tuple[bool, str | None]
alongside the existing install_harness_cli(key) -> bool, which becomes a
thin wrapper that discards the reason. Single implementation, no caller
churn: the four setup-wizard call sites keep their boolean contract
unchanged.
The reason is derived from the existing failure branches (manual-only
spec, missing installer, timeout, OS error, non-zero exit, post-install
binary-not-found) without capturing installer output — so omni setup's
live npm output UX is preserved. A later PR's UI-driven install returns
this reason to the user instead of a bare failure.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* feat(host): install harness on request + resolve the install result
Adds the host daemon side of UI-driven install:
- _handle_install_harness in host/connect.py runs
install_harness_cli_with_reason off the event loop, recomputes
configured_harness_map(), and returns a HostInstallHarnessResultFrame
carrying either the fresh readiness map or a failure reason.
- host_tunnel.py's receive loop resolves the pending_installs future.
- A shared allowlist/resolver (ui_installable_harnesses / ui_install_key)
in onboarding/harness_install.py is the single source of truth for
which harnesses are UI-installable (claude, codex, pi, opencode, qwen)
and their install-spec keys.
Defence in depth: the handler re-checks ui_install_key, so a stray or
spoofed frame can never drive the installer for a non-allowlisted
harness (e.g. hermes, whose installer is a curl | bash). Inert until PR4
wires a sender: nothing emits HostInstallHarnessFrame yet.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* feat(server): add UI harness-install route behind a default-off flag
Adds POST /v1/hosts/{host_id}/harnesses/{harness}/install: the server
endpoint the web UI's Install action calls. It validates in order —
feature flag (404 when off) -> allowlist (400) -> auth/require_user ->
owner (403) -> liveness (409) — then forwards a HostInstallHarnessFrame
over the tunnel via _proxy_install_harness and returns the host's
refreshed configured_harnesses map.
- Reuses the _proxy_create_dir request/future/wait_for template; the
install timeout (330s) sits above install_harness_cli's 300s subprocess
ceiling so the result is received before the server gives up.
- Concurrent installs of the same (host, harness) coalesce onto one
in-flight task (conn.inflight_installs) so a double-click can't fire two
non-race-safe global npm installs.
- Gated by OMNIGENT_HARNESS_INSTALL_ENABLED, surfaced to the SPA via
GET /v1/info (harness_install_enabled), mirroring smart_routing_enabled.
Allowlist ordering (400 before 403) avoids leaking host ownership through
error codes. Ships dark: with the flag off the route is 404, so merging
this changes nothing in production.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(host): make UI install idempotent + widen the server wait
End-to-end testing against a real host surfaced two issues the stubbed
unit tests masked:
- The host ran `npm install -g` even when the harness CLI was already on
PATH; npm re-resolves over the network and took >60s for an
already-present binary, so a repeat Install click hung. _handle_install_harness
now short-circuits on harness_cli_installed(key) and just returns fresh
readiness (reusing the existing check) — sub-second on the happy path.
- The server's per-call wait (330s) sat only 30s above install_harness_cli's
own 300s subprocess cap, so a genuine cold npm install could finish right
as the server gave up — a "504 but actually installed" outcome. Widened
to 420s (300s + 2min headroom for readiness recompute + tunnel latency).
Verified end-to-end: happy path 200 in 0.8s (already-installed fast-path),
a real cold opencode install completes route->tunnel->daemon->npm->readiness,
hermes rejected 400, codex reports needs-auth post-install.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* chore(openapi): regenerate spec for the harness-install route
CI's openapi-drift guard flagged openapi.json as out of sync after the
new POST /v1/hosts/{host_id}/harnesses/{harness}/install route. Regenerated
via scripts/dump_openapi.py so the committed spec matches the app.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* refactor(server): share the harness-install flag env-var name
Extract OMNIGENT_HARNESS_INSTALL_ENABLED into a single
HARNESS_INSTALL_ENABLED_ENV constant in hosts.py, read by both the
install route and the /v1/info flag in app.py, so the flag the UI sees
and the flag the route enforces can never drift on a typo. Also switch
the install-task scheduling from asyncio.ensure_future to the more
idiomatic asyncio.create_task.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* feat(server): describe per-harness setup steps for the UI setup flow
Extends the harness-install backend so the web UI can render a "set up this
agent" checklist that mirrors omnigent setup, instead of a single Install
button.
- /v1/harnesses now carries an ordered setup_steps list per harness (install,
then auth), derived from the existing HarnessInstallSpec so it can't drift
from the real install/login commands. Claude/Codex/Pi/OpenCode/Qwen get a
first-class two-step flow; other harnesses get a generic "run omnigent setup"
step.
- The host readiness map now reports a two-step signal (binary-missing /
needs-auth) for Claude and OpenCode too, matching Codex, so the UI can show
install-done vs sign-in-done. Pi/Qwen stay binary-only (their credential
isn't locally determinable).
- The launch gate (harness_is_configured) is unchanged and stays binary-only,
so a not-signed-in harness is never blocked from launching.
- /v1/info advertises installable_harnesses (bare + native spellings) so the
UI offers setup only where the install route will accept it.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* feat(server): key harness setup steps by every spelling for the UI
The setup dialog looks up steps by the harness a session declares — often a
native wrapper (codex-native) or an installable id that isn't a picker row
(opencode/qwen), none of which appear in the harness catalog. Add
harness_setup_steps_by_spelling() and return it from GET /v1/harnesses as a
top-level setup_steps map so the dialog can resolve steps for whatever id it
holds, without adding non-pickable rows to the catalog.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(server): use host.user_id in the install route's owner check
The install route still compared host.owner, but the Host model's owner field
was renamed to user_id (identity-columns unification on main). An authenticated
install therefore 500'd with AttributeError. Switch to host.user_id (matching
every other host route) and add an owner-mismatch test that exercises the
ownership branch with a real user_id — the existing tests run unauthenticated,
so the comparison was never hit.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* docs(server): correct the setup-step "can't drift" comment
The auth-step commands (codex login, etc.) are display-only literals, not
derived from HarnessInstallSpec.login_args — only the install step's label is
derived. Reword the comment/docstring so they don't overstate the guarantee.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* Address review: family-keyed install coalescing + clearer naming
- Coalesce concurrent UI installs on the resolved install *family* key
(ui_install_key) rather than the raw spelling, so codex + codex-native
(both the openai npm package) share one in-flight install. Cleanup is
tied to task completion via add_done_callback and every caller awaits
under asyncio.shield, so a cancelled request can't clear the map out
from under a follow-up and start a second concurrent `npm install -g`.
- Add an integration test that fires two overlapping same-family installs
and asserts exactly one frame reaches the host.
- Rename install_harness_cli_with_reason -> try_install_harness_cli and
return a HarnessInstallResult NamedTuple instead of a bare tuple.
- Trim the over-long install-handler docstring and UI-installable map
comment to the essentials.
Co-authored-by: Isaac
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
---------
Signed-off-by: xq-yin <xiaoqian.yin@databricks.com>
* fix(web): don't queue messages while only background work is running
A session with a running background job (background shell / still-running
sub-agent) settles into the `waiting` status: the turn already ended and the
server's turn gate is free to accept a new turn, but the frontend treated
`waiting` as busy and queued every new message client-side until full idle.
Two independent gates forced this:
- `shouldQueueSend` / `maybeFlushQueuedHead` treated `sessionStatus ===
"waiting"` as busy, so sends queued and the queue wouldn't drain.
- The `session_status` handler grouped a `waiting` edge carrying a
`response_id` (which the claude/cursor-native Stop hook always posts) with
`running`, forcing local `status = "streaming"`, which never cleared while
background work ran. The composer's "(queued)" placeholder and the send gate
both key off local `status`, so this alone kept messages queued on native
sessions.
Treat `waiting` as a turn-end edge everywhere it gates sends: drop it from the
busy checks and finalize the local send lifecycle like `idle`, while keeping
`sessionStatus = "waiting"` and `backgroundTaskCount` so the "Working…" spinner
and sidebar dot still reflect the background activity. A new message now starts
a fresh turn immediately, matching what the server already accepts.
This only affects sessions with background work running — a turn that ends with
no background work still settles on `idle` and behaves exactly as before.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): treat waiting as turn-end on reconnect; add e2e coverage
Address the Polly review notes on the message-queueing fix and add the
e2e_ui coverage the required gate asks for.
- `reconnectStatusPatch`: a `waiting` snapshot is a turn-end edge, so it now
finalizes the local send lifecycle like `idle` instead of reopening a
streaming response. The server keeps `active_response_id` populated across
`waiting` (it only pops on idle/failed), so grouping `waiting` with
`running` re-opened "streaming" on a reload/reconnect and re-queued sends —
the exact behavior the fix removes. Now covered for the reloaded-tab path,
not just live SSE.
- The live-SSE mismatched-id `waiting` branch now finalizes a still-streaming
bubble to `completed`, matching the matching-id path, so a stale bubble
doesn't linger spinning with no edge left to close it.
- Add tests/e2e_ui/chat/test_send_while_background_task.py: publishes the
native Stop-hook `waiting`+response_id edge live, then asserts the composer
sends directly (idle placeholder, user bubble renders, no queued strip)
instead of queueing behind the background task.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Replace M individual session.get() PK lookups + M individual UPDATEs
with a single IN-clause query to fetch existing to_user grants, then
one bulk DELETE for duplicates and one bulk UPDATE for reassigns.
For M grants this reduces the query count from 1 + M + up to M = 1+2M
down to 3 queries regardless of M.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* perf(store): batch FTS inserts in append and fork_conversation
Each call to insert_fts issued a separate raw SQL INSERT into the
conversation_items_fts table, causing N+1 queries when appending or
forking conversations with many items.
Add insert_fts_bulk(session, rows) in omnigent/db/utils.py that issues
a single multi-row INSERT for any number of rows. Replace the per-item
insert_fts calls in append and fork_conversation with a single
insert_fts_bulk call after the loop. Keep insert_fts intact for
single-item callers.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(db): chunk insert_fts_bulk to avoid SQLite variable limit
Split rows into chunks of 300 (3 params × 300 = 900 binds) so a
single INSERT never exceeds SQLite's SQLITE_MAX_VARIABLE_NUMBER (999
on pre-3.32 builds). Without chunking, fork_conversation on a large
conversation raises OperationalError: too many SQL variables.
Also add the list[tuple[str, str, str]] annotation to fts_rows in
fork_conversation to match the append call site.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Skipping unresolvable function policies left an empty gate that allowed
every tool call. Install a deny sentinel instead so a misconfigured
policy cannot disappear silently.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Reintroduce the remote path split out of the initial dictation PR, now as
a registered engine rather than a special-cased branch.
- Register a `remote` engine (OMNIGENT_DICTATION_ENGINE=remote) that relays
each take to a dictation worker over the same wire protocol the browser
speaks. Selected purely by env var — OMNIGENT_DICTATION_REMOTE_URL points
at the worker; no CLI integration, keeping the surface small for a niche
deployment (weak main server + a beefier LAN box).
- Ship the standalone worker (python -m omnigent.server.dictation_worker):
create_dictation_router served on its own, unauthenticated, LAN-only.
- Per-take fallback to the local sherpa engine (lazy) when the worker is
unreachable and models are installed.
- Widen the web client's ready/stop timeouts to outlast the worker's
cold-load budget.
websockets is already a core dependency, so no new package. The engine slots
into the registry with no changes to the route, protocol, or selection logic.
Co-authored-by: Isaac
Signed-off-by: kerry.chang <kerry.chang@your.hostname.com>
Co-authored-by: Ubuntu <kerry.chang@your.hostname.com>
* feat(scheduled tasks): track run completion + expose run history
The fire path records a scheduled_task_runs row as `running` and never
revisits it, so runs stayed `running` with finished_at=NULL forever even
after the agent turn completed (the FU-1 gap confirmed in prior E2E).
list_runs also existed in the store but was exposed by no REST route.
Add a periodic reconciliation backstop + run-history endpoint:
- Store `update_run` (conditional WHERE status=running, idempotent — an
already-terminal run is never clobbered and concurrent sweeps can't
double-transition) and `list_runs_by_status_all_workspaces` (the sweep
source). ScheduledTaskRun entity now carries workspace_id so the sweep
can re-enter each run's workspace_scope.
- `run_reconciler.py`: a 60s asyncio loop (own module, off the
ScheduledTaskScheduler) that reads each running run's conversation and
transitions it — completed transcript -> succeeded; a failure label /
missing conversation -> failed(code); live_status running/waiting is a
cheap pre-filter. A run past a 6h max-age with no terminal state is
force-failed (error_code=incomplete) so every run eventually terminates.
Wired into the server lifespan next to the scheduler.
- `GET /v1/scheduled-tasks/{id}/runs`: owner-scoped run history (404 if
not owned), API-stable field naming.
No schema/migration change — status codec already had succeeded/failed and
the columns (finished_at/error/error_code) already exist. FU-3 + #2978
semantics intact (owner via user_id; API-stable owner_user_id JSON key).
Tests: update_run transitions + idempotency; reconciler classification
matrix (completed->succeeded, errored/cancelled->failed, in-flight and
young runs left alone, stale->failed(incomplete)); GET runs 200/empty/404.
Full targeted suite green (155). E2E on a live server + connected host:
a real timer fire's run flipped running->succeeded with finished_at set
(the exact thing that stayed running before), readable via the runs
endpoint; honest-fail still records failed(no_online_host) and the sweep
leaves terminal runs untouched.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* feat(scheduled tasks): make run completion event-driven (replaces poll)
Replaces the 60s all-workspaces reconciliation poll from the previous commit
with an event-driven completion hook + a poll-free orphan backstop, matching
how the sibling scheduled-task systems reconcile (at a lifecycle boundary, not
on a timer).
Primary mechanism: a completion hook
(``session_live_state.persist_scheduled_run_completion``) fired from
``_publish_status`` the instant a fired conversation's turn reaches a terminal
edge (idle -> succeeded, failed -> failed+error_code). It rides the same
long-lived SSE relay that already persists ``live_status`` for a browserless
scheduled fire, routed through the same ordered/contextvar-copying executor so
the run's ``workspace_scope`` reaches the write thread. A reverse lookup
(``get_running_run_by_conversation``, backed by a new
``(workspace_id, conversation_id)`` index) finds the run; the idempotent
conditional ``update_run`` (WHERE status=running) transitions it and never
clobbers an already-terminal row. For the common (non-scheduled) conversation
the lookup returns None and the hook is a cheap no-op.
Orphan backstop (no periodic poll): the ``ScheduledRunReconciler`` becomes a
ONE-SHOT startup sweep (reconciles runs left ``running`` by a restart
mid-fire), and a lazy-on-read pass at ``GET /v1/scheduled-tasks/{id}/runs``
force-fails a task's runs past the 6h max age (``incomplete``). Together they
keep the invariant "every run eventually reaches a terminal state" without a
recurring background sweep.
One migration: the ``conversation_id`` index. FU-3 / #2978 owner semantics,
the ``GET /runs`` response shape, and the fire-time ``_record_run`` writes are
unchanged.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* refactor(scheduled tasks): drop startup sweep, lazy-on-read is sole backstop
Simplifies the orphan backstop per review. The event hook already transitions
every normal run the instant its turn ends; the boot-time startup sweep is
removed entirely (fewer moving parts). A run orphaned by a mid-fire restart
that nobody ever opens staying `running` in the DB is harmless until read, and
reading it fixes it.
Changes:
- Remove `run_startup_sweep`, the `ScheduledRunReconciler` class, and its
lifespan wiring in app.py. `run_reconciler.py` reduces to the stale-run
policy: the constants + a shared `force_fail_stale_runs` helper (pure
age-based, no conversation I/O).
- Run the lazy force-fail-stale reconcile on BOTH read endpoints:
- `GET /v1/scheduled-tasks/{id}/runs` (detail, already there).
- `GET /v1/scheduled-tasks` (list, ADDED) — force-fail the owner's tasks'
runs still `running` past 6h so a Tasks-list badge never shows a stale
orphan as `running`. Owner-scoped indexed query
(`list_running_runs_for_tasks`), conditional `update_run`, no per-run
conversation read.
- Drop the now-unused `list_runs_by_status_all_workspaces` store method.
Net mechanism: (a) event hook = primary, instant terminal transition;
(b) lazy-on-read force-fail-stale on list + detail = the only orphan backstop.
No startup sweep, no periodic poll of any kind. Keeps the 6h
STALE_RUN_MAX_AGE_SECONDS invariant "every run eventually terminal".
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* refactor(scheduled tasks): drop dead ScheduledTaskRun.workspace_id field
The ``ScheduledTaskRun`` entity carried a ``workspace_id`` field solely so the
cross-workspace reconciler sweep could re-enter each run's ``workspace_scope``
before acting on it. That sweep is gone — completion is event-driven and the
lazy-on-read backstop both run inside a single ambient ``workspace_scope`` — so
the field has no reader. Its only consumer was the deleted ``_reconcile_run``.
Remove the field from the entity dataclass and drop the ``workspace_id=`` line
in ``_run_to_entity``. The DB column ``scheduled_task_runs.workspace_id`` (the
real tenant partition key) and its index are unchanged; the store still filters
every query on ``current_workspace_id()``.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* refactor(scheduled tasks): PR polish — comment fix, fired_at age basis, hook wiring test
Addresses three review findings on the FU-1 run-completion PR:
- Fix a stale finally-block comment in app.py: it still said the run reconciler
is "a one-shot startup sweep (no periodic task to cancel)", but the startup
sweep was removed — completion is event-driven + lazy-on-read, so there is no
reconciler task at all. Comment now says only the per-job scheduler needs
stopping. The scheduled_task_scheduler.stop() logic is unchanged.
- Measure the lazy-on-read stale window from fired_at (falling back to
scheduled_at when a run never recorded a fire time), not scheduled_at. A run
that fired late no longer gets a shortened effective window — the 6h clock
starts when dispatch actually began. Locked by two unit tests: a run fired
>6h ago is force-failed; a run scheduled >6h ago but fired recently is left
alone.
- Add integration coverage for the primary completion mechanism at the
_publish_status seam: drive the real _publish_status(conversation_id, "idle")
/ "failed" edge (the way the SSE relay does) and assert the scheduled_task_run
transitions running -> succeeded / failed(+error_code) with finished_at set,
through the hook + shared session_live_state executor (workspace_scope
contract exercised, not bypassed). This locks the wiring so a future
_publish_status refactor can't silently break scheduled-run completion.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
---------
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* feat(server): streaming dictation endpoint (local speech-to-text)
Adds WS /v1/dictation/stream + GET /v1/dictation availability probe,
backed by a lazily-loaded sherpa-onnx streaming transducer (new
optional extra: omnigent[dictation]) with optional online
re-punctuation. Fills the gap documented in web/electron/README.md:
dictation where the browser Web Speech API has no backend, with audio
never leaving the operator's infrastructure.
A deterministic fake engine (OMNIGENT_DICTATION_ENGINE=fake) keeps CI
hermetic and will drive the Playwright e2e test.
See designs/server-dictation.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): stream server dictation into the composer mic button
When the browser has no Web Speech backend (Electron, Firefox,
Chromium), the mic button now falls back to the server recognizer:
GET /v1/info advertises dictation_available, an AudioWorklet
downsamples the mic to 16 kHz PCM over WS /v1/dictation/stream, and
partial transcripts form live in the composer via a replaceable
interim region (useDictationInsert) shared by ChatPage and
NewChatDialog. Web Speech behavior is unchanged where it works.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e-ui): dictation loop against the fake engine
Fake mic (Chromium fake media device) -> AudioWorklet -> dictation WS ->
OMNIGENT_DICTATION_ENGINE=fake -> transcript lands in the composer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: ruff format + regenerated openapi.json for dictation routes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: prettier formatting
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e-ui): honor plugin context args in the dictation test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: drop the caller-less GET /v1/dictation probe
ponytail review: the web UI only reads dictation_available from
GET /v1/info, so the dedicated probe endpoint had no caller. Also
simplify the engine singleton (config never changes mid-process;
tests inject engine_provider) — a failed load still caches nothing,
so gaining models doesn't require a restart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: hardware sizing table for dictation models
Measured on Apple M-series and an Intel N95 mini-PC: the default
Nemotron 0.6B is too slow for N95-class servers (0.6-0.7x realtime);
the mid-size streaming zipformer decodes 1.4-2.3x realtime there in
~190 MB and held accuracy in spot checks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(server): remote dictation worker relay with local fallback
OMNIGENT_DICTATION_REMOTE_URL relays takes to a dictation worker on a
beefier LAN box over the existing wire protocol; local models (when
installed) serve as a lazy fallback when the worker is down. Ships a
standalone single-route worker entrypoint
(python -m omnigent.server.dictation_worker). Motivated by real
hardware: an N95 main server decodes the default 0.6B model at only
0.6x realtime, but a workstation on the same LAN runs it at 9x.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(deps): pin sherpa-onnx-core + numpy explicitly in the dictation extra
sherpa-onnx's wheel metadata declares its native payload package
(sherpa-onnx-core, which carries libonnxruntime) inconsistently across
platforms, so it was missing from uv.lock — failing the hashed OSV
audit in CI and breaking aarch64 installs. Pinning it explicitly fixes
both and removes the fetch script's aarch64 fixup. numpy is imported
directly by the engine, so declare it instead of riding transitives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: harden dictation take lifecycle (adversarial review findings)
Server: the route now closes the engine stream handle on every exit
path — an abandoned take (browser vanished mid-dictation) previously
leaked the remote relay's worker WebSocket and reader thread, holding a
worker capacity slot forever and eventually starving dictation for
everyone.
Web client, all confirmed by review:
- useDictationInsert strips the interim region only when the draft
still ends with the exact text it inserted, so dictation can never
delete user-typed text; ref bookkeeping moved out of the setState
updater (StrictMode double-invokes updaters).
- The worklet flushes its partial chunk before stop() tears the graph
down — trailing speech under the 100 ms boundary was being clipped
from every take.
- Client ready/stop budgets now exceed the server's cold-load and
worker-flush budgets (40 s / 15 s), so slow first takes and slow
tail flushes no longer fail or drop text spuriously.
- The 1013 at-capacity close surfaces as "busy — try again" instead of
"unavailable", and engine-init error frames surface their message.
- A socket close during audio-graph setup now fails the start instead
of resolving a dead session that silently drops all audio.
- Web Speech network-error fallback is per take, not sticky: a
transient blip in real Chrome no longer permanently downgrades the
page to the server model, and stale events from the dead recognizer
can no longer clobber the live server take's state (which could
leave the mic recording while the button showed idle).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: dictation model choices for other languages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(web): format dictation files
* fix(server): close dictation takes even when the task is cancelled
An ASGI server cancels the websocket handler task on shutdown. The
cleanup awaited asyncio.to_thread(handle.close) inside finally, so the
CancelledError could arrive before the worker thread ran close() --
about half the time, measured. contextlib.suppress(Exception) never
caught it: CancelledError is a BaseException.
Create the close task before the first await point and shield it, so it
runs to completion while cancellation propagates. Hold a strong ref
(asyncio keeps only a weak one) and retrieve the result so a failing
close logs instead of warning.
Also corrects the comments: an abandoned take is reaped by the ASGI
server's ping timeout (~20s), not held forever. Verified against a live
worker with OMNIGENT_DICTATION_MAX_STREAMS=1.
* refactor(dictation): split out remote, add engine registry, fold beautify
Keep this PR focused on local dictation and make future model swaps cheap:
- Defer the remote worker (RemoteDictationEngine, dictation_worker.py, and
the close-on-cancel machinery that existed to release a worker slot) to a
follow-up PR. Remote only helps a narrow deployment; local sherpa runs at
many-times realtime on any normal machine, so this does not block testing.
- Select engines by name from a registry (register_engine); get_engine and
engine_availability resolve from it instead of an if/elif ladder. Adding
an engine is one call with a factory + availability probe.
- Fold punctuation into the sherpa engine and drop beautify from the
DictationStreamHandle protocol. Emitted text is display-ready, so the
seam is PCM-in -> text-out -> close; models that punctuate themselves
(Whisper, Parakeet) implement nothing extra.
Co-authored-by: Isaac
* chore: re-trigger CI checks
Empty commit to re-run the security scan and CI on this PR.
Co-authored-by: Isaac
* build(deps): minimize dictation lock diff to sherpa-only, public index
The merge re-lock rewrote every uv.lock URL to the Databricks internal
index proxy and would fail the public-registry lint. Restore public
pypi.org / files.pythonhosted.org URLs so the lockfile diff versus main
is only the two dictation packages (sherpa-onnx, sherpa-onnx-core), with
no unrelated churn.
Co-authored-by: Isaac
* fix(web): sync ServerInfo test fixtures with merged capability fields
The main merge made single_user/sharing_mode/public_sharing_enabled
required on ServerInfo while dictation_available became required from this
PR, but four test fixtures each construct a ServerInfo literal missing the
other side's fields, failing tsc (and the web build via Docker/E2E-UI).
Add the missing fields so every fixture is a complete ServerInfo.
Co-authored-by: Isaac
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ubuntu <kerry.chang@your.hostname.com>
Idle shutdown was terminating runners while sys_call_async results were
still in flight because has_active_work only checked foreground/harness
turns. Keep the runner alive for live async tasks, timers, and parked
approvals without pinning on completed or housekeeping work.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Malformed JSON previously fell through to {}, which could run a
default/no-argument system tool. Require a JSON object and return the
canonical structured error before dispatch.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Descriptions told the model to cancel with task_id while dispatch already returned handle_id. Align schemas/messages on handle_id and keep task_id as an identical compatibility alias scheduled for removal in 0.8.0.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
_COMPACT_LOCKS existed but was never acquired, so concurrent compact
events could both observe idle and run at once. Hold a WeakValueDictionary
lock per session, recheck status after acquire, and cover the race with a
deterministic concurrency test.
Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
Per-parent child-title uniqueness was enforced by a UNIQUE index on
(workspace_id, parent_conversation_id, title_hash), where title_hash was a
16-byte sha256(title)[:16] mirror of title maintained solely to key that
index. Reads never used it (the runner's find-or-create pre-check filters
title, whose 3rd index column was title_hash), so it was pure write
amplification.
Move the check into create_conversation: a per-parent (parent, title)
existence SELECT served by idx_conversations_parent, raising
NameAlreadyExistsError on a hit. Only children are scoped; top-level (NULL
parent) sessions may reuse titles freely, as before. Drop the index, the
title_hash column, the two hash helpers, the _CKSUM16 alias, the ORM default
and the two rename-path recomputes, and the store's IntegrityError->title
translation (the id-PK branch stays).
Trade-off: the DB index was the atomic backstop for concurrent same-name
spawns (tool calls dispatch concurrently within a turn). The app check is
best-effort, so a rare concurrent duplicate spawn now yields a stranded
duplicate child + a wasted runner instead of a clean error. Bounded, not
corruption; the common repeat-send path is unaffected (served by the runner
pre-check).
Migration 72e6dceae14f. SQLite drops/recreates idx_conversations_parent by
hand around the batch rebuild so its DESC ordering survives; MySQL/Postgres
use native DROP COLUMN. Downgrade re-adds title_hash, back-fills it in
Python, and restores the unique index.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
Runners were reported to be "randomly dying" with no explanation in the
runner log — an uncaught exception left only a bare traceback on stderr,
and orderly shutdowns (signal, idle timeout, tunnel drop, parent death)
logged nothing at all.
Attribute the exit on each hookable path so the runner log always says
why it stopped:
- uncaught exceptions via sys.excepthook (with traceback) — the
silent-crash case
- SIGTERM/SIGINT, recording the specific signal
- idle timeout, websocket tunnel close, and the parent-death hard-exit
backstop (logged at the os._exit call site, which skips atexit hooks)
- fatal server rejection keeps its concise stderr message
SIGKILL and os._exit remain uncatchable in-process; the absence of an
exit line is itself the signal that the runner was killed uncatchably.
Co-authored-by: Isaac
Replace M individual session.get() PK lookups + M individual UPDATEs
with a single IN-clause query to fetch existing to_user grants, then
one bulk DELETE for duplicates and one bulk UPDATE for reassigns.
For M grants this reduces the query count from 1 + M + up to M = 1+2M
down to 3 queries regardless of M.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
When deleting a conversation with N descendants, each FTS row was
deleted in a separate DELETE statement. Replace the per-ID loop with
a single DELETE ... WHERE conversation_id IN (...) via the new
delete_fts_by_conversation_ids helper. The single-ID function is
kept intact for other callers.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(telemetry): track sdk harness name in SessionCreatedEvent
SDK sessions (claude-sdk, openai-agents, codex, etc.) previously emitted
`harness: null` on the SessionCreatedEvent because only native agents have
a `native_agent.harness` attribute. Fall back to `_resolve_harness(conv)`,
which already handles harness_override and spec lookup, so every harness
kind is now represented in telemetry.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* feat(session-ui): support HTTP headers on MCP servers in session UI
Adds the ability to set, view, and edit HTTP headers (e.g. Authorization)
on HTTP-transport MCP servers through the session agent info panel.
Backend:
- MCPServerSummary now includes a headers field; values are always
[REDACTED] in API responses (only key names are exposed).
- UpsertMCPServerRequest accepts headers: dict[str, str] | None.
None preserves existing headers; {} clears them.
- New _apply_headers() helper replaces the old _preserve_keys() call for
headers so edits via the UI actually take effect rather than always
restoring the bundle's headers.
- Fixed sessions.py and builtin_agents.py MCPServerSummary construction
to populate headers (previously always returned {}), which caused
headers to disappear when reopening the edit dialog.
Frontend:
- McpFormState/UpsertMcpServerInput/McpServerSummary all carry headers.
- McpServerManagerDialog shows a key-value editor for HTTP headers
(add row with +, remove with x, values show as [REDACTED] for
existing headers).
- Fixed AgentInfoButton popover closing when the MCP manager Dialog
opens: uses onInteractOutside/onFocusOutside on PopoverContent to
suppress Radix's outside-click dismiss while a nested dialog is open.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(create-agent): accept KEY: VALUE format in headers textarea
parseKVLines only split on '=' so users typing the natural HTTP header
format (Authorization: Bearer ...) got silently dropped. Now accepts
both '=' and ':' as separators, taking whichever comes first.
Updated the placeholder to show the colon form.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(mcp-headers): preserve real secrets when [REDACTED] sent on edit
When a user opens the MCP server edit dialog, header values come back
as [REDACTED] from the API. If they save without changing those values
the client sends { Authorization: '[REDACTED]' }, which was being
written literally into the bundle YAML — overwriting the real token.
_apply_headers now treats a value equal to the '[REDACTED]' sentinel
for an existing key as 'preserve the stored value', restoring it from
the existing bundle entry instead of writing the placeholder.
Also reverts unrelated package-lock.json churn and adds a round-trip
integration test covering the edit-with-existing-headers scenario.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* chore: regenerate openapi.json for MCP headers fields
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(mcp-headers): send {} to clear headers when all rows removed
When editing a server and removing all header rows, the frontend was
sending null (preserve) instead of {} (clear), so stale auth tokens
were silently kept in the bundle.
null now only means 'preserve' for new servers (no originalName).
Editing an existing server with zero rows sends {} to explicitly clear.
Adds integration test covering the clear-all path.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Bump the SDK-proxy harness subprocess and native CLI pane idle-reap
defaults from 30 minutes to 1 hour so short lulls between turns don't
tear down live sessions. Both defaults intentionally mirror each other;
the runner-level watchdog was already at 1 hour, so it now consistently
outlives the inner reapers it contains. Both remain env-overridable.
Co-authored-by: Isaac
* perf(web): lazy-load Shiki so it leaves the main bundle
Shiki's engine (including its WASM regex engine) was pulled into the app's
main entry chunk even when no code block ever rendered. Two eager importers
kept it there: code-block.tsx and the @streamdown/code highlighter plugin
wired into chat markdown via streamdown-security.ts.
Defer both. code-block.tsx now imports shiki at highlight time inside its
existing per-language cached getHighlighter helper. A new lazyCodePlugin
wraps @streamdown/code, satisfying Streamdown's CodeHighlighterPlugin
contract (default themes synchronously; highlight() returns null until the
engine loads, then resolves tokens through the callback) while deferring the
@streamdown/code import — and with it shiki — to the first highlight call.
Rendering, theming, language handling, and public APIs are unchanged. Shiki
now splits into a separate on-demand chunk: the main entry chunk drops from
4,551.81 kB to 4,356.25 kB (~196 kB raw, ~60 kB gzip), and Vite no longer
reports the ineffective-dynamic-import warning.
Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com>
* test(web): prove lazy Shiki highlighting through Streamdown + harden callback
Address cross-vendor review of the lazy-Shiki change.
Verified lazyCodePlugin matches Streamdown's real consumption contract:
HighlightedCodeBlockBody runs highlight() inside a useEffect and stores the
result via setState — `let r=o.highlight({...}, c=>{i(c)}); r&&i(r);`
(streamdown/dist/highlighted-body-OFNGDK62.js). Returning null keeps the raw
code in state; the callback calls setState, forcing a re-render with the
highlighted tokens. The highlighted body is itself React.lazy + Suspense
(chunk-BO2N2NFS.js), so raw text paints first and highlighting streams in.
So the null-then-callback path reliably produces highlighted output.
- Add streamdownCodeHighlight.test.tsx: renders MessageResponse (which uses
STREAMDOWN_PLUGINS with code: lazyCodePlugin) on a fenced code block,
asserts raw code shows immediately, then waits for the lazy @streamdown/code
import + callback and asserts multiple per-token colored spans appear
(Streamdown colors tokens via the --sdm-c CSS custom property).
- Harden highlight() against double callback invocation with a fire-once guard
so the callback runs exactly once whether the real plugin resolves via its
return value (sync cache hit) or its own callback. Add a unit test asserting
the callback fires exactly once.
- Clarify supportsLanguage: Streamdown has zero call sites for it/
getSupportedLanguages, and highlight() falls back to "text" for unknown
languages, so the optimistic pre-load answer is safe.
Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com>
* test(e2e): assert chat code blocks lazy-load Shiki highlighting
Regression guard for the lazy-Shiki change: seeds a deterministic
assistant message with a fenced code block and asserts the observable
syntax-highlighted token spans appear once the on-demand Shiki import
resolves, proving highlighting survives the deferral.
Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com>
* style: apply ruff format to lazy-Shiki e2e test
`ruff format` collapses the multi-line `wait_for_function` string
concat onto one line; matches the pre-commit CI fix so the check
passes.
Co-authored-by: Isaac
* test(ui-snapshot): wait for lazy Shiki highlight before chat capture
The lazy-Shiki change defers `@streamdown/code`, so the fenced code
block first paints raw and only re-renders with syntax-highlighted
token spans once the on-demand import resolves. The visual snapshot
was capturing the pre-highlight frame, drifting from the committed
(highlighted) baseline and failing the UI Snapshot gate.
Wait for the `--sdm-c` token spans (same signal the lazy-Shiki e2e
test uses) before capture so the render is highlighted and matches
the existing baseline — no baseline regen needed.
Co-authored-by: Isaac
* test(ui-snapshot): update chat baseline for lazy-Shiki render
The lazy-Shiki change defers `@streamdown/code`; in the pinned headless
Playwright renderer the fenced code block paints uncolored even after the
token spans mount (confirmed across two CI runs — the DOM wait added last
commit does not repaint the colors at capture). Highlighting works in a
real browser, so this is a snapshot-environment artifact, not a UX
regression. Adopt the CI-rendered baseline (byte-identical to the gate's
render) so the visual gate matches, and keep the token-span wait so the
capture is the settled post-import DOM rather than a mid-tokenization frame.
Co-authored-by: Isaac
* test(ui-snapshot): fix chat snapshot flake on lazy Shiki highlight
The chat baseline flaked between highlighted and raw code renders. The
lazy `@streamdown/code` import mounts the colored token spans a frame
before the browser composites their colors, so waiting on span presence
raced the paint — the screenshot sometimes caught the raw frame.
Wait until the tokens resolve more than one distinct computed color (the
raw fallback is a uniform `inherit`), then flush two animation frames so
the colors are painted before capture. Restore the highlighted baseline
as the correct target (a prior commit had adopted a raced raw render).
Co-authored-by: Isaac
---------
Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com>
Co-authored-by: Daniel Lok <daniel.lok@databricks.com>
* feat(scheduled tasks): make workspace/host optional on create
Many scheduled tasks do no code work — research, summaries, chat-only —
so requiring a workspace and a connected host at create time is wrong.
Make both optional on CREATE. No schema/migration change: the DB columns
are already nullable.
- routes/scheduled_tasks.py: CreateScheduledTaskRequest.workspace and
host_id become optional (still reject empty strings). The router's
_validate_launch_inputs skips connected-host workspace validation when
BOTH are unset and returns a null canonical workspace; supplying just
one of the pair is still an error. PATCH is unchanged — it still cannot
null an already-set workspace/host_id.
- scheduled/fire.py: a fired task with neither host nor workspace creates
a default/no-workspace session and seeds its prompt as the opening user
turn (the no-host analog of the connected-host launch+dispatch), instead
of recording a failed run. A task that pins a host_id (with or without a
workspace) stays on the honest connected-host path and still records a
skipped/failed run when that host is missing or offline.
- tools/builtins/scheduled_tasks.py: drop workspace/host_id from the
sys_scheduled_task_create required list; they remain optional properties.
Normal POST /v1/sessions is unchanged — the shared session-create
validation and the sessions route still require a workspace.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* fix(scheduled tasks): resolve owner's live host when host unset (rework)
Rework of the optional-workspace/host semantics: an unset host_id no
longer means "run hostless" — it means "run on the owner's live host,
whichever it is". The prompt always runs on real compute.
- Unset host_id: resolve the owner's most-recently-active ONLINE host at
fire time (host_store.list_hosts(owner) + host_registry; v1 first-online
tiebreak). No online host, or no host store/registry, records a failed
run (no_online_host / host_registry_unavailable) — never a silent no-op.
- Unset workspace: default to the host's HOME, canonicalized to an
absolute realpath via a host.stat of '~' (_resolve_default_workspace).
The stored conversation row never holds a literal '~'; an unresolvable
HOME records a failed run (default_workspace_unresolved).
- Removed the hostless seed-prompt dispatch path; every fire goes through
connected-host launch+dispatch. Resolution produces an effective task
(dataclasses.replace) threaded through preflight/validate/create/dispatch
and is never written back to the stored row.
- Pinned-host tasks are unchanged (offline still skipped/failed); the API
partial-binding rejection and PATCH rules are unchanged.
Fixes two /review MAJOR findings from the rework:
- literal '~' persisted where an absolute realpath is contracted → now a
canonical absolute path via host.stat.
- os_env.cwd boundary bypassed for a defaulted workspace → workspace
validation is gated on the resolved effective.workspace, so a defaulted
HOME outside a boundary-pinned agent records a failed run, matching
POST /v1/sessions.
Tests: 101 passed across the scheduled fire/routes/tool-dispatch and
scheduler-lifespan suites; ruff clean.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* docs(scheduled tasks): correct optional host/workspace wording to resolve-live-host
Doc-only. The tool description, workspace/host_id schema property text,
and the route request comment + _validate_launch_inputs docstring still
described the pre-rework hostless design ('fires as a default/no-workspace
session', 'omit both for research/summaries/chat-only', 'needs neither a
workspace nor a connected host'). After the rework an unset host_id
RESOLVES the owner's online host at fire time (a failed run is recorded if
none is online) and an unset workspace defaults to that host's home dir —
it is not hostless. Reword the surface text to match. No logic change.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* feat(scheduled tasks): allow pinned host without workspace (default to host HOME)
Workspace is now ALWAYS optional. A task may pin a host but omit the
workspace — e.g. a task that only talks to an MCP (PagerDuty, etc.) needs
no code directory. The workspace defaults to the launch host's home
directory whether the host was pinned OR resolved from the owner's live
hosts at fire time.
The four combos:
- host none + workspace none → resolve owner's live host, default workspace to HOME.
- host set + workspace set → run there (workspace validated at create).
- host set + workspace none → run on the pinned host, default workspace to HOME. (was 400; now allowed — the fix.)
- host none + workspace set → still 400 (a path with no machine is meaningless).
- routes/scheduled_tasks.py _validate_launch_inputs: short-circuit to a
null canonical workspace whenever workspace is None (host set or not),
skipping validate_existing_host_workspace (which raises on a null
workspace). Only workspace-without-host stays a 400. Agent + model/effort
validation still run.
- scheduled/fire.py _resolve_effective_task: the HOME default already
applies to a pinned host (host_id kept, workspace resolved to canonical
HOME); docstring clarified that a pinned host is not re-resolved.
- tools/builtins/scheduled_tasks.py: tool + property text note workspace is
always optional and a host may be pinned without one.
Shared _session_create_validation.py / sessions.py untouched — normal
POST /v1/sessions still requires a workspace.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* fix(scheduled tasks): check pinned-host ownership before stat RPC
When a task pinned host_id but omitted the workspace, _resolve_effective_task
issued a host.stat of '~' to the pinned host to derive the default workspace
BEFORE the ownership check (which lived in the preflight, run after
resolution). A task pinning another owner's online host would thus dispatch a
stat RPC to a host it doesn't own on every fire — the preflight then correctly
rejected it (host_not_owned, no session, path not leaked), but the RPC had
already gone out.
Reorder, not new validation: extract the existence + ownership check into a
shared _authorize_pinned_host helper (a local host_store.get_host read — no RPC
to the host) and call it for a PINNED host before _resolve_default_workspace.
The preflight reuses the same helper. A resolved host (host_id was unset) is by
construction the owner's own, so its path is unchanged and not double-checked.
Single-user / auth-disabled (owner_user_id None) behavior is unchanged — the
owner check is skipped, matching the preflight.
Net: for a pinned host, ownership is authorized before any RPC reaches it;
owned/valid hosts behave exactly as before.
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* fix(scheduled tasks): authorize pinned host at create even when workspace omitted
_validate_launch_inputs returned early the moment workspace was None,
before any host authorization ran. So a scheduled-task create/PATCH with
host_id set but no workspace persisted the host_id without verifying the
caller owns it or that it exists (200), and a bad reference only surfaced
as a failed run at fire time.
Authorize a pinned host (existence + ownership) BEFORE the workspace-None
early return, reusing the same resolve_host_owner the workspace-present
branch already calls inside validate_existing_host_workspace (whose
semantics fire.py:_authorize_pinned_host mirrors) so create-time and
fire-time authorization cannot drift. It is a LOCAL store read only — no
host.stat / workspace RPC — preserving the no-workspace contract (workspace
defaults to host HOME at fire time). Single-user / auth-disabled mode still
skips the owner check (existence is still enforced), matching the fire path.
A nonexistent host now 404s and a non-owned host 403s at create; PATCH is
covered via the shared helper. Updates the test that asserted the old 200,
adds nonexistent/non-owned create cases and a PATCH-adds-host case, and
keeps the fire-path late-failure backstop tests.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* style: ruff-format test_desktop_update.py (whole-repo pre-commit gate)
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
---------
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
Three tables stored the same session-owner Databricks identity under
different column names and widths. hosts.owner (VARCHAR(256)) and
scheduled_tasks.owner_user_id are renamed to user_id (VARCHAR(128)),
matching user_daily_cost.user_id and the schema-wide identity
convention (session_permissions.user_id, account_tokens.user_id,
device_grants.user_id).
The change is confined to the DB + Python layer: the JSON API keys
("owner", "owner_user_id") are preserved at the route boundary, so the
HTTP contract, OpenAPI, SDKs, and web UI are unaffected.
Migration b3c1a2d4e5f6 renames both columns (narrowing hosts.user_id
256->128), swaps uq_hosts_workspace_owner_name ->
uq_hosts_workspace_user_id_name and ix_scheduled_tasks_owner_user_id ->
ix_scheduled_tasks_user_id, with a full downgrade. Verified
up/down/data-preservation on SQLite.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
## Related issue
N/A
## Summary
The Electron build workflow could not install the web dependencies because it used strict peer resolution against a lockfile generated with legacy peer handling. Use `--legacy-peer-deps` consistently with the web lockfile generation and other web CI jobs.
## Test Plan
- `cd web && npx --yes --package npm@11.12.1 npm ci --legacy-peer-deps --no-audit --no-fund`
- `cd web && npm run build:overlay`
- `uv run pre-commit run --files .github/workflows/electron-build.yml`
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [x] Test / CI
- [ ] Breaking change
## Test coverage
- [ ] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Verified the install with CI's pinned npm 11.12.1 and built the update overlay successfully. This workflow-only correction does not require a new automated test.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
N/A
## Summary
Desktop update UX is moved out of the server-rendered web bundle into the
Electron shell, so an update notification shows regardless of the connected
server's web-bundle version (an older server that predates the in-page banner
no longer leaves the desktop app unable to say it's out of date).
- Shell-owned overlay: a transparent, frameless child window (per shell window)
renders the SAME `UpdateBanner` component (reused, not duplicated) built into
`electron/overlay/` via a standalone Vite entry. It sizes to the card via
ResizeObserver height reports and collapses to a 1px click-through sliver when
empty (never `hide()`, so the renderer keeps laying out and can re-appear).
- Banner-safe server-page bridge: `preload.js` collapses
available/downloaded/error-security to `idle`, so no web bundle — including
older ones still mounting the in-page banner — can show a duplicate; Settings
still reads/writes update prefs and surfaces check errors.
- Menus: "Check for Updates…" and "Restart to Update" (with native up-to-date /
failed / nothing-ready dialogs) live under the production Server menu;
notification sounds + DevTools fold into a dev-only Debug menu.
- Security: `forceDevUpdateConfig` is derived from `!app.isPackaged` (env var
removed) so a packaged build can never be redirected to the HTTP dev feed.
- In-app theme is mirrored to `nativeTheme` (setColorScheme IPC) so the overlay,
native dialogs, and menus follow the theme switcher, not just the OS.
- Feed: publish provider points at the omnigent.ai generic feed; the build
workflow uploads `latest-linux.yml` / `latest.yml`. The overlay is built
automatically before dev/packaging via `prebuild:*` hooks.
## Test Plan
- `npm test` in web/electron — 218 pass.
- `npx vitest run` for UpdateBanner / SettingsPage / settingsNav — pass.
- `npx tsc -b` clean; `npm run build:overlay` produces the island.
- Manual: ran the unpackaged app against a local fake feed (127.0.0.1:8765
advertising 0.6.1); confirmed the overlay appears, re-appears across repeated
checks (root-caused a hidden-window ResizeObserver stall and fixed it), the
in-page top banner stays suppressed, and "Check for Updates…" shows the native
up-to-date / failure dialogs.
## Demo
N/A — desktop overlay; verified manually (see Test Plan). No media captured in
this environment.
## Type of change
- [ ] Bug fix
- [x] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Unit tests cover the updater main-process wiring and the UpdateBanner states.
The windowed overlay (positioning, show/collapse, theme) was verified manually
against a local fake feed, since it can't be exercised headlessly.
## Changelog
Desktop update notifications now appear in a native corner toast that works
regardless of the connected server's version.
## Follow-up review fixes
- Overlay lifecycle: explicitly `destroy()` the child overlay when its parent
shell window closes (Electron does not auto-close child windows, so it would
otherwise be orphaned with live IPC handlers).
- Production install path: "Restart to Update" moved into the production Server
menu (not just the dev-only Debug menu) so a user who dismisses the toast can
still install a downloaded update; surfaces a native dialog when nothing is
ready instead of silently no-op'ing.
- Overlay build: `publicDir: false` in the overlay Vite config so the ~150KB of
PWA icons / favicon from `web/public/` are no longer copied into the shipped
`electron/overlay/` bundle.
- Theme on reload: push the live `nativeTheme` theme on every
`did-finish-load` (not just on `nativeTheme` changes), so Cmd+R on the overlay
no longer reverts to the stale OS theme captured in the `?theme=` URL param.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
- Move the optional filesystem probe off the runner startup path
- Deduplicate setup across processes and linked worktrees
- Keep runner and workspace registry initialization explicit and idempotent
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* fix(telemetry): track sdk harness name in SessionCreatedEvent
SDK sessions (claude-sdk, openai-agents, codex, etc.) previously emitted
`harness: null` on the SessionCreatedEvent because only native agents have
a `native_agent.harness` attribute. Fall back to `_resolve_harness(conv)`,
which already handles harness_override and spec lookup, so every harness
kind is now represented in telemetry.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* deps(policies): migrate CEL evaluation from cel-expr-python to cel-python
cel-expr-python had no wheels for Linux aarch64 or macOS x86_64, requiring
a platform conditional in pyproject.toml and graceful degradation. cel-python
(cloud-custodian/cel-python) is pure Python and ships on all platforms.
- Replace cel-expr-python with cel-python>=0.5 (unconditional dependency)
- Rewrite omnigent/policies/builtins/cel.py to use the celpy API:
- celpy.Environment() + env.compile() + env.program() for compile phase
- prog.evaluate({"event": celpy.json_to_cel(event)}) for eval phase
- CELParseError / CELEvalError for specific exception handling
- Direct MapType key lookup (key in result / result[key]) rather than
converting the whole map to strings
- Remove platform restriction notes from deploy READMEs
- Update NOTICE attribution URL
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* chore: update uv.lock and apply pre-commit fixes for cel-python migration
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The managed-host launch-token auth path no longer needs a token_hash
index. The tunnel endpoint is /hosts/{host_id}/tunnel, so the connecting
peer already names the host it claims to be — resolve_launch_token now
seeks the row by the (workspace_id, host_id) primary key and compares the
stored digest to the presented token's digest with hmac.compare_digest
(constant-time, preserving the no-timing-oracle property).
Drops uq_hosts_token_hash (workspace_id, token_hash). Its uniqueness was
never load-bearing — launch tokens are 256-bit secrets.token_urlsafe(32)
values whose digests do not collide in practice — and nothing rides it now
that the lookup keys on the PK.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* fix(web): trust server session.status so "Working…" clears on idle
The main chat's "Working…" indicator reads only `sessionStatus`, but the
`session.status` handler dropped a bare `idle` (no responseId) whenever an
`activeResponse` was still `streaming` — deferring to `response_end` to own
the lifecycle. `response_end` only sets the local `status`/`activeResponse`,
never `sessionStatus`, so when that guard fired nothing ever cleared the one
field the indicator reads. On a fresh session the first-turn wrapper-response
id mismatch leaves `activeResponse` stuck `streaming`, so the turn's genuine
terminal `idle` was eaten and the shimmer stayed lit even though the server,
sidebar, and local status all reported idle.
Remove the guard so `sessionStatus` tracks the server's session-level status
1:1. The idle heuristic now lives in exactly one place — the runner's
PTY-activity watcher — instead of being split between server and client. The
bubble lifecycle (`status`/`activeResponse`) still defers to `response_end`,
independently of the session-level status.
Co-authored-by: Isaac
* test(e2e-ui): cover Working indicator clearing on a bare server idle
The E2E UI gate requires a tests/e2e_ui/** test covering the visible chat
behavior this branch changes. Add a Playwright test that drives the exact
edge shape the claude-native PTY-activity watcher emits on a plain turn — a
turn-start `running` carrying a `response_id` (opening the streaming
`activeResponse`), then a trailing bare `idle` with no `response_id` — and
asserts the "Working…" indicator clears. This is the case the removed
dropped-idle guard covered; before the fix the indicator stayed lit forever.
Verified the test fails with the old guard restored and passes with the fix.
Co-authored-by: Isaac
* fix(telemetry): track sdk harness name in SessionCreatedEvent
SDK sessions (claude-sdk, openai-agents, codex, etc.) previously emitted
`harness: null` on the SessionCreatedEvent because only native agents have
a `native_agent.harness` attribute. Fall back to `_resolve_harness(conv)`,
which already handles harness_override and spec lookup, so every harness
kind is now represented in telemetry.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* style: reformat harness ternary in SessionCreatedEvent telemetry
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
The in-memory host registry keyed live connections by host_id alone,
but a host_id is only unique within a workspace — the hosts table PK is
(workspace_id, host_id). A BYO/local host has a stable config.yaml
host_id, so a user who belongs to multiple workspaces and points that
host at more than one presents the same host_id to each.
Keyed on host_id alone, the second workspace's connect treated the
first's healthy tunnel as stale: it evicted the entry (newest-wins) and
poisoned the first connection's outbound queue, so that workspace's host
operations then failed with "connection was replaced". Without host-
tunnel replica affinity, routing could also resolve the wrong
workspace's tunnel for the same host_id.
Key the registry by (workspace_id, host_id) to mirror the DB PK. The
workspace defaults to current_workspace_id() — 0 in single-tenant/OSS,
so behavior there is unchanged — and is captured into HostConnection at
register time so the long-lived sender loop's send_text guard never
reads request context. Every call site is already request-scoped, so no
call-site changes are needed; the change is contained to host_registry.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
The `policies` table carried three overlapping secondary structures that
didn't pull their weight: `ix_policies_created_at` matched no query,
`ix_policies_session_id` and a scope-less listing left `list_defaults`
scanning every session row to find the handful of global policies, and a
`uq_policies_session_id_name_cksum` unique constraint that only enforced
session-name uniqueness (default-name uniqueness was already app-enforced).
Collapse the two listing indexes into one combined
`ix_policies_scope_session (workspace_id, scope, session_id, id)`. `scope`
leads `session_id` so `list_defaults` (WHERE ws + scope='default') seeks the
prefix and `list_for_session` (WHERE ws + scope='session' + session_id) seeks
the full key — `list_for_session` gains a `scope='session'` predicate so it can
reach `session_id` in the key (proven via EXPLAIN QUERY PLAN; without it the
planner table-scans). `created_at` is deliberately omitted: with `session_id`
between `scope` and `id` it cannot cover the `ORDER BY created_at, id` for both
queries, so both sort their small result set in memory (as the session listing
already did).
Drop the `uq_policies_session_id_name_cksum` unique constraint and enforce
session-name uniqueness in the store (`create`/`update`), mirroring the
existing default-policy path. The session-policy PATCH route now maps a rename
collision to 409. Net: one fewer index maintained per write, no DB constraint,
same seek performance on both reads.
Migration d4c1b9e6f3a2 (off a7f3c1b9e2d4).
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
- Send versioned launch metadata with the session-init handshake
- Share initialization across tunnel callbacks and first-turn dispatch
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
Bump omnigent-desktop-electron from 0.3.0 to 0.6.0 in web/electron/package.json and package-lock.json. The shell reads its version dynamically via Electron's app.getVersion() (sourced from package.json#version), so no source, build-config, or updater changes are needed.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
The Telemetry disclosure section added in #2934 (5fd0012f) was accidentally
removed by #2933 (c555ba9c), which deleted it in the same diff that added the
Configuration section. Restore the Telemetry section verbatim between "Write
your own agent" and "Contributing", and remove the Configuration section.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Widen the comments PK from (workspace_id, id) to
(workspace_id, conversation_id, id) and drop the now-redundant
ix_comments_conversation_id index (workspace_id, conversation_id,
created_at, id).
The (workspace_id, conversation_id) prefix the secondary index shared
with the PK is now carried by the PK itself, so it backed the
per-conversation reads (list_for_conversation, the fingerprint
aggregate, the cascade delete) purely as write/space overhead. Its one
extra job -- feeding list_for_conversation's ORDER BY created_at, id an
index-ordered scan -- is given up for a filesort over the small
per-conversation comment set.
The three store point-lookups (get/update_comment/delete) already
receive conversation_id, so they now key on the full PK tuple instead of
fetching by (workspace_id, id) and filtering conversation_id in Python;
the lookup itself enforces the conversation scoping.
Migration a7f3c1b9e2d4 (off z9a2b3c4d5e6) is a pure key change:
conversation_id is already NOT NULL and populated, so no backfill.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
`ix_files_created_at` on `files` (workspace_id, created_at, id) only served
a session-less listing (WHERE workspace_id ORDER BY created_at, id), and
nothing issues that query. Every read of a session's files goes through
`FileStore.list(session_id=...)` — the agent `list_files` tool (in-process
and runner-proxied over GET /v1/sessions/{id}/resources/files) and the
session-resources route — all of which filter by session_id and are served
by `ix_files_session_id_created_at`. Global (session_id IS NULL) files are
only surfaced via the `include_unscoped` OR query, which also rides the
session-scoped index.
Since the global listing had no caller, `FileStore.list` now requires
`session_id` (the `session_id=None` branch that produced the unindexed
query is removed), and migration c3e8f1a9d2b7 drops the index.
`ix_files_session_id_created_at` is unchanged.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
Add rahulrav1 to the canonical maintainer roster in .github/MAINTAINER. This grants merge-approval, the skip-security-scan waiver, and e2e-approved permissions per the existing workflows.
A journey's setup ran unwrapped inside run_latency/run_throughput, so a
transient 500 there (e.g. _setup_target_session's raise_for_status) propagated
up and aborted the whole benchmark suite mid-run. Separately, a run in which
every operation failed contributed all-zero latencies to the summary averages,
so a failed run masqueraded as an infinitely fast one and skewed the reported
numbers toward zero.
- journeys.py: catch setup failures and record them as a single failed run
(`setup: HTTP 500`); suppress teardown failures; unify per-op failure
classification in `_failure_reason`.
- measure.py: aggregate() and check_thresholds() average only runs with a
successful sample; summaries gain runs_total/runs_ok and omit metric keys
when every run failed. print_results matches and notes excluded runs.
- run.py: outer per-journey safety net — any other unexpected error records a
`skipped` block and the suite continues. A no-successful-sample journey fails
the CI gate only when a threshold was supplied.
- compare.py: report skipped/all-failed journeys as `skipped` rather than a
spurious -100% improvement.
- schema.py: bump SCHEMA_VERSION 3 -> 4; update sample_output.json + README.
Co-authored-by: Isaac
test_build_report_contains_required_fields pinned the expected version line
to "omnigent 0.6.0.dev0". The 0.7.0.dev0 bump (#2950) left it stale, so the
misc pytest shard fails on main and every branch cut from it. Assert against
`omnigent.version.VERSION` so the check tracks the real version and does not
break on future bumps.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* ⚡ perf(auth): Reuse delegated runner credentials
- Exchange host launch binding tokens for short-lived owner bearers before resolving user credentials.\n- Share runner auth with Claude and refresh hook snapshots without exposing the binding token.
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* ♻️ refactor(auth): Address review feedback
- Avoid logging bridge paths and collect cancelled refresh tasks explicitly.\n- Inject the refresh interval so tests use the existing direct import style.
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* fix runner auth fallback behind Apps proxy
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* perf(auth): bootstrap runners with host bearer
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
* docs(api): regenerate OpenAPI schema
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
---------
Signed-off-by: Daniel Lok <daniel.lok@databricks.com>
dev/benchmarks/omnigent/seed.py seeded the benchmark corpus through the
production store ORM API one row at a time (~2M single-row INSERTs, ~20k
commits, each preceded by throwaway PRAGMAs on session open), taking ~6-10
min on CI. The benchmark only measures the store read path, so the write
strategy does not taint what's measured provided the resulting corpus is the
same shape.
Add a SQLAlchemy Core bulk-insert fast path (_seed_via_core) that writes the
whole corpus in one transaction via ~10 batched executemany flushes (1 commit
instead of ~20k). It uses the ORM Table objects so Uuid16 binds bare-hex to
16 bytes byte-identically to the store, computes title_hash explicitly
(Python defaults don't fire under executemany, and sets all kind/status
columns explicitly. The schema at head carries no FK constraints (migration
p1a2b3c4d5e6 dropped them all), so insert order is free under
PRAGMA foreign_keys=ON.
Dialect-gated: SQLite uses the fast path; every other dialect (e.g. the
nightly Postgres benchmark) falls back to the existing store-API loop
(_seed_via_store), extracted verbatim, so behavior there stays identical.
Byte-stable: same RNG seed/counts/_FRAGMENTS, same generate_*_id calls, same
per-session draw order (title first, then items), same 0-based position
allocation, same label stamped on the last session, same _meta_value config
string. Item data/search_text are built byte-identical to
MessageData.model_dump(exclude_none=True) + extract_search_text (the slow
path keeps _make_items as the single source of truth). The fast path item
build bypasses pydantic (building plain dicts) to keep the 1M-item Python
phase cheap; a byte-stability test pins both paths to identical corpora.
Idempotency preserved: the reuse-skip check, --reseed, and --print-head work
unchanged; ensure_user(local) and the seed-meta label upsert are mirrored
via sqlite_insert.on_conflict_do_*.
Target: ~20-30s end-to-end (was ~6-10 min) for the 5000x200 corpus; measured
~27s locally. Scope: seed.py + a new test file only; no product store/db code
under omnigent/stores/ or omnigent/db/ touched.
EOF
)
* feat(routing): server-side smart routing via external routes:select gateway
Adds a GatewayRoutingClient that implements the existing RoutingClient
protocol by calling an external routes:select gateway (the Databricks
AI-Gateway routing service, or any endpoint speaking the
omnigent.api.routing.v1 proto). Because every frontend — CLI, web UI,
SDK, the native-harness forwarders, and child sessions — already routes
through the server's route_turn() chokepoint, swapping the routing
client covers all of them with no per-client code and no web changes.
Server config selects between two mutually-exclusive providers via a new
routing: block (gated on OMNIGENT_SMART_ROUTING=1 as before):
routing:
provider: gateway # or "llm" (default, existing built-in judge)
base_url: https://<host>/ai-gateway/routing/v1
router_name: task_v0
profile: <databricks-profile> # optional; mints a bearer for the gateway host
Candidate models come from the server's live catalog (the same
available_models the built-in judge receives), mapped to proto
route_options; the SelectRouteResponse maps back to a RoutingResult.
Requests use snake_case proto3-JSON (preserving_proto_field_name=True).
A gateway error or empty selection returns None so the turn proceeds on
the agent's default model.
Routing is gated per-session by the existing cost_control_mode_override
switch (the web UI's "Intelligent model" toggle). The CLI had no way to
set it, so this adds a /route on|off slash command (and the SDK
set_cost_control_mode + Session.cost_control_mode_override plumbing it
needs); turning routing on clears any pinned /model override in the same
PATCH, matching the web client.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* refactor(routing): rename GatewayRoutingClient to ExternalRoutingClient
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* refactor(routing): drop CLI /route toggle; keep ExternalRoutingClient for parity
Tables the CLI-side cost-control enablement (the /route slash command and
its SDK set_cost_control_mode / Session.cost_control_mode_override
plumbing). Scope is now feature parity with today's routing: the server
can route via an external routes:select gateway (ExternalRoutingClient +
routing: config), gated per-session by the existing
cost_control_mode_override switch that the web UI toggle already sets.
Enabling routing from the CLI can come later.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* refactor(routing): add ROUTES_SELECT_PATH constant; provider "external"
- Extract the "routes:select" custom-method path to a ROUTES_SELECT_PATH
constant in smart_routing.py.
- Rename the config provider value "gateway" -> "external" (routing.provider:
external) and update prose/logs to say "external"/"router" instead of
"gateway" (the Databricks AI-Gateway product name and its URL path are
kept where they refer to the real endpoint).
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* refactor(routing): split _build_routing_client into per-provider helpers
_build_routing_client is now a thin dispatcher on routing.provider,
delegating to _build_external_routing_client and
_build_local_llm_routing_client. Behavior unchanged.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* refactor(routing): inline provider dispatch; drop _build_routing_client
The provider selection (routing.provider -> external vs llm) now lives
inline at the server startup call site, calling
_build_external_routing_client / _build_local_llm_routing_client
directly. Behavior unchanged.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* refactor(routing): simplify provider dispatch at startup
Collapse the provider-selection block to a single condition: an
``external`` provider requires ``routing.provider == "external"``;
anything else (no block, other/missing provider) falls through to the
built-in llm judge, preserving the OMNIGENT_SMART_ROUTING + llm: parity.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* refactor(routing): flatten external routing-client config parsing
Normalize base_url/router_name/profile with (x or "").strip() up front so
the validation collapses to plain `if not base_url or not router_name`.
Drop the dead isinstance(dict) guard (the caller guarantees a dict) and
its now-invalid test.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* test(routing): merge redundant missing-field cases into one test
base_url and router_name are validated by a single condition now, so
fold the two separate missing-field tests into one.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* feat(routing): config-driven model_prefix + log gateway error bodies
ExternalRoutingClient now round-trips model ids through a per-request
router_id -> local_id map: it applies an optional, config-declared
model_prefix (routing.model_prefix, default empty) to strip a
deployment's catalog prefix on the way out and restore the exact catalog
id on the router's answer. No provider is hardcoded in core — an
unconfigured deployment sends catalog ids verbatim, so OSS/non-Databricks
setups (bare model ids) work unchanged. A Databricks workspace whose
serving endpoints are named "databricks-<model>" sets
model_prefix: databricks- to match a router (e.g. task_v0) that keys on
bare ids.
Also split routes:select error handling so the gateway's response body
is logged on 4xx/5xx (the actual reason, e.g. task_v0's required-model
error) instead of a bare status code, and surface transport/parse
failures at warning level.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* feat(routing): add provider-agnostic routing.api_key auth option
routing.profile is Databricks-specific. Mirror the llm: block by adding
an env-expandable routing.api_key: an explicit bearer token (${ENV}
expanded) that takes precedence over profile, else the Databricks profile
convenience, else unauthenticated. Non-Databricks deployments can now
authenticate an external router without a Databricks CLI profile.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* refactor(routing): use click.echo for config warnings, drop lone _logger
Match cli.py's house style (click.echo(..., err=True)) for the two
routing-config warnings instead of introducing the file's only
logging.getLogger. Behavior unchanged.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
* feat(routing): multi-prefix model map + validate router pick against candidates
Address review feedback on external routes:select routing:
- model_prefix accepts a list (or scalar) so multiple catalog prefixes
(databricks-, system.ai.) can be stripped; first match wins.
- key the router-id -> local-id map on (harness, router_id) so the same
bare model id served under different harnesses (Databricks-authed PI vs
a Codex subscription) maps back to distinct local ids.
- validate the router's returned model against the candidate set we sent,
like the built-in judge: an out-of-set pick returns None instead of being
persisted as the session's model_override.
Co-authored-by: Isaac
Signed-off-by: Lilly <lilly.gray@tecton.ai>
---------
Signed-off-by: Lilly <lilly.gray@tecton.ai>
Co-authored-by: Lilly <lilly.gray@tecton.ai>
Three release-workflow bugs that blocked the 0.6.0rc1 release. Real CI on
the base commit was green in all cases — the failures were self-inflicted.
1. Assert-green-CI gate self-poisoning. The gate queried the base SHA's
check-runs and failed on any non-green run, but counted check-runs produced
by THIS workflow (plan, benchmark, cut, bump-main, …). A single premature
failure on a prior dispatch left a failure conclusion on the SHA and
poisoned every later dispatch in a self-sustaining loop.
Fix: exclude every check-run belonging to a release.yml run (identified by
workflow run ID in details_url, not by job name — so a real nightly
`benchmark` regression from a different workflow still gates). One-shot
fail-fast design preserved.
2. benchmark ModuleNotFoundError. The benchmark job's first `uv run --no-sync`
ran seed.py before any `uv sync`, so the venv had no deps and `import yaml`
died. The sync was buried later, too late for the seed steps.
Fix: add one `uv sync --extra dev` up front (the "sync once" half of the
repo's existing --no-sync pattern), matching benchmark.yml/benchmark-pr.yml.
3. Baseline benchmark fails across schema boundary. The baseline step checked
out the previous release tag and booted its server against a bench.db seeded
by the current (newer) code. The DB was at the newer Alembic head; the older
server didn't know that revision (migrations are forward-only) → server
died → 90s health-check timeout.
Fix: seed at the OLDER release's schema head instead. The baseline (older
code) reads it natively; the candidate (newer code) auto-migrates it forward
on startup. Reordered the benchmark job: find the previous tag first, then
seed + run baseline at the older schema, then re-sync and run the candidate
(which migrates the same bench.db forward). Removed the seed cache (the cache
key was scoped to the newer schema head, which no longer matches the seed
point; the separate seed-perf PR will make seeding fast enough not to need it).
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Convert the three remaining raw TEXT columns — policies.handler,
policies.factory_params, and hosts.configured_harnesses — to
CompressedText (a transparent zstd-compressed BLOB) so they satisfy the
no-TEXT/MEDIUMTEXT schema rule and stay 1:1 with the managed USM schema.
These columns hold opaque handler paths / machine-generated JSON and are
never used in a SQL predicate, so storing them as a compressed byte frame
is safe. The Python type stays `str`, so stores and callers are unaffected.
Migration z9a2b3c4d5e6 mirrors z4a2b3c4d5e6 (TEXT->LargeBinary on upgrade,
no backfill; downgrade decompresses each value then restores TEXT). Its
downgrade addresses each row by that table's real PK column — hosts keys
on host_id, not id.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* fix(repl): treat /model show|list|status|current as display, not a switch (#2779)
Typing /model show (intending to display the current model) was parsed as
a switch to the literal model id 'show', persisting it as model_override and
breaking every subsequent turn with no UI way to recover. Route the display
keywords show/list/status/current to the same readout as bare /model instead
of setting an override.
Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com>
* ♻️ refactor(repl): Simplify model command tests
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
---------
Signed-off-by: simtsc <2539637+simtsc@users.noreply.github.com>
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
The automatic "auto-title" rename asks the model to call
sys_session_rename on the first turn of every fresh session — an extra
model round-trip that slows every new session. Gate it behind
OMNIGENT_SESSION_RENAME, defaulting to off, so the feature ships
disabled out of the box while keeping the implementation (tool
registration, dispatch, the auto-title endpoint) intact. The manual
"Rename" sidebar item is unaffected.
session_rename_instruction() and session_rename_allowed_tools() are the
single canonical gate both the Claude-native launcher and the shared
runner consult; returning None / () there suppresses the instruction
and empties the tool preapproval everywhere.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
ix_scheduled_tasks_state (workspace_id, state, created_at, id) on
scheduled_tasks does not earn its keep. Its per-workspace query shape --
WHERE workspace_id AND state ORDER BY created_at, id (list_active) -- has no
production caller; the scheduler reads active tasks exactly once at boot via
list_active_all_workspaces (WHERE state ORDER BY workspace_id, created_at,
id), which is a near-full scan regardless.
ix_scheduled_tasks_created_at (workspace_id, created_at, id) already serves
that boot read: scanning it yields the exact ORDER BY workspace_id,
created_at, id the query wants, with state applied as a residual filter. The
residual check is free here because the store selects whole rows (state is
already loaded), and scheduled_tasks is low-cardinality (a handful of tasks
per user, and delete is a hard delete so no deleted rows linger) -- nothing
meaningful to skip. So the index is pure write/space overhead.
The state column and its ck_scheduled_tasks_state check constraint are
unchanged -- only the index is removed. Index-only, no data change; DROP is
native on every dialect and the downgrade restores it.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
In #2605 the `memory` optional-dependency extra was renamed to `hindsight`
without keeping the old name around, making `omnigent[memory]` / `--extra
memory` silently install a nonexistent extra. Re-add `memory` as an alias
extra pulling the same `hindsight-client` so existing install commands keep
working. Scheduled for removal in 0.70 (TODO).
Add a polymorphic `harness:` key in config.yaml — a scalar (legacy) or a
mapping with `default` plus per-harness `command`/`args` overrides. The
legacy scalar form still works and auto-migrates to the mapping form on the
next config write.
Harness binary-path precedence: `OMNIGENT_<NAME>_PATH` env var >
`harness.<id>.command` config > built-in default. `args` follow the same
precedence with config args as the base and CLI pass-through args appended.
Env-var standardization: `OMNIGENT_<NAME>_PATH` (base id, `-native` suffix
stripped) is the canonical per-binary override, unifying the headless
`HARNESS_*_PATH` and native `OMNIGENT_*_PATH` conventions into one namespaced
name. The env var keys off the underlying binary, not the harness id, so
`claude-sdk` (which runs the `claude` CLI) shares `OMNIGENT_CLAUDE_PATH` with
`claude-native`.
The legacy `HARNESS_<NAME>_PATH` (codex/pi/kimi/goose/qwen/hermes) is still
read as a deprecated fallback — a one-time runner-side log warning when it
provides the value, plus a terminal-visible CLI startup notice for
interactive invocations. Slated for removal in v0.8.0.
The pre-existing `omnigent claude --command` flag is deprecated (warns on
use, pointing to `OMNIGENT_CLAUDE_PATH`/config) and will be removed in a
future release. No other native command gained a `--command` flag —
override via env or config.
New module `omnigent/harness_startup_config.py` (leaf resolver, lazy-imports
the alias helper): `resolve_harness_config`, `resolve_harness_command`,
`resolve_harness_args`, `resolve_harness_path`, `config_harness_path_override`.
Config deep-merge of the `harness` mapping across global+local (per-harness
sub-keys). Write-side scalar→mapping migration with a one-time stderr notice.
`config set harness=<id>` deep-merges into existing overrides; `config list`
renders the default + notes overrides.
`args` wiring: the 11 native Click commands thread config args as the base
with CLI pass-through args appended (via `_resolve_harness_startup_args`).
The 7 env-resolver native commands (pi/cursor/kiro/goose/hermes/qwen/kimi)
thread `harness.<name>-native.command` config into `OMNIGENT_*_PATH` before
`_ensure_backend`. The 5 headless spawn-env builders (codex/pi/kimi/goose/qwen)
set `OMNIGENT_*_PATH` from config when ambient env is unset.
Signed-off-by: Zeyi Fan <zeyi.f@databricks.com>
ix_conversation_metadata_kind (workspace_id, kind, id) on
omnigent_conversation_metadata has no serving query. kind is fully
determined by parent_conversation_id nullness -- a child always has a
parent, a top-level session never does -- so list_conversations filters
kind on the AP conversations table (parent-nullness) and the sub-agent
roll-up (list_child_conversation_ids_by_parent) rides
idx_conversations_parent; neither reads the metadata kind column. kind is
also a 2-value column (kind IN (1, 2)), so a standalone index could never
be selective.
The kind column and its ck_conversation_metadata_kind check constraint are
unchanged -- only the index is removed.
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
## Related issue
N/A
## Summary
- Add a **Telemetry** section to the README disclosing that Omnigent collects
anonymized usage data by default, with no sensitive or personally
identifiable information.
- Link to the [Usage Telemetry](https://omnigent.ai/docs/deploy/telemetry)
docs page for opt-out instructions, and note that managed-service users
should consult their service agreement.
## Test Plan
- Previewed the rendered markdown locally; verified the section sits between
"Write your own agent" and "Contributing" and the docs link points to
https://omnigent.ai/docs/deploy/telemetry.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [x] 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
Docs-only change; verified by reading the rendered README diff.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Fold the 1-to-1 agent_configuration companion table back onto
conversations: agent_id returns as a first-class indexed column and the
four per-session overrides collapse into one nullable session_overrides
JSON blob (VARCHAR(512), NULL when the session uses all agent/spec
defaults).
The overrides were never filtered in SQL, so a blob loses no query
capability while dropping a table, an extra INSERT, the get_conversation
JOIN, and the paired-row repair/fork/delete plumbing. agent_id stays a
real indexed column (ix_conversations_agent_id) so the agent->conversation
reverse lookup and the agent_id / has_agent_id / agent_name list filters
stay index-backed.
- db_models: delete SqlAgentConfiguration; add agent_id + session_overrides
to SqlConversation; restore ix_conversations_agent_id.
- conversation store: add _encode/_decode_session_overrides; rewire
create/get/list/update/fork/switch/delete and the bulk reads onto the
merged row; drop the JOIN, batch-fetch, and missing-row repair logic.
Fix the id-collision -> ConversationAlreadyExistsError translation, which
had relied on the agent_configuration INSERT failing first.
- agent store: session-id reverse lookup reads conversations.agent_id.
- migration b7e4d2c9a1f3: reversible; ids are normalised to bytes in Python
so the copy is correct on SQLite/Postgres/MySQL regardless of the source
column's declared type (the split created it VARCHAR; conversations stores
ids as raw bytes).
Reverses bb2c3d4e5f6a.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
ix_conversation_items_conversation_id_position was UNIQUE on (workspace_id, conversation_id, position, created_at). The created_at tail only existed because a UNIQUE index must contain the partition key, and with it in the key the DB no longer enforced position uniqueness anyway (only per epoch-second). Strict position uniqueness is owned by the next_position allocator under _lock_conversation, which never reuses a position; no code path catches a position IntegrityError.
So the UNIQUE flag is redundant. Repoint the index to a plain (workspace_id, conversation_id, position): same access path for the dominant per-conversation position-ordered scan, one less uniqueness probe on the hot insert path, and created_at drops out (a non-unique index needs no partition key). The PK still carries created_at, so the table stays partition-ready.
Migration c7d2e9f4a1b8; index-only, no data change. Updates the three tests that asserted the old unique/created_at shape.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* feat(scheduled): real on_fire fire path + wire store into entrypoints
Replace the no-op _placeholder_on_fire with a real fire path
(omnigent/server/scheduled/fire.py): on firing, re-read the row (skip if
missing/non-active), create an owner-granted session bound to the task's
agent, launch its connected-host runner, dispatch the prompt, and record
the run — all fire-and-forget via asyncio.create_task so the scheduler
timer re-arms immediately. managed_sandbox targets are recorded as a
skipped run for now (connected_host only in v1).
Wire SqlAlchemyScheduledTaskStore into all three entrypoints (cli.py,
deploy/databricks, deploy/docker) so the scheduler actually starts.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* feat(scheduled): add /v1/scheduled-tasks CRUD routes
Owner-scoped CRUD for scheduled tasks (create/list/get/update/delete),
mirroring the hosts router. Create/update validate the RRULE via
validate_rrule (400 on invalid); every mutation keeps the live
ScheduledTaskScheduler in sync via add/update/remove. Mounted under /v1
whenever a scheduled_task_store is configured.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* feat(scheduled): add sys_scheduled_task_* MCP tools
Four agent-facing builtins — create/list/update/delete scheduled tasks —
always registered by ToolManager (no spec opt-in, like the policy tools).
The runner dispatches each to the /v1/scheduled-tasks REST endpoints via
server_client; RRULE validation and owner scoping stay server-side. Added
to the local-dispatch and native-relay tool sets so native harnesses see
them too.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* style(scheduled): ruff lint + format cleanup
Sort imports, drop unused imports, dict-literal, de-Yoda a condition,
wrap long tool-schema descriptions, and drop redundant None defaults —
no behavior change.
Co-authored-by: Isaac
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* test: allow scheduled task tools in manager schemas
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* Tighten scheduled task fire v1 scope
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* Trigger CI rerun
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* fix(scheduled): timezone validation, remove unused FireDeps.agent_store, fix _grant_owner docstring
- Validate IANA timezone on POST /v1/scheduled-tasks and PATCH
/v1/scheduled-tasks/{id}; an unrecognized timezone name returns HTTP 400.
- Remove FireDeps.agent_store: the field was declared but never read inside
fire.py. Updated the FireDeps constructor in app.py and test_fire.py.
- Correct _grant_owner docstring: permission_store=None is a no-op (auth
disabled), not a grant — the previous wording claimed the grant was never
skipped, directly contradicting the early-return on line 281.
- Add integration tests for invalid timezone on create and update.
Signed-off-by: Rahul Ravindranathan <rahul.ravindranathan@databricks.com>
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* Fix scheduled task validation and failure runs
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* Preserve scheduled workspace validation comments
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* Preserve session metadata validation comments
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* Remove scheduled fire v1 wording
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
* Fix scheduled fire races and scoping
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
---------
Signed-off-by: Rahul Ravindranathan <rahulnathan73@gmail.com>
Signed-off-by: Rahul Ravindranathan <rahul.ravindranathan@databricks.com>
The per-parent child-title unique index keyed on the wide title column (a 512-char prefix on MySQL, ~2 KB per entry on utf8mb4). Add a title_hash column holding sha256(title)[:16] and repoint the index at it, so entries are a fixed 16 bytes. The index keeps its name so the store's IntegrityError to NameAlreadyExistsError translation still matches; semantics are unchanged (two titles collide iff their 128-bit digests do, and only among siblings under one parent).
The ORM default stamps title_hash on INSERT and the store recomputes it on the two rename paths; the column is nullable so raw-SQL inserts that bypass the ORM default don't have to supply it. Migration a2b7c3d8e4f9 adds the column, backfills existing rows (keyset-batched Python, since SQLite has no sha256), and swaps the index.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
The two bare (workspace_id, <ts>, id) sort indexes on conversations are never the chosen access path: the sessions list is ACL-scoped (id IN (...)) and resolves via the PK, the default sidebar (archived=false, updated_at DESC) is served by ix_conversations_archived_updated, and sub-agent/root listings use their own indexes. Meanwhile updated_at is rewritten on every item append, so the index is pure write amplification.
Migration f4a1c8b2d3e6 drops both; downgrade recreates them.
Co-authored-by: Isaac
Signed-off-by: aravind-segu <aravind.segu@databricks.com>
* feat(ci): auto-assign the maintainer with most context on a feature blog
Mirror doc-sync's reviewer assignment, adapted for the multi-PR nature of a
feature blog: tally who merged the feature's contributing PRs (from pr_refs)
and request review from the most frequent merger — the maintainer with the
most context. Authors are the fallback (outside contributors may lack site
access; a maintainer always merges), bots and the CI identity are skipped.
The merger/author tally reuses the existing per-PR `gh` loop in Draft posts
(one extra `gh pr view --json mergedBy,author` per ref), writing the chosen
login to /tmp/reviewer_<idx>.txt. The Open-draft-PRs step @-mentions them in
the body (durable ping) and best-effort --add-reviewer/--add-assignee,
tolerating GitHub's 422 for non-collaborators.
Co-authored-by: Isaac
* fix(ci): write reviewer @-mention on the draft-PR update path too
Polly review: the force-push update path called assign_reviewer but never
refreshed the PR body, so an existing draft never got the durable @-mention.
Since --add-reviewer commonly 422s (the source-repo maintainer isn't an
omnigent-site collaborator), the mention is the only reliable ping — it must
land on both paths. Build the body once and `gh pr edit --body` it on update.
Also surface gh-pr-view failures in the merger tally with a ::notice:: instead
of swallowing them silently, so a systematic API failure isn't invisible.
Co-authored-by: Isaac
* feat(ci): auto-generate a hero image for each feature-blog post
The drafter now emits an IMAGE_PROMPT line describing a concrete visual scene
for the feature (subject only, grounded in the post content, no style words).
The workflow appends a fixed brand style suffix, calls the image model on the
same gateway host (databricks-gemini-3-pro-image), writes the PNG to
public/images/blog/<slug>.png, and rewrites heroArt to point at it.
- Content-driven: the subject comes from the feature the drafter just wrote
about, so every hero depicts that feature (not a generic mascot).
- Fail-soft: any error (no gateway/key, bad response, non-PNG) logs a warning
and leaves heroArt blank, so image generation never blocks a draft.
- No new secret: the image endpoint is derived from GATEWAY_BASE_URL's host and
authed with LLM_API_KEY, both already in the step env.
- Hero art / byline drop from the mandatory-human checklist to review-only.
Co-authored-by: Isaac
* fix(ci): scope gateway URL to image step, guard heroArt rewrite
Address Polly review on the hero-image change:
- Scope GATEWAY_BASE_URL to the image-generation Python invocation only,
instead of the whole Draft posts step. The unsandboxed drafter run no longer
inherits it, so it can't reach the drafter's stdout (which is embedded in the
PR body and only scanned for LLM_API_KEY).
- If the post has no double-quoted `heroArt` field to rewrite, discard the
generated PNG and warn, instead of committing an unreferenced image.
Confirmed omnigent-site's .gitignore only ignores /public/pagefind, so the
generated public/images/blog/<slug>.png commits normally.
Co-authored-by: Isaac
* fix(ci): sync draft-PR boilerplate with auto hero, harden slug path
Address Polly non-blocking notes:
- The "Open draft PRs" body still told reviewers to "add hero art, set the
author byline" — now auto-generated. Reword to say the hero image and
`author: omnigent` byline are generated and only need review, keeping the
demo + voice pass as the human tasks.
- Re-validate slug as strict kebab-case at the point the hero PNG path is
built (defense-in-depth; slug is already validated upstream but this is the
one place it names a new file).
Left as-is per review: inline GATEWAY_BASE_URL expansion is intentional (env:
would re-expose it to the drafter run), and max_tokens on the image endpoint
is harmless.
Co-authored-by: Isaac
* perf(runtime): speed up changed-files git status on large repos
The changed-files panel runs `git status --porcelain --untracked-files=all`
with a hardcoded 5s cap. On large repos that walk is slow and the panel fails
hard (HTTP 500 / git_status_failed) when it exceeds the cap. Three changes:
- Make the git-subprocess timeout configurable via
OMNIGENT_GIT_STATUS_TIMEOUT_SECONDS and bump the default 5s -> 30s so slow
(but not hung) repos get more headroom before erroring.
- Enable core.untrackedCache=true best-effort on registry init so
`git status` stops re-stat'ing every untracked path (upstream git >= 2.8).
- Pass `:(exclude)` pathspecs for _SKIP_DIRS so git never walks large
untracked build/cache trees (node_modules/, .venv/ ...) that we discard
anyway; the root-level post-filter stays as a safety net.
Adds functional tests for the timeout knob, the skip-dir pathspecs, and the
untracked-cache init (including graceful degradation on config failure).
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* perf(runtime): make untracked-cache config a one-shot per git-root
The host fallback path (server reading the host filesystem directly when the
runner is offline) builds a fresh WorkspaceReader — and thus a fresh
GitFilesystemRegistry — for every fs request, unlike the runner path which
caches registries per session. That meant the new core.untrackedCache config
write re-spawned a `git config` subprocess on every host changes/diff/list/
search request.
Guard the write with a process-global set keyed by git-root so it runs at most
once per root per process. Idempotent and thread-safe; adds a test asserting
repeated registry construction on the same root issues the config write once.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* perf(runtime): gate untracked-cache on git's --test-untracked-cache probe
Enabling core.untrackedCache unconditionally risks stale results on
filesystems with unreliable directory mtimes — a newly-untracked file could
then be missing from the changed-files panel. Git's own guidance is to run
`git update-index --test-untracked-cache` first, which exits non-zero on such
filesystems.
Gate the config write on that read-only probe: only enable the cache when the
probe passes. Failures anywhere still degrade silently (pure speedup). Adds a
test asserting the config is left unset when the probe fails.
Addresses a non-blocking review comment on #2905.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(sessions): don't show runner_disconnected error on intentional stop
Clicking "Stop session" in the web UI on a host-spawned session showed a
red "Error · runner_disconnected / Runner disconnected unexpectedly."
card even though the user stopped it on purpose. Stop deliberately tears
the runner's WS tunnel down (_stop_session_host_runner) so runner_online
flips false, which makes the SSE relay hit the same
except (httpx.HTTPError, ConnectionError) path a genuine runner death
takes. That block couldn't tell an intentional stop from a crash, so it
published a failed status with runner_disconnected and persisted durable
error labels that also polluted snapshots and child summaries.
Add a one-shot _intentional_stop_sessions marker set alongside the
existing _interrupt_fenced_sessions. The stop handler marks the session
right before tearing the tunnel down (host-spawned branch only), and the
relay's disconnect handler consults it: an intentional drop resolves to a
quiet idle with cleared error labels, while a genuine disconnect still
surfaces runner_disconnected as before. Safety-net discards on the next
running edge and on session delete keep a stale marker from swallowing a
later real disconnect.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(sessions): clear intentional-stop marker on every relay exit path
Address a correctness regression flagged in review: the one-shot
_intentional_stop_sessions marker could outlive the turn that set it and
silently downgrade a LATER genuine runner disconnect to a quiet idle,
defeating the runner_disconnected surfacing the relay was built to
provide.
Two holes are fixed:
- The running-edge discard was nested under
`if session_id in _interrupt_fenced_sessions`. A Stop typically emits a
terminal response.cancelled first, which clears the fence, so the outer
guard was false on every subsequent running edge and the marker could
never be cleared there. Move the discard into the fence-independent
session.status running branch so a new turn always clears it. The
terminal branch is deliberately NOT used: on an intentional stop the
terminal event arrives over the tunnel before the tunnel drops, so the
marker must survive it to be consumed by the disconnect handler.
- A best-effort stop that never dropped the tunnel (host offline, ack
timeout, host-reported failure) left the marker set with no disconnect
to consume it. _stop_session_host_runner now returns whether teardown
was actually delivered, and the stop handler discards the marker when it
wasn't. A finally-block discard in the relay is added as a belt-and-
suspenders clear for clean/cancelled exits.
Add test_relay_running_edge_clears_stale_intentional_stop_marker covering
the stop -> terminal event clears fence -> new running edge -> later
genuine disconnect sequence; it fails without the running-edge fix.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(web): show busy spinner on new-session Send while create is in flight
The new-session landing screen awaits the full backend round-trip (session
bootstrap + git worktree setup) before navigating to /c/{id}. During that
multi-second window the Send button only went disabled with no other feedback,
so the click read as "frozen" — the typed message just sat in the composer and
users assumed nothing was sent.
Swap the Send button's static arrow for a spinning Loader2Icon while `creating`
is true, and add `aria-busy` + a "Starting session" label. The button was
already disabled via `canSubmit`, so this only adds the missing visual signal
that the click registered and work is in flight.
This is the perceived-latency fix; it doesn't change the actual backend timing.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* test(e2e_ui): cover the new-session Send busy spinner
Add a Playwright test that holds the create POST open with a gate so the
in-flight window is observable, then asserts the Send button flips to its busy
state (disabled + aria-busy="true" + "Starting session" label) while the create
is pending and the landing composer is still mounted, and that navigation runs
once the create resolves. Satisfies the E2E UI Required gate for the visible
submit-button behavior change.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
The randomize button lives inside a Radix PopoverContent that animates in
and is repositioned by Floating UI on mount. A click racing that enter
transition/reposition intermittently timed out with "element is not stable"
/ "detached from the DOM" on loaded CI runners.
Disable CSS animations/transitions on the page and wait for the popover to
fully mount (its hex input visible) before clicking randomize, so the click
lands on a settled node.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Managed sandbox hosts boot in a fresh HOME with env-var credentials only,
so there was no way to give them config.yaml-level configuration — locking
provider-agnostic harnesses like pi out of self-hosted model gateways
(LiteLLM/vLLM) in managed sessions.
- New top-level `sandbox.host_config:` server config key — verbatim
in-sandbox ~/.omnigent/config.yaml content (e.g. a providers: block with
kind: gateway, default: [pi]), provider-agnostic across all managed
launch providers.
- Validated fail-loud at server startup: mapping shape, providers block
through the same provider_config parser omnigent itself uses (secrets
deliberately not resolved — api_key_ref: env:VAR names sandbox env),
inline api_key literals rejected at parse time, the block's own default
scopes checked for collisions, plus a JSON round-trip so YAML-native
values can't fail every launch at runtime.
- Materialized before `omnigent host` starts, from one shared rendering
primitive so merge semantics can't drift between providers: exec-model
providers run a self-contained python3 -c merge script (stdlib+yaml
only) via the shared SandboxLauncher.start_host; kubernetes appends the
same rendered command to its init-container prep script, landing the
file on the HOME emptyDir before the main container boots the host.
- Merge mirrors cli.py's deep_merge_keys=("providers",): providers entries
merge one level deep (injected wins), other top-level keys replace
wholesale. The payload rides base64, so arbitrary YAML content never
touches shell quoting.
- Server-managed replacement semantics: a marker file records what was
injected, and each launch/resume removes those entries by name before
merging the current payload — a renamed gateway or a removed host_config
block cleans up on the next wake instead of stranding stale providers.
User-created config in the sandbox survives; config and marker are
written atomically. A missing or corrupt marker degrades to additive
merging — never delete without evidence of what was injected.
Closes#2126
Signed-off-by: Bryan Li <bryan.li@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(harness): resolve CLI binaries off the daemon's frozen PATH in readiness gates
Follow-up to the codex/claude resolver fix. The general readiness gates
still probed bare shutil.which(spec.binary), so a claude-native /
cursor-native / kiro-native / etc. CLI installed into an nvm/npm-managed
global bin dir (only on PATH via interactive shell init) could still be
reported 'binary missing' by the host daemon, whose PATH snapshot omits
that dir — the same split the codex fix closed for its own gate.
Route harness_cli_installed, missing_harness_cli, and the
harness_is_configured fallback gate through the shared resolve_cli_binary
(PATH -> global-dir ladder), so readiness matches what the launch will
see for every CLI harness. install_harness_cli keeps a bare shutil.which
check: it runs in the setup flow's own process, where the ~/.local/bin
PATH refresh (and the subsequent bare-binary login shell-outs) depend on
the binary being reachable via this process's PATH.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* refactor(harness): drop unreachable spec-None guards in install_harness_cli
Past harness_install_command(key), a spec-less key has already raised
KeyError, so spec is non-None — the 'if spec is not None' guards and the
trailing 'return False' were dead. Assert the invariant instead, per PR
review.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* test(harness): patch resolve_cli_binary, not readiness.shutil
The harness_is_configured fallback gate now resolves via resolve_cli_binary
(shutil was dropped from harness_readiness), so the community-harness
readiness test must patch that instead of the removed readiness.shutil.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
A context overflow on a live (stream=true) turn raised
_ContextWindowOverflow uncaught, since only the background-turn path
caught it, so the process manager's in-flight marker never cleared and
the harness subprocess leaked forever.
Catch it inside proxy_stream() itself so both paths clean up the same
way. Adds a regression test confirmed to fail before this fix and pass
after.
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
* fix(acp): make prompt timeout configurable
Signed-off-by: Robert Dosen <robert.dosen@gmail.com>
* docs(acp): document HARNESS_ACP_PROMPT_TIMEOUT_S and tidy timeout code
Document the new prompt-timeout env var alongside the other HARNESS_ACP_*
vars in the acp_harness module docstring, its discoverability home. Hoist
the duplicated validation error string to a single _PROMPT_TIMEOUT_ERR
constant, and rework the timeout comments so each constant's comment sits
adjacent to it (the init-handshake timeout was left orphaned by the new
parsing block).
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
---------
Signed-off-by: Robert Dosen <robert.dosen@gmail.com>
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
Co-authored-by: Bryan Qiu <bryan.qiu@databricks.com>
* feat(web): gate sidebar row actions on ownership, not permission level
The session sidebar derived every row affordance (rename, share,
move-to-project, drag-to-file) and the My/Shared tab split from each
row's `permission_level`. That forced the server to resolve the
caller's effective grant for every listed session on each list build
and updates poll.
The sidebar only ever needs owner-vs-not, and every list row already
carries `owner`. Switch `isOwnedByViewer` to compare `owner` against
the resolved viewer id (permissive when owner is null — single-user /
legacy rows), and gate the row actions on ownership alone:
- Rename, Share, Move-to-project, and drag-to-file are now owner-only
(Share was manage-gated, Rename/move/drag were edit-gated).
- Non-owners get a read-only row; finer-grained edit/manage affordances
remain on the open-session view, which fetches the caller's real
level via GET /v1/sessions/{id}.
`permission_level` is no longer read anywhere in the sidebar, so a
backend can list sessions without a per-session permission lookup.
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
* feat(web): make sharing owner-only and null-safe on managed list rows
Two follow-ons to the owner-only sidebar, for backends whose session
list is owner-only and omits the caller's effective permission_level
(the Databricks-managed server):
- derivePermissionLevel no longer concludes from a sidebar row whose
permission_level is null. That null is "level not carried", not the
permissive null sentinel, so we skip the fast path and defer to the
authoritative single-session snapshot / read-only fallback. A backend
that keeps emitting a level on list rows (OSS default) is unchanged.
- The header Share affordance is now owner-only (isOwnerLevel of the
derived level), matching the sidebar's owner-only Share gate and the
terminal readOnly gate. Was manage-or-higher (>= 3).
- ChatPage's liveness row prefers the snapshot's permissionLevel over
the sidebar row's, so host_offline's isOwner (who may reconnect the
host) isn't decided by a null managed list level reading as permissive.
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
* test(e2e): cover sidebar owner-vs-not row gating and tab placement
Adds the Playwright e2e coverage the E2E-UI-Required gate asks for on
this PR: the sidebar derives ownership (and every owner-only row action)
from the session's `owner`, not from an effective permission level.
Two flows on a dedicated multi-user server (the shared single-user
live_server hides the My/Shared tabs and the Share item, so the split
can't be observed there):
- Owner: session under "My sessions", kebab Rename + Share enabled,
Rename opens the inline edit.
- Non-owner granted EDIT: session under "Shared with me" (absent from
"My sessions"), kebab Rename + Share disabled — owner-only gating
regardless of the granted level.
Test-only; no product code changes.
Co-authored-by: Isaac
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
---------
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
In an embedded mount (basename e.g. `/omnigent`) the app matches absolute
paths, so `useLocation().pathname` already includes the basename. The
settings sidebar captures that location as the "Back to Omnigent" return
target — on the home page that's the bare basename plus the host's search,
`/omnigent?o=<workspace>`. The link then routes it back through
`rebasePath`, whose idempotency guard only treated `=== basename` and
`${basename}/` as "already under the basename".
`/omnigent?o=123` matches neither (the char after `/omnigent` is `?`, not
`/`), so it gets prefixed a second time → `/omnigent/omnigent?o=123`, which
404s. A conversation return path (`/omnigent/c/abc`) escaped the bug only
because it happens to start with `/omnigent/`.
Treat `/`, `?`, `#`, and end-of-string as the basename boundary, matching
the guard's documented "does not double-prefix a path already under the
basename" contract, while still rebasing a distinct sibling segment like
`/mounting`.
Adds regression coverage in routing.test.tsx for the query/hash boundary
forms (Link + rebasePath primitive) and the over-match guard.
Signed-off-by: Bryan Qiu <bryan.qiu@databricks.com>
Replace Python's raw wall-of-red traceback with a calm, branded crash
screen and a one-tap path to file a GitHub issue from the repo's
bug_report.yml template.
On crash: amber header, compact traceback (shortened paths, collapsed
library frames, first-party packages always visible), report path
next to the [Y/n] prompt. On yes: opens a pre-filled GitHub issue
(template, title, version, OS, traceback in Description). Clipboard
as backup. URL drops body if >8000 chars.
New: omnigent/crash_ui.py, omnigent/crash_handler.py,
tests/cli/test_crash_handler.py (21 tests).
Wired into omnigent/cli.py:main().
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Add two new sections to the agent guidance:
- Finishing a task: agents should print explicit testing instructions
(commands, inputs, reproduction steps) when completing a task so the
user can verify the work without guessing.
- Deprecating features: record the target removal version in code (e.g.
a @deprecated tag/comment naming the release) and in the PR/commit
description, so the feature can be cleaned up when that version ships.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* feat(web): open agent info panel on hover over the (i) icon (#2736)
The agent info popover (agent name, session cost, model usage, etc.)
only opened on click. Make it also open when the pointer hovers the (i)
icon and stay open while the pointer is on the icon or the panel — a
short close delay bridges the gap between them so it doesn't flicker
shut mid-move, and re-entering either side cancels the pending close.
Click and keyboard still toggle the panel, so touch devices (no
mouseenter) and keyboard users are unaffected. Hover-open suppresses
Radix's auto-focus into the panel (which would steal focus / scroll)
while click and keyboard opens keep it. The redundant "Agent tools &
policies" tooltip is hidden while the panel is open.
Co-authored-by: Isaac
* fix(web): gate agent-info hover-open to mouse pointers so taps still open (#2736)
In-browser testing (real Chrome via CDP) surfaced a touch regression the
unit tests missed: a tap synthesizes pointerenter + click, so the
mouseenter-based hover-open fired on the pointerenter and then Radix's
synthetic click toggled the panel straight back shut — a tap could never
open the panel.
Switch the hover wiring from onMouseEnter/Leave to onPointerEnter/Leave
gated on `pointerType === "mouse"`. Touch/pen now fall through to Radix's
native click-to-open, while mouse hover-open (with the stay-open bridge
and close delay) is unchanged. Verified end-to-end in a browser: hover
opens, moving onto the panel keeps it open, leaving both closes after
~150ms, click toggles, and a touch tap now opens the panel.
Add regression tests for the touch-tap-opens path and the
hover-then-click-closes path.
Co-authored-by: Isaac
* test(e2e-ui): cover agent-info popover hover interaction
Add a Playwright e2e under tests/e2e_ui for the agent-info (i) popover's
hover flow (issue #2736): hover opens the panel, the 150ms close-delay
bridge keeps it open when the pointer crosses from the icon onto the
panel, leaving both closes it after the delay, click toggles, and a
touch tap falls through to native click-to-open. The existing coverage
was component/unit only; this exercises the pointer-type gating and the
hover→panel bridge in a real browser.
Co-authored-by: Isaac
* test(e2e-ui): strengthen agent-info hover bridge + click coverage
Two test-quality fixes so the popover tests prove the behavior rather
than passing incidentally:
- Bridge test now walks the pointer down through the real vertical gap
between the icon and the panel (computed from bounding boxes), dwelling
in the empty space past a fraction of the close delay, then lands on the
panel. A bridge-less (zero-delay) implementation closes the panel during
the transit and fails the test — verified by temporarily setting
HOVER_CLOSE_DELAY_MS=0.
- Click test now drives a real mouse pointer (hover + click) instead of
dispatch_event("click"): on a mouse the pointer must move onto the icon
first (hover-opens), so the meaningful click behavior is toggling the
open panel shut and keeping it shut (no double-open). Click-to-open on a
hover-less pointer stays covered by the touch-tap test.
Co-authored-by: Isaac
* fix(web): keep AgentInfo click-to-open reliable under the hover model
A mouse click's own pointer arrival hover-opens the panel (pointerenter →
setOpen(true)) before the click's Radix trigger toggle runs. On a slow render
the hover-open commits open=true first, so the controlled toggle reads true and
flips it back to false — the panel never opens. This regressed click-to-open
(and re-open after a modal dialog closes) on slow/CI machines, failing
test_agent_info_policy_add_and_remove.
Swallow an onOpenChange(false) that lands within a short grace window
(HOVER_CLICK_GRACE_MS) of a hover-open: those two events are one gesture, so the
close is the racy self-toggle, not a dismiss. A deliberate hover-then-click
dismiss dwells far past the window, so click-to-dismiss, the hover bridge, and
the touch-tap fix are all unchanged.
Co-authored-by: Isaac
## Related issue
N/A
## Summary
- Add a required `version-code` input to the `workflow_dispatch` trigger in the Android Bundle workflow. The value is passed to Gradle via `-PversionCode=N` and read in `build.gradle.kts` so each CI-built AAB gets a unique, Play-compatible `versionCode` without manual edits to the build file.
## Test Plan
- Verified locally: `./gradlew -PversionCode=99 assembleDebug` produces an APK with `versionCode='99'`.
- Verified fallback: `./gradlew assembleDebug` (no property) still defaults to `versionCode=2`.
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [x] 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 the Gradle property override produces the correct versionCode in the built APK via `aapt dump badging`.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
## Related issue
N/A
## Summary
- Add a `workflow_dispatch`-triggered GitHub Actions workflow that builds an unsigned release AAB (`./gradlew bundleRelease`) and uploads it as a workflow artifact. Download the artifact and sign it locally with the upload keystore — no secrets in CI, no signing key on GitHub.
## Test Plan
- Triggered the workflow manually on this branch; verified the build succeeds and the AAB artifact is produced.
- Verified `bundleRelease` produces an unsigned AAB when no keystore credentials are present (existing `build.gradle.kts` behavior).
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [x] 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
Triggered the workflow on the branch; confirmed the AAB is built and uploaded as an artifact.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* fix(web): disable "Create custom agent" on a managed sandbox
Selecting a managed sandbox as the target and then creating a custom
agent leaves the affordance offered but unsupported: the sandbox
provisions its runner from a baked image and has no create path for an
uploaded bundle. Gate the "Create custom agent" picker item on
`sandboxSelected` — when a sandbox is the target, render it disabled with
an explanatory tooltip (mirroring the disabled New-Sandbox row) instead
of opening the dialog. On a connected host it stays enabled and opens the
dialog as before.
Adds vitest coverage (disabled on sandbox, enabled on host) and a
Playwright e2e test under tests/e2e_ui/start_session.
Signed-off-by: Edwinhe03 <41037314+Edwinhe03@users.noreply.github.com>
* fix(web): hide "Create custom agent" on a sandbox instead of disabling
Follow-up on the sandbox gating: rather than showing the "Create custom
agent" picker item disabled with a tooltip on a managed sandbox target,
omit it entirely. On a connected host it is shown and opens the dialog as
before. Tests updated to assert the item is absent on a sandbox and
present on a host.
Signed-off-by: Edwinhe03 <41037314+Edwinhe03@users.noreply.github.com>
* fix(web): drop redundant sandboxSelected prop comment
Signed-off-by: Edwinhe03 <41037314+Edwinhe03@users.noreply.github.com>
* fix(web): drop a selected pending custom agent on a sandbox target
Hiding the "Create custom agent" button stops a new pending agent from
being created on a sandbox, but a pending agent selected before switching
to a sandbox would still be submitted through the unsupported multipart
path. Gate the pending pick on `!sandboxSelected`: on a sandbox the
selection falls back to a real agent (`effectiveAgentId`) and the pending
row is hidden from the picker. Off the sandbox the pending pick is kept.
Adds vitest + Playwright e2e coverage for the host->sandbox deselection.
Signed-off-by: Edwinhe03 <41037314+Edwinhe03@users.noreply.github.com>
* fix(web): drop redundant pendingAgent prop comment
Signed-off-by: Edwinhe03 <41037314+Edwinhe03@users.noreply.github.com>
---------
Signed-off-by: Edwinhe03 <41037314+Edwinhe03@users.noreply.github.com>
## Related issue
N/A
## Summary
- Add a floating server-switcher pill to the Android WebView shell, mirroring the iOS `ServerSwitcher`. The pill is always visible at the top center of the screen, shows the current server's host, and opens a dropdown menu with recent servers, Reload, and Connect to New Server — giving users a universal recovery path when the server is unreachable or a non-Omnigent page loads.
- Add an Android-specific scroll-fade gradient so the chat transcript fades smoothly into the pill area, starting at the pill's bottom edge. The fade offsets are driven by CSS variables (`--omnigent-android-switcher-margin/height`) so they stay in sync with the pill dimensions.
- Theme-aware pill styling via the app's brand color resources (light/dark).
## Test Plan
- `./gradlew :app:assembleDebug :app:lintDebug` — 0 lint errors, build succeeds.
- Manual: installed on a Pixel 9a via `adb install`, verified the pill renders with correct theme colors, the dropdown menu opens with recent servers and actions, switching servers reloads the bridge for the new origin, and the scroll-fade gradient appears below the pill.
- Verified the pill stays visible across page loads (always-visible default, backward compatible with older web builds).
## Demo
N/A — tested on physical device; screenshots taken via `adb screencap` during development.
## Type of change
- [ ] Bug fix
- [x] Feature
- [x] 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
Manual verification on a Pixel 9a (API 35): confirmed pill rendering, theme-aware colors (light/dark), dropdown menu with group dividers, server switching via `reloadWithNewServer` (removes old bridge, re-registers for new origin), scroll-fade gradient position, and backward-compatible always-visible default. Existing Robolectric unit tests fail due to Maven Central network blocking (pre-existing, unrelated to this change).
## Changelog
Android app shows a floating server switcher pill with a dropdown menu for quick server switching
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* feat(web): add QR code for opening a session in the mobile app
The share dialog (PermissionsModal) gains an "Open in mobile app"
button next to "Copy link". Clicking it opens a separate modal with
a QR code encoding the session's
deep link — the same scheme the desktop shell's deep-link handler
parses (electron/src/deepLink.js). The QR sits on a fixed white tile
with error-correction level M so it stays scannable in dark mode.
- getDeepLink() derives the host (with port when non-default) from
the same shareable URL getShareableLink() resolves, so standalone
and embedded (host-transformed) origins agree on the same server.
- The QR modal is a sibling Dialog inside the share Dialog, so closing
it returns the user to the share dialog rather than dismissing both.
- Tests pin host resolution for standalone origin, non-default port,
and the embedded host-transform case.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* test(e2e_ui): add QR code modal test to permissions modal suite
Add a Playwright e2e test covering the new "Open in mobile app" QR code
flow in the share dialog: the button opens a second dialog with the QR
code, and closing it returns to the share modal.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
---------
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
The Electron Build workflow's Windows job failed at `npm ci` with
ETIMEDOUT because 5 packages in web/electron/package-lock.json had
`resolved` URLs pointing at npm-proxy.cloud.databricks.com — an
internal proxy unreachable from public GitHub Actions runners.
- Rewrite all 5 internal proxy URLs to registry.npmjs.org in
web/electron/package-lock.json
- Add web/electron/.npmrc pinning the public registry so future
`npm install` runs don't reintroduce internal proxy URLs
- Add scripts/normalize_package_lock_registry.py (fixer + --check mode),
mirroring the existing normalize_uv_lock_registry.py for npm
- Wire normalize-package-lock-registry into .pre-commit-config.yaml for
all three package-lock files (web, web/electron, editors/vscode)
- Add a pre-`npm ci` guard step in the workflow that uses the shared
script to fail fast if internal registry URLs are detected
- Split Linux AppImage and .deb into separate downloadable artifacts
Signed-off-by: Zeyi Fan <zeyi.fan@databricks.com>
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* feat(policies): show config-file policies in admin policy page
Policies loaded from the server --config YAML (RuntimeCaps.default_policies)
were applied to every session but invisible in the admin UI, which only read
from the database. The GET /v1/policies response now appends them as read-only
entries tagged with source: "config".
The frontend renders them with a "Config" badge and omits the toggle/delete
controls, since they are managed via the config file rather than the admin UI.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test(policies): cover config-file policies in GET /v1/policies
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
get_client's model-change branch (a concrete harness, different model requested for the same conversation, respawn) had no direct test coverage despite running in production via post_responses. Adds test_get_client_respawns_on_model_change, covering both the respawn-on-change case and the no-respawn-on-same-model case.
Follow-up to the discussion on #2226.
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
@tiptap/markdown (beta) can hand back a bare inline image with no wrapping
paragraph — a standalone image in document flow (blank lines around it, or
after ---) or an image-first list item (1. ). The doc and listItem
content models are block+, which cannot hold a bare inline node, so the
parsed doc is schema-invalid; nodeFromJSON loads it without validating and
the first transaction (a user edit, or StarterKit's TrailingNode on load)
throws "Called contentMatchAt on a node with invalid content", crashing the
whole file panel ("Page failed to load") and leaving the conversation
bricked until the session is stopped.
This is the known residual documented in #2320 (which fixed block-FIRST
list items via block+ but could not cover bare INLINE children). Fix it the
way #2320's follow-up note prescribed: generalize #2004's toBlockContent
guard from blockquote-only to every block container, as a post-parse
normalization on MarkdownManager.parse (same runtime-patch pattern as the
existing serializer patch in tiptapMarkdownPatches.ts).
Verified against the real triggering file: pre-fix, its only schema
violation is the doc-level standalone image (its :::list-table nested lists
are already handled by #2320); post-fix the file loads, edits, and
round-trips.
Fixes the crash family of #2559 / #2004 / #2320.
Signed-off-by: Jenny <jenny.sun@databricks.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Switching to a previously-viewed chat blanked the view and blocked on
two network fetches before rendering, every time — including switching
back to a chat opened seconds ago. Cache each conversation's rendered
transcript per client and paint it synchronously on switch-back, then
revalidate in the background: bindStream still refetches metadata and
history and reconciles by item id, so items committed while away still
land. In-flight live previews are never cached, the history cursor is
restored atomically so scroll-up paging keeps working, and the cache is
bounded by an LRU cap.
The changed-files panel gained per-file +N/-M line counts, threaded from
the filesystem registry through the runner endpoint to the web UI. But
the changed-files list has a second server-side builder: when a session's
runner is offline and the host holding the workspace answers over the fs
tunnel, WorkspaceReader.changes() shapes its own entry dict — and it
dropped the new lines_added / lines_removed fields, so the counts silently
vanished whenever the list was host-served.
Forward both fields there too, matching the runner endpoint exactly. The
underlying registry already populates them (host and runner share
create_filesystem_registry), so this is purely payload parity.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
omnigent-site now renders each blog post's title + author + date + reading-time
byline via a <BlogPostHeader slug="..." /> component. Update the drafter prompt
so generated posts use it: export the `meta` object (title/date/category/
author/heroArt), render <BlogPostHeader slug="SLUG" /> as the first body
element, and never hand-write a `# H1` title (the component draws it, so an H1
would duplicate the title).
Co-authored-by: Isaac
The host daemon snapshots PATH at spawn and never refreshes it, so a
codex or claude CLI installed into an nvm/npm-managed global bin dir
(only added to PATH by interactive shell init) is invisible to
shutil.which. Native Codex readiness then reports 'binary-missing' and
the claude-sdk executor can't find its system CLI — even though a
foreground launch works, because that runs in the interactive shell's
PATH.
Add a shared resolve_cli_binary(name, env_var) in _platform.py:
override env var -> PATH -> a ladder of common global install dirs
(~/.local/bin, /usr/local/bin, /opt/homebrew/bin, ~/.npm-global/bin).
Route _find_codex_cli (OMNIGENT_CODEX_PATH) and _find_system_claude
(OMNIGENT_CLAUDE_PATH) through it, and the codex readiness gate too, so
the readiness verdict and the actual launch can't disagree. Update the
codex binary-missing UI message and the ImportErrors to point at the
real fix (restart the host, or set the override) instead of 'omnigent
setup', which doesn't address a stale PATH snapshot.
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* fix(ci): make feature-blog drafts read user-facing, not machine-generated
The first drafted posts leaked the prompt's skeleton labels as literal text
("Who it's for:", "The problem it solves"), buried the reader in
implementation detail (per-harness verification status, internal component
names, harness ids), and overused " — " dashes that read as AI-generated.
Rework the drafter prompt:
- The 5 items are the post's SHAPE, not headings or sentence lead-ins. Only the
H1 title is a heading; everything else is flowing prose. Explicitly ban the
label phrases as headings or sentence starts.
- Add a "Voice and content rules" section: write what the user can DO (not how
it's built/verified); never list harness ids / component names / PR numbers /
verification caveats — say "works with any agent you run in Omnigent"; cap the
whole post at one dash; plain, active, no marketing adjectives.
Co-authored-by: Isaac
* feat(ci): surface drafted post body for dry-run review
A dry_run=true run opens no PR and the workflow didn't upload the drafted
page.mdx, so the actual post body was invisible — you could only see the
drafter's narration + summary. Copy each drafted post to /tmp/post_<i>.mdx
(added to the uploaded artifact) and render it into the job summary inside a
collapsible block, so the post can be reviewed on a dry run without opening a
PR. Also rename the upload step to reflect that it runs on success too.
Co-authored-by: Isaac
* fix(ci): find drafted post via -uall (untracked dir hid page.mdx)
`git status --porcelain` collapses a brand-new untracked directory to
"app/blog/<slug>/" and never names page.mdx inside it, so `grep page.mdx`
returned empty and `$post` was blank. That silently skipped everything guarded
on $post: the CTA footer, the HTML-comment guard, and the drafted-post
copy/summary — the post still committed via `git add -A`, so it looked fine.
Add -uall to both porcelain reads so individual new files are enumerated.
Co-authored-by: Isaac
Remove the daily weekday cron trigger from the Reviewer SLA workflow so it
no longer auto-pings reviewers, adds second reviewers, and labels open PRs
awaiting review. Keeps workflow_dispatch so the sweep can still be run
manually if needed.
Co-authored-by: Isaac
* Show per-file and total line-change counts in changed-files panel
Add +N/-M line-change counters beside the A/D/M badge for each file in the
changed-files panel, plus totals in the "Changed N" header. Line counts come
from git numstat, computed at the record source and threaded through the
runner API to the web UI (also used by desktop and iOS webview clients).
Binaries and non-git workspaces render no count. No backend consumer outside
the web UI.
* Refine changed-files line counts: right-align status, drop size and untracked/total stats
- Move the A/D/M status badge to the right of each row; left-align the
filename with a muted parent-directory suffix.
- Remove the per-row file-size label from the changed-files list.
- Only surface line counts from `git diff HEAD` (numstat); untracked files
no longer read off disk to count lines, matching VS Code / Cursor.
- Drop the +/- line totals from the "Changed" header pill.
Co-authored-by: Isaac
* Hoist git subprocess timeout into a shared _GIT_TIMEOUT_SECONDS constant
All four git calls backing the changed-files view shared a literal
timeout=5. Name it once so the cap can be tuned in a single place.
Co-authored-by: Isaac
* Hide the line-count badge for mode-only changes; clarify rename docstring
- A chmod-only edit surfaces in numstat as 0/0; suppress the "+0 −0" badge
(it's noise) while still rendering a real deletion's −N.
- Clarify the _run_git_numstat docstring: with --no-renames a pure rename
shows +N on the destination, not (None, None).
Co-authored-by: Isaac
---------
Co-authored-by: Anthony Ivan <anthony.ivan@example.com>
Co-authored-by: Serena Ruan <serena.rxy@gmail.com>
Sample the omnigent server process's CPU% and RSS memory in a 1-second
background thread (BenchEnvironment._sample_resources via psutil) for the
full duration of each benchmark run. Summarise as mean/min/max/samples and
emit under a top-level 'resource_usage' key in the JSON report.
Schema bumped to version 3 so the workspace ETL can branch on it.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Native TUI CLIs that read LC_ALL / LANG directly (opencode, pi, hermes)
rather than calling POSIX setlocale render multibyte UTF-8 as mojibake when
the inherited env has an empty LANG and no LC_ALL (only a UTF-8 LC_CTYPE,
as in a minimal container). They fall back to an ASCII/Latin-1 codeset and
re-encode their own UTF-8 output byte-by-byte; because the corrupt bytes
are what the CLI physically writes to the tmux pane, the garbling shows up
in the raw terminal view too. CLIs that call setlocale (claude, codex) are
unaffected because glibc honors LC_CTYPE.
TerminalInstance.launch now forces LANG=LC_ALL=C.UTF-8 into the pane spawn
env when the inherited env carries no UTF-8 signal in the vars those CLIs
actually read. A UTF-8 LC_CTYPE alone is not treated as a signal (it does
not help them). Operator-provided UTF-8 locales are preserved; a pinned
non-UTF-8 LC_ALL is corrected; no-op on Windows (tmux panes are POSIX-only).
C.UTF-8 is used because it needs no locale archive and so is present on
minimal images where en_US.UTF-8 is not.
Helpers _is_utf8_locale_value / _has_utf8_locale / _apply_utf8_locale_default
are pure and unit-tested: codeset parsing, POSIX LC_ALL-over-LANG precedence,
the LC_CTYPE-only repro config, operator-locale preservation, non-UTF-8
LC_ALL correction, and the Windows no-op.
Closes#2427
Signed-off-by: abhay-codes07 <abhaysingh0293@gmail.com>
* fix(sessions): stop running child sub-agents, not just the parent, before archive/delete
_best_effort_stop used the child-rollup status only to decide whether to act, then always issued the stop against the parent's own session id. A parent that had gone idle while a sub-agent child kept running got a no-op stop, and the child was then orphaned by the recursive subtree delete/archive (still running, but unreachable via the API).
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
* fix(sessions): walk the full sub-agent tree, not just direct children
_best_effort_stop only checked one level of children, but delete_conversation's recursive subtree delete has no depth limit. A running grandchild (or deeper descendant) was invisible to the one-level check and stayed orphaned exactly like the original bug. Now walks the whole descendant tree level by level and stops every running/waiting descendant at any depth.
Addresses review feedback from TomeHirata on PR review.
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
---------
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
The Open-draft-PRs step created the PRs but only logged the already-open case
to the job summary, so a normal run left no clickable link to the drafts it
opened. Capture `gh pr create`'s stdout URL and write a "Draft blog PRs"
section with a markdown link per feature (both newly created and
force-push-updated existing drafts).
Co-authored-by: Isaac
The drafter emitted the demo placeholder as an HTML comment
(`<!-- DEMO REQUIRED ... -->`), which is invalid in MDX — only `{/* ... */}`
works. It passed prettier's fmt:check but broke the site's `next build`
(page.mdx:36 "Unexpected character !"), so every generated blog PR failed CI.
- Change the drafter's demo marker to an MDX comment `{/* DEMO REQUIRED ... */}`
and update the summary reference to match.
- Add a fail-fast guard in the workflow: if the drafted page.mdx contains any
`<!--`, abort before opening the PR so we never ship a build-red PR again.
Co-authored-by: Isaac
The forwarder's _PostRetryTracker exhausts only permanent 4xx failures
(_is_permanent_http_error = 400 <= status < 500); a 503 is treated as
transient and retried forever with backoff. The runner's
`subagent_delivery_not_confirmed` 503 -- a terminal sub-agent result that
could not be delivered to the parent inbox -- is usually a brief dispatch
race and should be retried, but when the parent host is gone the condition
is permanent, so unbounded retries let a single orphaned sub-agent flood
the shared server indefinitely.
Add `_is_subagent_delivery_not_confirmed()` (a 503 whose JSON body carries
error == "subagent_delivery_not_confirmed") and bound this class to
_SUBAGENT_DELIVERY_NOT_CONFIRMED_MAX_ATTEMPTS (12). The budget spans the
backoff schedule (capped at 30s) -- a few minutes, comfortably covering the
dispatch race -- after which the entry is dropped as exhausted (and
non-permanent, since the failure is environmental). Generic 5xx retry
behaviour is unchanged.
Signed-off-by: abedegno <jon@jonwilliams.org.uk>
chatStore was invalidating ["conversation", convId, "items"] on turn
completion, but useSessionItems registers its cache under
["session", sessionId, "items", "raw"]. The key mismatch meant the
execution-logs panel's cache was never invalidated by SSE, so the
panel stayed stale after a turn ended and relied solely on its 3s
refetchInterval to show new items.
Import sessionItemsQueryKey from useSessionItems and use it in the
invalidateQueries call so the hook's cache is actually invalidated
when a session turn completes.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
Add a `max_posts` workflow_dispatch input (default 3) so a manual run can ask
for more or fewer blog drafts. The guard step sanitizes it to a positive
integer, and the value is threaded into both the scout prompt (told to return
at most N, ranked) and the parse step's defensive cap (cands[:max_posts]),
replacing the hardcoded 3. The scout config's cap wording now defers to the
run-supplied limit. A real release cut (workflow_run) still uses the default.
Co-authored-by: Isaac
The omnigent-site blog surface now exists on main (app/blog/ layout + index +
lib/blog.js scanner + nav link, from omnigent-site#334). The drafter must stop
scaffolding it — its runs were nondeterministic (one candidate invented the
whole layout/index/nav, others wrote only the post), producing incoherent,
merge-order-dependent PRs. Tighten the prompt so the drafter creates ONLY
app/blog/<SLUG>/page.mdx, reads existing posts + lib/blog.js read-only to match
conventions, and flags any missing infra under "Manual review needed" rather
than inventing site plumbing that can break the build.
Co-authored-by: Isaac
The omnigent-site CI gates on `prettier --check .`, and LLM-generated MDX/JS
(plus the CTA footer the workflow appends) is rarely prettier-clean, so draft
PRs fail `fmt:check` on arrival. Run `prettier --write` on the drafter's
changed files from inside the site checkout — so it picks up the site's
.prettierrc.json + .prettierignore — before staging and committing. Pinned to
prettier@3 (the site's major). Non-fatal: a formatting failure logs a warning
and commits anyway, since these are human-reviewed draft PRs and CI still
reports residual issues.
Co-authored-by: Isaac
Add any_policies_apply() to builder.py — a cheap check that returns False
when the combined policy list (session + agent guardrails + server defaults)
would be empty. Call it in POST /policies/evaluate after loading the agent
spec, returning POLICY_ACTION_ALLOW immediately when nothing would fire —
matching what the engine returns when all policies pass.
This avoids the engine build and its associated conversation-store reads
(labels, state, usage) on every tool call hook for sessions with no policies
configured — the common case. The session-policy check uses the existing
LRU cache so it's a cache hit after the first call per session. Mid-session
policy additions invalidate the cache immediately, so newly added policies
are visible on the very next evaluate call.
sys_add_policy TOOL_CALL events always bypass the fast path: the engine
unconditionally injects _ASK_ON_ADD_POLICY_SPEC to require human approval
before an agent can install session policies. Passing phase and tool_name
to any_policies_apply() ensures that gate is never skipped.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(policies): thread turn-initiating created_by as policy actor via runner
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test(policies): verify runner-supplied actor overrides request identity at evaluate and MCP proxy
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(policies): stash turn actor server-side to prevent body-based spoofing
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(policies): bound _session_turn_actor with LRUCache; skip None on stash; fix test cleanup
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(host): silently refresh Databricks token on /v1/me 401 before failing
When omnigent-host.service starts in headless mode and the stored OIDC
token has expired, _ensure_databricks_server_auth probes /v1/me, gets
401, and immediately raises ClickException — crashing the daemon before
the tunnel is ever attempted.
Fix: before giving up, attempt a silent SDK token refresh via
_databricks_workspace_token (which calls _resolve_databricks_auth and
mints a fresh bearer from the cached OAuth grant). If the retry succeeds
(HTTP 200), return normally so the daemon continues to start. Only raise
the ClickException if the SDK has no valid grant either.
This is the root cause of the mass runner-stranding incident, where an
expired OAuth token caused 32+ crash-loop restarts of the host daemon,
killing all 48 runner processes simultaneously.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(policies): persist turn actor to conversation labels for cross-replica safety
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* style: ruff format sessions.py
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(policies): guard omnigent.turn_actor label against client writes; drop unrelated cli.py change
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(policies): guard omnigent.turn_actor on multipart bundle-create path; drop dead created_by runner body field
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* refactor(policies): simplify turn-actor label guard; trim comment; drop redundant None check
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* docs(policies): document turn-serialization gap and native-terminal bypass; restore None guard on mcp_conv
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(fork): drop CLI-specific launch args when a fork switches harness
Forking a Claude Code session onto pi failed to start with
`required_terminal_exited`. The fork copied the source's
`terminal_launch_args` verbatim, so `--permission-mode auto` (a Claude
Code flag) reached the pi argv; pi rejects the unknown option and exits 1
at launch, taking the required terminal — and the session — down with it.
Launch flags are CLI-specific and must not survive a cross-CLI switch:
- `fork_conversation` gains `copy_terminal_launch_args` (default True);
the fork route passes `not switching_agent`, so a same-agent fork still
inherits flags but an agent switch starts with clean args.
- `switch_conversation_agent` (in-place claude->pi switch, same latent
bug) now clears `terminal_launch_args` alongside `external_session_id`.
Co-authored-by: Isaac
* test(fork): teach route-test fake store the copy_terminal_launch_args arg
The route fake's fork_conversation lacked the new keyword-only parameter,
so every forking route test raised TypeError. Add it to the signature,
record it in fork_calls, and assert the route's switch-gated wiring:
False on an agent switch, True on a same-agent fork.
Co-authored-by: Isaac
* fix(runner): recover cold-resume context when server GET returns null external_session_id
On reconnect, the GET /v1/sessions/{id} may return external_session_id=null
due to a workspace-scope ContextVar defaulting to 0 on fresh tasks. The runner
then launches a fresh Claude session and loses all conversation context.
- app.py: after the GET block in _auto_create_claude_terminal, fall back to
read_claude_session_id(bridge_dir) if session_external_id is still None; the
local bridge state file survives reset_transcript_forward_state and holds the
previous claude_session_id, so we use it as the resume hint.
- claude_native_forwarder.py: on a 400 PATCH rejection in
_maybe_mirror_external_session_id, fetch the server-bound external_session_id
and include both the rejected sid and the server-bound sid in the warning so
operators can identify which session retains the context.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(runner): capture bridge claude_session_id before prepare_bridge_dir wipes it
The cold-resume fallback read read_claude_session_id(bridge_dir) after
prepare_bridge_dir had already deleted _STATE_FILE, so it always returned
None and the fallback was dead code.
Fix: read read_claude_session_id from the pre-wipe bridge dir (computed via
bridge_dir_for_bridge_id using the bridge_id already resolved at that point)
before the prepare_bridge_dir call, stash the result, and use the stash in
the fallback block.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* test(runner): assert cold-resume fallback reads bridge sid before prepare_bridge_dir wipes it
Adds a test for the ES-2065116 fix: when the server snapshot omits
external_session_id (workspace-scope miss), the runner falls back to the
claude_session_id written in state.json by the prior launch. The test
pre-populates state.json before _auto_create_claude_terminal runs and
asserts _ensure_local_claude_resume_transcript is called with the local
sid, proving the read happens before prepare_bridge_dir deletes the file.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* revert(forwarder): remove diagnostic GET on 400 PATCH rejection
The extra snapshot fetch on 400 was purely for logging and adds an
unnecessary round-trip. Restore the original single-line warning.
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
PR #2764 extracted the LLM-runner scaffold (uv + Claude Code CLI + gateway
provider config + agent run + stdout secret-scan) into the composite action
.github/actions/run-omnigent-agent, now shared by draft-release-notes.yml and
publish-changelog.yml. feature-blog.yml still inlined all of it.
Replace the five setup steps + the scout run + its secret-scan with one
`uses: ./.github/actions/run-omnigent-agent` for the tools-less scout (−54
lines). The per-candidate drafter loop still calls `omnigent run` directly —
it interleaves git operations between invocations, which the single-shot
action can't model — and reuses the environment (PATH, ~/.omnigent, .venv)
the action provisions when the scout runs.
Co-authored-by: Isaac
* test(proc): de-flake process_alive nondestructive-probe PID-recycling race
Pin the child via psutil.Process(pid) so the post-teardown liveness
assertion can't be fooled by a recycled PID masquerading as the reaped
child, removing the process_alive(pid) TOCTOU race in the test.
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* test: pin psutil handle in terminate_tree test to kill PID-recycling race
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
---------
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* adjust slack bot behavior so that in channels only @ trigger omnigent, but in DMs, threads strictly map to sessions
streaming text and take advatange of markdown_text support; build towards multi-user support in the slack integration
improve placeholder experience and the ability to handle closed streams
device grant to support accounts-based auth for slack integration
slack integration now supports both accounts and oidc auth
* pre-commit clean-up
* slack socket server security enhancement
* improve security posture
* update uv.lock
* fix test failures: CI builds no web SPA, so the SPA catch-all mount at / is absent
* feat(auth): read the OIDC email identity from a configurable id_token claim
_resolve_oidc_email reads only the email claim and hard-fails when it is
absent. Microsoft Entra ID commonly issues id_tokens that carry the user
identity in preferred_username (the UPN) with no email claim at all, so
native OIDC login against Entra fails with "Could not determine user
email" and nothing actionable in the logs.
Add OMNIGENT_OIDC_EMAIL_CLAIM (default: email), mirroring oauth2-proxy's
--oidc-email-claim: the operator names the id_token claim that carries
the email identity. The default path is unchanged. A custom claim always
requires the existing OMNIGENT_OIDC_SKIP_EMAIL_VERIFICATION opt-out:
email_verified refers to the email claim (OIDC core), so it vouches
nothing about a custom identity claim, and a token carrying
email_verified true for a different address must not smuggle the custom
claim past the gate. The absent-claim rejection now logs the configured
claim and the claim names present.
Only the generic-OIDC path is affected; GitHub OAuth has no id_token.
Tests: a UPN-only token mints a session with the claim configured plus
the opt-out; a custom claim without the opt-out is rejected both with no
verified marker and with email_verified true referring to a different
email claim; a token missing the configured claim is rejected even when
a verified email claim is present (no silent fallback).
Signed-off-by: Robert Dosen <robert.dosen@gmail.com>
* fix(auth): reject malformed OIDC identity claims
Signed-off-by: rdosen <robert.dosen@gmail.com>
---------
Signed-off-by: Robert Dosen <robert.dosen@gmail.com>
Signed-off-by: rdosen <robert.dosen@gmail.com>
A session's selected working folder (snapshot.workspace) was honored by the
Files panel / primary OS environment (see per-session-workspace fix) but NOT by
the spawned harness subprocess. _build_spawn_env_from_spec received the runtime
cwd and forwarded it only to pi/kimi; codex, claude-sdk, cursor, qwen, goose,
and copilot builders never set their HARNESS_<H>_CWD env var, so the harness
subprocess (e.g. codex reading HARNESS_CODEX_CWD) fell back to cwd=None and
inherited the runner's launch directory instead of the session workspace.
Thread cwd into all six builders (set HARNESS_<H>_CWD when provided) and pass
cwd=cwd at the dispatch call sites. Mirrors the existing pi/kimi handling.
Adds a parametrized regression test locking cwd threading for all six.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Co-authored-by: jykim-bagel <jykim@bagel-labs.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs(releases): match the real MLflow release-post format
The first pass mirrored the whole release body — every feature bulleted into a
numbered section, a "Fixes & improvements" section, and PR refs carried through.
The actual mlflow.org/releases posts are curated: only the outstanding features
get a section, there is no bug-fixes section, and there are no PR links.
Rework the release-post-formatter prompt to:
- curate down to the ~4-6 outstanding features and drop minor items entirely,
- omit the bug-fixes section (comprehensive changes live behind Full Changelog),
- drop all PR references from the post,
- write each feature as what-it-is + how-to-use-it, and
- emit per-feature demo and docs-link placeholders (literal TODO) for a human to
fill in on the auto-opened PR, since the release body carries no media or URLs.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* docs(releases): pre-fill real docs links, omit when none match
Instead of a blanket TODO "Learn more" placeholder, give the formatter the list
of the site's real /docs pages (URL + title) and have it link each feature to a
matching page — or omit the line entirely when nothing fits.
- publish-changelog.yml builds a docs index from a blobless sparse checkout of
the public omnigent-site app/docs tree (no token) and feeds it to the prompt;
best-effort, so a fetch failure just yields an empty index (links omitted).
- The formatter links only to a verbatim URL from that list, never guesses or
emits a TODO doc link. The demo image stays a TODO placeholder for a human.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* docs(releases): link features to the most specific docs section
Page-level docs links are coarse — an ACP-harness feature should point at
/docs/build/harnesses#custom-acp-agents, not the whole page. Index each doc
page's h2/h3 section anchors alongside the page itself and let the formatter
pick the most specific match.
- The docs-index step now emits indented `url#slug <TAB> title` rows per section,
computing the slug with the same algorithm the site's HeadingAnchors uses so
the anchor resolves. It skips fenced code blocks and reduces `[label](url)`
headings to their label (the site slugs rendered text).
- The formatter prompt prefers a matching #section anchor over the bare page,
and still omits the "Learn more" line when nothing fits.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(policies): wire PolicyStore in Docker entrypoint and thread session owner as actor
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(policies): prefer authenticated caller over session owner as actor
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* fix(policies): skip get_session_owner DB call when user_id is present; add actor fallback tests
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
* revert(policies): remove get_session_owner fallback from actor resolution
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
---------
Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
test_control_bridge_burst_then_exit_delivers_full_tail relied on a fixed
sleep(10.0) to let the reader drain the tmux control stream, which was slow
and still racy under load. Add two inert, default-None asyncio.Event hooks
(reader_done / forward_done) to bridge_tmux_control_to_websocket that fire
when the reader and forwarder finish, and switch the test to wait on those
events instead of a wall-clock sleep.
The hooks default to None, so the hot path is unchanged for real callers;
only the test opts in. Target test now completes in ~2s (was ~10s).
Co-authored-by: Isaac
Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
* docs(releases): reformat website release posts in MLflow narrative style
The website /releases/<version> post was a verbatim mechanical mirror of the
GitHub Release body (emoji bullets). Reformat it into the narrative, prose-driven
style of mlflow.org/releases, while leaving the GitHub Release notes untouched.
- New release-post-formatter agent rewrites the curated release body into an
intro summary + numbered prose feature sections (no emoji), preserving every
PR ref and inventing nothing. Same tools-less security posture as
release-notes-drafter.
- publish-changelog.yml gains the LLM machinery to run it, degrading to the raw
release body on any failure, plus a workflow_dispatch dry_run mode that renders
and prints the page (log + job summary) without minting a token or opening a PR.
- release_to_mdx.py adds MLflow-style site chrome the release body can't carry: a
byline (date + read time + author) and a "What's Next" footer. Keeps the exact
_Released <date>_ token the site index reads.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* refactor(ci): extract shared LLM-runner into a composite action
The publish-changelog release-post formatter reused ~150 lines of the
draft-release-notes LLM machinery (uv, venv cache, Claude CLI, provider config,
agent run, output secret-scan) verbatim. Extract it into a
.github/actions/run-omnigent-agent composite action and call it from both
workflows, so the runner scaffold lives in one place.
- The action takes a workdir input so it works whether the repo is checked out
at the workspace root (draft-release-notes) or in an omnigent/ subdir
(publish-changelog), driving the venv path, cache key, and uv --project/agent
paths off it.
- The action now always secret-scans the agent output when it runs (gated by the
caller's creds check), instead of the old outcome=='success' gate that also
skipped the scan when the step was skipped.
- Callers keep their own prompt-build, output-extract/fallback, and artifact
redaction; only the shared scaffold moved.
Co-authored-by: Isaac
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
---------
Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
* fix(sandbox): grant the private scratch tmpdir before the spawn-time wrap
A darwin_seatbelt claude-sdk seat booted the sandbox-exec wrap but then
died with `FileNotFoundError: No usable temporary directory` — the
follow-up to the seatbelt cluster (#2743/#2749).
run_launcher runs twice for spawn-wrap backends: the host pass builds the
wrap (baking the seatbelt SBPL profile / bwrap binds) and execvp's into
it; the in-wrap pass activates and runs the target. The private scratch
tmpdir was minted only in the in-wrap pass, via mkdtemp() against $TMPDIR
= the system tempdir root — which the already-baked profile only granted
a subpath of. bwrap masked this via its --tmpfs /tmp fallback, so only
seatbelt (no tmpfs, $TMPDIR always set on macOS) hit it.
Mint + grant the scratch dir on the host BEFORE the wrap (the pattern
_HelperProcessClient._start_locked already uses), re-encode the policy so
both the profile and the in-wrap pass see the granted root, and hand the
path to the in-wrap pass via a marker env var so it adopts that exact dir
and owns cleanup. The marker is retained through the spawn-env prune;
using it (not _scratch_tmpdir re-derivation) for cleanup avoids rmtree'ing
a spec-supplied write root like /tmp.
Verified on a real Mac: the reported FileNotFoundError reproduces pre-fix
and is gone post-fix; a jailed claude-sdk seat boots through to the
provider. Adds macOS-gated (seatbelt) and Linux-gated (bwrap) end-to-end
regression tests driving the full create_exec_launcher -> run_launcher
two-pass re-exec.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* test(bwrap): allow .venv under the granted project-root read root
The dotfile masker tmpfs-masks hidden dirs under read roots, which hid
the project .venv from the in-wrap re-exec — the inline import of
omnigent.inner.sandbox died with ModuleNotFoundError: yaml before the
tmpdir path ever ran. The seatbelt twin already carries this allowance.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
---------
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* fix: use a private mode-700 dir for the modal foreground pidfile
exec_foreground recorded the remote pid at a fixed, predictable path in the
world-writable /tmp (/tmp/oa-foreground.pid). A co-tenant process in the
sandbox could pre-seed that path as a symlink (so `echo $$ > ...` writes
through it) or overwrite its contents (so `kill $(cat ...)` signals an
arbitrary pid).
Record the pid in a private, unpredictably-named dir created with
`mkdir -m 700` (no -p, so it fails closed if the path already exists), and
only signal a numeric pid read back from that file before removing the dir.
Update the tests to assert the new structure instead of the fixed path.
Signed-off-by: agharsallah <17379925+agharsallah@users.noreply.github.com>
* fix: resolve symlinks before trusting a SQLite path as a test DB
looks_like_test_db accepted a file-backed path on its 'test' name token or its
temp-dir location without resolving symlinks first. A symlink planted in a
world-writable dir like /tmp (e.g. sqlite:////tmp/test.db) could therefore
point a 'throwaway' test DB at a real database and pass the guardrail.
Resolve the path before the token and temp-dir checks so the resolved target
is what gets classified, and add a regression test covering a test-named
symlink that resolves outside any temp root.
Signed-off-by: agharsallah <17379925+agharsallah@users.noreply.github.com>
* fix: share safe foreground-pidfile helper across sandbox launchers
Extract a single fail-closed foreground-pidfile implementation into
base.py (foreground_pidfile / foreground_record_prefix /
foreground_kill_command) and route Modal, CoreWeave (cwsandbox), and
OpenShell through it, closing the same /tmp symlink-redirect + pid-spoof
vector the Modal-only fix addressed in two other shipped providers.
- cwsandbox: drops the vulnerable fixed /tmp/oa-foreground.pid and
unvalidated 'kill $(cat ...)' — now uses the private mode-700 dir
with a numeric-gated kill. Adds exec_foreground regression tests
(none existed before) and extends the cwsandbox fake to record exec
commands and raise on wait.
- openshell: drops the predictable {sandbox_id} pidfile template and
unvalidated kill for the shared, numeric-gated path.
- modal: drops its inline copy and imports the helper; behavior
unchanged for the security properties.
- All three: clean up the run dir on normal exit too (previously only
on Ctrl-C), so a successful run no longer orphans a mode-700 dir.
- Helper hardening: shlex.quote the derived run_dir/pidfile inside
foreground_record_prefix and foreground_kill_command so the public
API stays injection-safe even if a future caller passes a non-hex
path. Hex paths quote harmlessly.
All 268 tests/onboarding/sandboxes tests pass; ruff check + format clean.
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
---------
Signed-off-by: agharsallah <17379925+agharsallah@users.noreply.github.com>
Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
Co-authored-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
* Support commenting in PDF viewer
Signed-off-by: kevin-lyn <kevin.lin@databricks.com>
* Apply prettier formatting to PDF comment helpers.
* Add e2e coverage for PDF comment selection and highlights.
Exercise the full PdfViewer flow: text-layer drag selection, floating add-
comment button, pending/saved highlight overlays, and PDF geometry anchors
via the comments API.
* e2e test
Signed-off-by: kevin-lyn <kevin.lin@databricks.com>
---------
Signed-off-by: kevin-lyn <kevin.lin@databricks.com>
- Offer the trusted vendor installer from the Hermes setup menu
- Refresh ~/.local/bin so configuration can continue without restarting
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
cursor-sdk's AsyncBridge.launch spawns the bridge subprocess without a
cwd=, so the bridge -- and the shell tools Cursor runs inside it --
inherited the runner daemon's directory instead of the spec's
os_env.cwd. --workspace only routes indexing, not command execution, so
pwd / git / relative paths operated on the wrong tree.
Set the process cwd to the resolved workspace across
AsyncClient.launch_bridge and restore it afterwards, serialised by a
process-global lock so an overlapping launch can't observe a
half-applied cwd. The underlying Popen(cwd=...) fix belongs upstream in
cursor-sdk; this compensates from the executor since the SDK is an
external dependency.
Refs #2111
cursor_policy_hook is the preToolUse gate for the Cursor SDK harness's native tools. On two failure branches it returned {"permission": "allow"}, so a transient Omnigent-server outage (resp is None after the retry budget) or a malformed response silently skipped DENY/ASK policy enforcement.
Fail closed with deny on both, matching hermes_policy_hook and the native hooks' fail_closed_hook_output (PR #163), and honoring post_evaluate_with_retry's documented contract that the caller handles None as fail-closed. The no-server, stdin-parse, and import-error branches keep failing open, exactly as the sibling hooks do.
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
A watchdog-cancelled turn raises asyncio.CancelledError, which is a
BaseException and bypasses run_turn's except-Exception cleanup boundary.
The wedged ClaudeSDKClient stayed cached in _clients, so every resume
reused it, emitted no events, and re-tripped the 240s idle watchdog;
the session was unrecoverable until a daemon restart.
Catch CancelledError at the same boundary, synchronously pop the client
and force-close it in a background task (awaiting a graceful close there
could itself be cancelled), then re-raise. The session is not crash-marked:
the next turn rebuilds a fresh client and replays history through the
text-prefix path.
Closes#2109
Signed-off-by: Enes Yilmaz <115046343+EnesYilmazcode@users.noreply.github.com>
Wrap failures used to kill the seat at connect time: resolve_sandbox
raised straight out of prepare_claude_cli_path, and wrap-time OSErrors
(un-grantable interpreter layout, profile-size cap, cwd-scan overflow)
fired inside run_launcher where they surface as an opaque exit-71 /
60s connect timeout.
Probe the wrap at prepare time — the last point where degrading is
still safe — and on failure return the CLI unwrapped with native tools
disabled plus a WARNING: the same confinement shape as the
OMNIGENT_CLAUDE_SDK_NO_SANDBOX bypass (file/shell access stays on the
independently sandboxed sys_os_* helpers, which fail closed on their
own). run_launcher itself stays fail-closed for every other lane.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Port the two bwrap visibility behaviours seatbelt never got:
- Walk argv[0]'s symlink chain hop-by-hop and grant a literal read on
every uncovered symlink (uv's version-floating cpython-3.12 dir hop
was denied, EPERM-ing every jailed helper execvp at boot).
- Stop discarding the launcher target: grant its symlink chain plus a
narrow subpath on the resolved binary's own directory so the wrapped
CLI (e.g. claude) is readable inside the sandbox. Never raises —
un-grantable layouts degrade to a literal grant plus a WARNING.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* perf(policies): remove unused trajectory DB read from policy evaluation
EvaluationContext.trajectory was populated on every POST /policies/evaluate
call via a list_items() query (last 10 conversation items), but no policy
implementation ever read it — FunctionPolicy, PromptPolicy, and LabelPolicy
all ignore ctx.trajectory. The fetch was dead work on every tool call hook.
Remove _populate_trajectory, _TRAJECTORY_WINDOW, EvaluationContext.trajectory,
and the now-unused ConversationItem import. Eliminates one DB read per
policy evaluation, which fires multiple times per turn across all harnesses.
* fix(ci): remove trajectory test, fix hosts_changed e2e health mock
- Delete test_engine_trajectory.py: tested EvaluationContext.trajectory
which no longer exists after removing the trajectory DB read
- Fix test_hosts_changed_frame_updates_host_badge: stub /health to return
empty sessions so liveOnline stays undefined; without this the health
poll sets liveOnline=null (no real host bound), overriding the useHosts
mock and preventing the badge from ever showing "online"
Since #2228 the tunnel route registers hosts under the bare-hex id,
but REST callers can still present the legacy host_<hex> spelling
(pre-migration config.yaml + older CLIs). Every DB path normalizes
via uuid_to_bytes, so GET /v1/hosts reported such hosts online while
the launch path's exact-string registry lookup missed the live
tunnel and 409'd "host is offline" — deterministically, straight
through the CLI's transient-409 retry ladder.
Canonicalize the key inside HostRegistry itself (register / get /
deregister), falling back to the verbatim string for ids that are
not uuid-shaped. One guard at the choke point covers
_host_launch.py, _workspace_validation.py, and any future caller,
and keeps HostConnection.host_id consistent with its storage key
(send_text's replaced-connection check relies on that).
Fixes#2740
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
A bwrap-sandboxed helper became unspawnable when the sandbox cwd was an
ancestor of the helper interpreter and the interpreter lived under a
dotdir (e.g. a `uv tool`-installed omnigent at
`~/.local/share/uv/tools/omnigent/bin/python` with cwd=$HOME). The
dotfile masker `--tmpfs`-masks `.local`, and since the mask is emitted
last to win over broad binds, it hid the interpreter and bwrap died with
`execvp ...: No such file or directory`.
Two interacting causes, both fixed:
- bwrap masker: `_ensure_executable_visible` emitted no explicit binds
for an interpreter that cwd nominally covers, so the `--tmpfs` mask
hid it with nothing to restore it. Now, after the mask, re-expose the
interpreter (and target) chain scoped strictly inside the masked dir,
so it layers over the mask and reaches exactly the interpreter subtree
— `.local` stays masked, only the interpreter dirs poke through.
- claude-sdk cwd: a relative `os_env.cwd` (the default ".") resolved
against `os.getcwd()` landed on the runner daemon's $HOME when no
workspace was selected — rooting the sandbox at the whole home dir and
disagreeing with the tmux terminal. Resolve relative cwds against
OMNIGENT_RUNNER_WORKSPACE (both sandbox-wrapping paths) and fall the
harness CLI cwd back to it, mirroring the kimi/pi/hermes harnesses.
Signed-off-by: Aditya Devarapalli <adityareddyd2@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(seatbelt): allow file-read-metadata globally so Bun's startup fstat() survives the sandbox
The bundled `claude` CLI runs on Bun. Bun's WriteStream constructor calls
fstat(2) on its inherited stdout/stderr pipe file descriptors at startup for
ANSI-color / TTY detection (internal:util/colors, fs/streams:244). Pipe fds
have no filesystem vnode path, so they match no path-scoped
`(allow file-read-metadata "...")` literal. Under the seatbelt profile's
deny-by-default policy the fstat returns EPERM, crashing the Bun process
before it emits any stream-json. The SDK connect handshake then never
completes and dies with "Claude SDK connect timed out after 60s". The failure
presents as a network/timeout bug but is a sandbox denial on a metadata syscall.
Only reproducible on the intersection macOS + darwin_seatbelt + claude-sdk;
with `sandbox.type: none` the same run succeeds, confirming the sandbox (not
the harness/auth) is the cause.
Fix: grant `file-read-metadata` globally (no path filter) in the SBPL
baseline, right after the existing global `(allow file-ioctl)`. This allows
fstat() on any fd including pipes. It grants inode metadata only
(stat/fstat/access/getattrlist) and does NOT grant file data access
(file-read* is unchanged), directly analogous to the baseline's existing
global `(allow file-ioctl)`.
Security note (stated honestly): this widens a metadata oracle — a sandboxed
agent can confirm file existence anywhere on the filesystem (it still cannot
read contents). Acceptable for single-tenant developer/operator use; an inline
caveat flags it for multi-tenant deployments, where maintainers may prefer a
narrower scope (metadata only on the inherited fds, or scoped to the sandbox's
own tree).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): add CLAUDE_CODE_OAUTH_TOKEN to the local daemon env allowlist
CLAUDE_CODE_OAUTH_TOKEN is in HARNESS_CREDENTIAL_ENV_VARS
(omnigent/host/connect.py) so _build_runner_env forwards it host->runner, and
an existing comment there already notes it is needed "for `claude setup-token`
subscription auth". But the daemon env is built earlier by
_build_host_daemon_env (omnigent/cli.py), which admits only
_RUNNER_ENV_ALLOWLIST + _LOCAL_DAEMON_ENV_ALLOWLIST. CLAUDE_CODE_OAUTH_TOKEN
was in neither list, so it was stripped from the daemon's environment at
launch. The daemon then came up without the token, and _build_runner_env had
nothing to forward — the HARNESS_CREDENTIAL_ENV_VARS membership was moot
because the value had already been dropped one layer up.
Net effect: on a local (non-cloud) macOS run with the managed daemon, a
claude-sdk agent authenticated via `claude setup-token` (subscription) behaves
as if it has no credentials. ANTHROPIC_API_KEY does not hit this because it IS
in _LOCAL_DAEMON_ENV_ALLOWLIST — which is exactly why API-key auth works and
subscription auth doesn't.
Fix: add CLAUDE_CODE_OAUTH_TOKEN to _LOCAL_DAEMON_ENV_ALLOWLIST so it survives
the cli->daemon env strip and is then available for _build_runner_env to
forward to the runner.
Security: it's a credential and is treated as one — it joins the same
allowlist that already holds ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN and the
other provider keys. No new class of secret is exposed; a subscription token is
placed on identical footing to the API key alongside it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On signed, packaged macOS builds, registerWebAuthn() called
app.configureWebAuthn(...), enabling the macOS Secure-Enclave platform
authenticator. That routes the whole WebAuthn ceremony through Apple's
provider, which cannot complete a roaming USB security-key request (e.g.
YubiKey) against a third-party SSO relying party (Okta) — the ceremony dies
with an opaque NotAllowedError ("The operation either timed out or was not
allowed").
Remove the platform-authenticator machinery entirely (per review), rather
than gating it. The platform authenticator served no supported Databricks
sign-in path: Touch ID sign-in goes through Okta FastPass (Okta Verify over
the localhost loopback — handled by the LNA-permission code in main.js,
unrelated to WebAuthn), and browser-registered passkeys are invisible to the
Electron keychain access group anyway. With it gone, security keys always
drive Chromium's built-in CTAP path, so YubiKey/opt-out sign-in works.
Removed:
- registerWebAuthn(), the WEBAUTHN_KEYCHAIN_ACCESS_GROUP constant, and the
call site in app.whenReady().
- The now-dead keychain-access-groups entitlement (entitlements.mac.plist)
and its Developer ID provisioning profile (signing/omnigent.provisionprofile
+ the provisioningProfile ref in package.json), which existed solely for
this feature. Removing them also eliminates the documented AMFI-SIGKILL
foot-gun those three coupled pieces created.
- The stale Passkeys (WebAuthn) section in README.md, rewritten to explain
why the platform authenticator is intentionally not enabled.
- The keychain-access-groups example in entitlements.mac.inherit.plist,
replaced with a general restricted-entitlement caution.
Because no restricted entitlements remain, a Developer ID certificate alone
is sufficient for signing — no embedded provisioning profile is needed.
Co-authored-by: Isaac <isaac@omnigent.ai>
The model-setup add menu offered both "Gateway — custom base URL + key
(e.g. OpenRouter)" and a standalone "OpenRouter — API key" option, which
read as two ways to do the same thing and confused users during setup.
Drop OpenRouter from the Gateway label and description; users who want
OpenRouter should pick its dedicated option.
Co-authored-by: Isaac
Signed-off-by: Dhruv Gupta <dhruv.gupta@databricks.com>
N/A
## Summary
- Adds `omnigent://<hostname>/c/<session_id>` deep links to the iOS app, mirroring the Electron desktop shell (`designs/desktop-deep-link.md`): an OS-routed link opens that session on that server.
- Window handling: same-server → navigate in-place via the SPA router (no reload), deferred until the page finishes loading so a cold-start link isn't lost; known server (in recents / saved) → switch + load the conversation directly, no prompt; unknown server → native confirmation (pinning a new origin is a privilege grant), with the workspace-mount probe running ONLY after consent so a link to an attacker-chosen host makes no pre-consent network request.
- The conversation path never enters the saved server URL or recents (only the load URL carries it), so a later deep link resolves against a clean server identity; a new `omnigent:open-path` main→renderer channel (separate from the notification channel) routes in-place.
## Test Plan
- `xcodebuild build -project web/ios/Omnigent.xcodeproj -scheme Omnigent -destination 'platform=iOS Simulator,name=iPhone 17'` → BUILD SUCCEEDED.
- `xcodebuild test -only-testing:OmnigentTests ...` → TEST SUCCEEDED; 21 tests pass (8 new DeepLinkTests, 2 new SettingsStoreTests for knownServerURL, 11 existing), 0 failures.
- swift-format + swift-format lint + prettier pre-commit hooks pass on all changed files.
- Manual (simulator): `xcrun simctl openurl booted 'omnigent://<reachable-https-host>/c/<id>'` — same-server navigates in-place; a known server switches to it; an unknown server shows the consent alert. Requires the web UI rebuilt (`cd web && npm run build`) so the served SPA has the `onOpenPath` subscriber.
## Demo
N/A — no visible UI change beyond in-app navigation / a consent alert triggered by an external link. (QR-code scanning routes through the same `.onOpenURL` path, so a QR encoding the link opens the installed app identically.)
## Type of change
- [ ] Bug fix
- [x] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Unit tests cover the pure parser (`DeepLinkTests`: scheme inference, port preservation, IPv6, trailing-slash normalization, rejections) and the known-server lookup (`SettingsStoreTests.knownServerURL`). The orchestration (`AppRootView.handleDeepLink`, the SwiftUI `.onOpenURL`/alert wiring, in-place deferral in `WebShellView`) isn't unit-testable without a UI harness, so it was verified by a clean build + simulator `simctl openurl` dispatch on a reachable https server.
## Changelog
`omnigent://<hostname>/c/<session_id>` links open that session in the iOS app, reusing the open window on that server in-place
- Route the provider-neutral composer surface through a generic goal API facade while preserving the Codex backend
- Rename goal components, state, selectors, and tests without changing the Codex-only capability gate
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
* Add cross-replica live-state mirror for the session sidebar
Under replica sharding, a session list / WS /v1/sessions/updates request can
land on any replica, but the sidebar's live fields — runner_online, turn
status, and the pending-approval count — historically lived only in the
in-memory caches of the replica holding a session's runner tunnel. This
mirrors them to three nullable columns on omnigent_conversation_metadata,
written by the tunnel-holding replica and readable anywhere:
- runner_last_seen: epoch seconds the bound runner's tunnel was last seen;
runner_online is derived from freshness (90s TTL), so an ungraceful
death self-corrects. Stamped on connect and each runner-tunnel ping-loop
tick (inside the handler's workspace_scope), cleared on graceful disconnect.
- live_status: last relay-observed turn status (enum_codecs.SESSION_LIVE_STATUS).
- pending_elicitation_count: outstanding approval-prompt count.
Writes funnel through one best-effort chokepoint (server/session_live_state.py):
ordered (single-worker executor), deduplicated, off the event loop, and run
inside a copy of the caller's contextvars so the per-request workspace_scope —
which every store query filters on — reaches the worker thread. A bare executor
would run the write at the default workspace, so on a multi-tenant replica every
UPDATE ... WHERE workspace_id == ... would match no rows and the mirror would
silently no-op; the read path (_bulk_session_liveness via asyncio.to_thread)
already propagates the context, so this makes the write path symmetric. A
dropped best-effort write evicts its dedupe entry so the next identical publish
retries rather than being swallowed. Writes never bump conversations.updated_at
(it drives sidebar ordering). The read path checks the in-memory registry first
and falls back to the row's freshness, so a replica that doesn't hold the tunnel
still reports correctly. The unread-dot baseline moves client-side (localStorage
+ server-seed max-merge) so it no longer depends on the serving replica.
Migration d7f1a2b3c4e5 adds the three nullable columns; NULL degrades to
today's behavior. This is the OSS SQLAlchemy path only — the managed EStore
store implements the same abstract methods separately, and host_id slice-key
routing is a separate PR.
Tests: workspace-scoped store round-trip through the chokepoint (fails on a bare
executor, passes with copy_context), contextvar propagation, ping-loop re-stamp,
dedupe stale-on-drop eviction, and cross-replica /health derivation from a
fresh / past-TTL / cleared row.
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* Drop the drain_for_tests hook; tests poll the observable effect
Remove the test-only drain_for_tests() from the production session_live_state
module — a test seam has no business in the shipped chokepoint. Tests now wait
on the observable effect of each background write (the recording store's
captured writes, the DB row, or the dedupe-map eviction) with a short polling
deadline, mirroring the host-tunnel route tests' _wait_* helpers.
The dedupe stale-on-drop test now gates its retry on the dedupe entry actually
leaving the map (the exact contract under test) rather than on the first store
call, closing a race the drain hook had been masking.
No production behavior change; 225 affected tests pass.
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* Drop unencodable live statuses before enqueue
persist_live_status forwarded any relay-observed status straight to the
store, but SessionStatusEvent.status permits "launching" (runner-local
sub-agent bookkeeping) which the live-status codec can't encode. Enqueuing
it made the store write raise; the best-effort failure hook then cleared
the dedupe entry, so every republish re-attempted and re-logged rather than
settling.
Guard in persist_live_status: statuses outside the codec's known set
(derived from SESSION_LIVE_STATUS so the two can't drift) are dropped before
the enqueue, warned once (deduped), and never reach the store. Latent today
(no producer emits "launching" as an external session.status), addresses a
Polly non-blocking note.
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* Update sidebar unread-dot e2e for browser-durable read-state
The mark-unread e2e's docstring asserted the OLD contract — read-state is
server-backed with "no localStorage", so a dot reappearing after reload
proved the server round-trip. This PR inverts that: read-state is now
localStorage-durable, mirrored best-effort to a per-replica server copy.
Rewrite the docstring to the new contract and add a case that pins the
pod-independence: after mark-unread + reload, stub GET /v1/sessions to
return viewer_unread=false / viewer_last_seen=null (a replica whose seed
never saw the PUT), and assert the dot still lights — proving it was
restored from localStorage, not the server seed. Fails on pre-localStorage
code (read-state-less seed → row reads seen → no dot).
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* Fix flaky live-state chokepoint test: wait for all writes, not the first
test_live_state_writes_via_chokepoint_land_in_scoped_workspace enqueues
three writes on the chokepoint's ordered single-worker executor
(touch_runner_liveness, persist_live_status, persist_pending_count) but
polled only for the first (runner_last_seen) before asserting all three.
On a loaded CI runner (Pytest stores shard, 8-way xdist) the read raced
the later two, so live_status read None -> "assert None == 'running'".
Poll until ALL three fields are observed, and raise the deadline (2s to
10s; a passing predicate returns immediately, so the ceiling only matters
on a real failure). Also raise the _wait_until default in the live-state
unit tests to 10s for the same load-robustness. Verified: 162 passed 3x
under 8-way parallel pytest, and 15x sequentially on the target test.
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
* Gate persisted pending-count fallback on runner binding
_build_session_list_item merged the in-memory elicitation index with the
persisted row via max(index, row). For an UNBOUND session that produced a
load-dependent flake: resolve() drops the index to 0 synchronously, but the
row's 0-write is async on the live-state executor, so a list read that beat
the write saw max(index=0, row=1)=1 — a stale-high badge. Deterministic
locally (fast SQLite), it surfaced under the stores/server-integration
shard's 8-way parallelism as "assert 1 == 0".
The persisted count is a CROSS-REPLICA mirror: only meaningful when a runner
tunnel exists on some replica, whose holder writes the row and whose
non-holders fall back to it. An unbound session (no runner_id) has no tunnel
anywhere, so the local index is authoritative and the lagging row must not
override it. Consult the row only when conv.runner_id is not None; otherwise
use the index directly.
Adds test_list_sessions_pending_count_falls_back_to_row_for_bound_session
pinning the fallback still fires for a bound session (index empty, row set),
complementing the existing unbound/index-authoritative test. Verified: full
server-integration suite 867 passed under -n 4, and the unbound test 20x with
no flake (row column never read on that path -> timing-independent).
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
---------
Signed-off-by: Edwin He <41037314+Edwinhe03@users.noreply.github.com>
A slow or unreachable Omnigent server made bind_session_runner leak a raw
httpx transport exception, so the CLI printed a full traceback (e.g. bare
`omnigent` -> run -> bind against a degraded backend) instead of an
actionable message.
Wrap the PATCH call and map each transport failure to a clean
ClickException, distinguishing unreachable (connect error / connect
timeout -> check URL & connection) from reachable-but-slow (read timeout
-> retry shortly). Honors the function's documented contract.
* fix(server): ask the host if a runner is coming before the connect grace
A host-bound session's first message waits up to _HOST_BOUND_RUNNER_CONNECT_GRACE_S
for the pinned runner's tunnel to register before relaunching. That wait is
correct for a booting new-session runner but pure latency for one that will
never connect — a non-sticky Stop dropped it, or the host restarted and lost
it. Neither case writes a host.runner_exited report (Stop pops the runner
before terminating; a dead host never sends the frame), so the old blind wait
burned the full grace every time on restart.
The host is the authoritative owner of runner-process liveness — it holds the
Popen. Add a host.runner_status frame pair so the server can ask: alive
(booting/serving → wait), dead (tracked but exited → relaunch now), or unknown
(stopped, crashed, or lost to a host restart → relaunch now). The dispatch
path races this query against the connect grace: the runner connecting (or a
crash report) always wins if it lands first, and a dead/unknown verdict cuts
the wait short so the relaunch runs immediately. Running the query alongside
the wait — not before it — keeps it strictly a speed-up: a host that is
offline, too old to answer, or slow yields no verdict and the grace runs its
normal course with no added latency.
Host-absent at dispatch skips the grace entirely (there is no one to query)
and falls through to the existing relaunch/503, unchanged.
Co-authored-by: Isaac
* Address code-quality review on the runner-status query
- Drain cancelled tasks via asyncio.gather(..., return_exceptions=True)
instead of `await task` inside contextlib.suppress, in both the race
helper and the integration test. Functionally identical, but avoids the
bare-expression-statement the static analyzer flagged as "no effect"
(it doesn't model `await` as side-effecting).
- Harden _query_host_runner_status: map any unexpected exception (e.g. a
future resolved with an error) to None so the query can only ever speed
up the connect grace, never break the message POST. CancelledError stays
a BaseException and still propagates, so the race helper's cancel/drain
is unaffected. Covered by a new test that resolves the pending future
with an exception.
Co-authored-by: Isaac
* test(e2e): stub /health so the host-badge push test isolates useHosts status
test_hosts_changed_frame_updates_host_badge failed at its first
assertion (before any hosts_changed frame): the badge read "status
unknown" instead of the stubbed "online". The test intercepts the
WS /v1/sessions/updates stream to keep liveOnline undefined, but the
open-session GET /health poll is a second, independent source of
host_online — and the real endpoint emits host_online: null for a
session it finds without a host binding. That null reaches
useSessionHostOnline as a live signal, which HostBadge treats as
authoritative "unknown", overriding the useHosts status the test drives.
Patch /health to drop the seeded session from the batch sessions map so
useSessionHostOnline stays undefined ("not observed yet") and the badge
falls back to the useHosts status field — matching the test's stated
intent and the existing snapshot/list route patches. HostBadge behavior
is unchanged; this only repairs the test's mock world, which had the
snapshot claiming host-bound while /health said otherwise.
Co-authored-by: Isaac
* ci(benchmark): allow dispatching against a specific commit SHA
Add an optional `checkout_sha` workflow_dispatch input wired into the
checkout step's `ref`, so an ad-hoc benchmark run can be pinned to any
commit while the workflow definition still comes from the trusted
dispatch ref. Blank falls back to the ref HEAD (schedule/default).
Also key the concurrency group per run (run_id / pinned sha) so repeated
manual dispatches on the same ref no longer cancel each other — needed
to collect multiple data points per commit for regression A/B testing.
Co-authored-by: Isaac
* ci(benchmark): key dispatch concurrency purely on run_id so repeats never cancel
Co-authored-by: Isaac
* perf(web): reduce GET /v1/sessions calls on session detail page
- useConversations: add staleTime 30s so components that mount in quick
succession (AppShell, Sidebar, ChatPage) share the cache instead of
each triggering a background refetch
- useConversations: bump page limit 20 → 30 to reduce second-page fetches
- useAgents: staleTime Infinity (data is driven by explicit refetch only)
- ChatPage: disable useAgents on session detail page (enabled: !urlConvId)
— useSessionAgent covers the bound agent there; useAgents is only
needed on the landing page agent picker
* ci(benchmarks): add PR and release benchmark gate workflows with compare script
Adds compare.py for detecting performance regressions between benchmark
JSON reports, plus two CI workflows: benchmark-pr.yml (runs on PRs touching
migration files, posts results as a PR comment) and benchmark-release.yml
(runs on release/v* pushes and blocks on regression).
* ci(benchmarks): add PR migration gate and integrate release benchmark into release.yml
- compare.py: compare two benchmark JSON reports, exit 1 on regression
- benchmark-pr.yml: block PRs touching migrations if >20% p50/p99 slowdown vs latest nightly
- release.yml: add benchmark job between plan and cut; compares release commit vs previous stable tag on the same runner, blocks cut on regression; skip_benchmark escape hatch mirrors skip_ci_check
* fix(benchmarks): fix ruff E501 lines and None guard in compare.py
* ci(benchmarks): raise threshold to 100%, add approval gate for release regressions, add stores path trigger
* ci(benchmarks): trigger PR benchmark when benchmark-pr.yml is edited
* ci(benchmarks): match nightly iterations in PR benchmark (100 iter × 3 runs)
* fix: split markdown header string at natural column boundary (ISC warning)
* ci(benchmarks): match nightly seed corpus (5000×200) in PR benchmark for comparable baselines
* ci(benchmarks): seed 5000×200 corpus in release benchmark, match nightly iterations (100×3)
* ci(benchmarks): switch regression metric from P99 to P95
* perf(web): replace GET /v1/hosts 10s poll with WS push
Host connect/disconnect events now flow through the existing
WS /v1/sessions/updates stream as a new hosts_changed frame:
- host_tunnel.py: pass owner to on_host_connect/on_host_disconnect
callbacks (avoids a DB lookup in the callback)
- sessions.py: add announce_hosts_changed(); extend _discovery() to
forward hosts_changed events as WS frames to the client
- app.py: wire on_host_connect/on_host_disconnect to call
announce_hosts_changed so the owner's open tabs invalidate immediately
- sessionUpdatesSocket.ts: add hosts_changed to SessionUpdatesFrame
- SessionUpdatesProvider.tsx: invalidate ["hosts"] on hosts_changed
- useHosts.ts: staleTime 10s→30s, refetchInterval 10s→60s fallback
(WS push handles the common case; poll catches missed events)
* test(e2e): add UI e2e for hosts_changed WS push → host badge update
* feat(files): serve session filesystem from host when runner is offline
When a session's runner process dies but its host is still connected,
the file panel (browse / changed files / diffs / search / file content)
used to go dark — every request 502/503'd and the user had to send a
message to wake a new runner just to look at files.
The server now falls back to reading the workspace over the existing
host tunnel when the pinned runner is offline. A shared, read-only
WorkspaceReader (confined to the workspace root) runs on the host and
returns the same JSON shapes the runner's filesystem endpoints do, so
the resolver (live runner -> host tunnel -> 503) and the frontend can't
tell which side answered. The panel stays live with a passive "Asleep —
files shown live from host" badge; no LLM, no wake-up.
Built as a resolver chain so a future host-death snapshot source drops
in as an additive third link without touching endpoints or the frontend.
- omnigent/workspace_fs.py: read-only WorkspaceReader (list/read/search/
changes/diff), reusing the runner's path-validation, glob, pagination,
and git change-registry helpers.
- host tunnel: host.fs_request / host.fs_result frames + host handler +
server-side proxy and pending-future routing.
- server: _fs_get_with_host_fallback wraps the 5 FS GET endpoints;
offline env-metadata is synthesized from the bound workspace.
- web: useWorkspaceServeable gate (runner-online OR host-online, tri-state
aware) replaces the runner-only gate across the FS hooks; host-served
badge in FilesPanel.
Test Plan: backend unit + integration (real host tunnel, offline runner,
real git workspace), frontend hook unit tests, and e2e_ui (real browser)
covering the file list + content viewer while the runner reads offline.
Co-authored-by: Isaac
* fix(files): address host-served FS review notes (bounded read, parity)
Follow-up to the PR review on the host-served filesystem path:
- WorkspaceReader now reads at most _MAX_READ_BYTES from disk (via a
bounded open().read) in both _read_file and diff's `after`, instead of
slurping the whole file — a multi-GB file opened while the runner is
asleep can no longer OOM the host process. Matches the runner's cap.
- _list_dir falls back to lstat for a broken symlink and lists it as
type="file"/bytes=None instead of silently dropping it — restores the
parity the docstring claims with the runner's list_dir.
- Host FS failures now mirror the runner proxy's status mapping: a
non-404/400 host error (e.g. git_status_failed) surfaces as 502 like
_proxy_get_to_runner, and a 400 stays a 400.
- Log a warning when a host fs op times out (the module's _logger was
previously unused); drop a dead `text = ""` assignment.
Adds tests for the oversize-read cap and the broken-symlink listing.
Co-authored-by: Isaac
* fix(files): keep oversize text as UTF-8 when truncation splits a codepoint
Follow-up to the PR review: WorkspaceReader._file_content_payload sliced
the read at _MAX_READ_BYTES on a raw byte boundary, so a text file larger
than the cap whose cut fell inside a multi-byte UTF-8 codepoint raised
UnicodeDecodeError and was served base64 — diverging from the runner,
which truncates on a valid boundary and keeps encoding="utf-8".
Now, when we truncated and the only invalid bytes are a partial trailing
codepoint (error within the last 3 bytes), drop them and re-decode as
text. A genuinely binary file has invalid bytes earlier in the buffer, so
it still falls through to base64. Adds tests for both.
Co-authored-by: Isaac
On the iOS native app, the file viewer is a `fixed inset-0` overlay, so
the iOS shell-lock (useIOSViewportLock, which only resizes flow content
inside .app-shell) can't lift it above the soft keyboard. When a user
selected text to comment, the auto-focused textarea in the bottom
comments panel sat behind the keyboard with no way to scroll to it.
Pad the mobile overlay's bottom by the keyboard inset (via the existing
useIOSNativeKeyboardInset hook that TerminalsPanel already uses) so the
comments panel and its textarea stay visible. No-op off iOS, on desktop,
and with the keyboard closed.
Co-authored-by: Isaac
* feat(ci): draft feature-blog posts at release cut
Add an automated feature-blog pipeline mirroring the existing doc-sync /
release-notes automation. At release cut (same workflow_run trigger as
draft-release-notes.yml), a scout agent selects the release's blog-worthy
features and a drafter agent writes one post per feature into omnigent-site
as a DRAFT PR — leaving the mandatory demo, hero art, and byline for a human.
- feature-blog-scout: no-tools selector; a >=2-of-4 signal bar, capped at 3,
emits a ranked BLOG_CANDIDATES block (usually empty).
- feature-blog-drafter: writes a short one-screen post following the 5-part
skeleton, marks DEMO REQUIRED, defaults author to "omnigent".
- feature-blog.yml: reuses generate.py's PR-range harvest, runs the two
agents, appends a fixed CTA footer, mints the omnigent-site App token only
after the agents finish, and opens a draft PR per feature. Idempotent;
workflow_dispatch supports dry-run testing against past releases.
Co-authored-by: Isaac
* fix(ci): address Polly review on feature-blog workflow
- Fix nested material-assembly heredoc: the unquoted delimiter let the
markdown code fences be backtick-command-substituted, silently dropping
every PR diff from the drafter's material. Quote the delimiter and pass the
candidate index + repo via env; build fences from a variable.
- Secret-scan the drafter output before it feeds the PR body, and scan the
drafted files (incl. untracked) before commit/push — the drafter runs with
LLM_API_KEY in env and its stdout reaches the PR description.
- Derive the post DATE from the release tag's commit in the omnigent checkout,
not the omnigent-site checkout's last-commit date.
- Warn loudly when posts were drafted but no App token is available, so a
misconfig isn't mistaken for "no candidates".
Co-authored-by: Isaac
* fix(ci): fix no-candidate job failure and harden feature-blog workflow
Address the second Polly review:
- B1: the mint/PR/warn steps gated on `drafted != '0'` fired on the common
no-candidates release, because a SKIPPED draftposts step reports an empty
output and '' != '0' is true — minting an unnecessary token and then failing
the job on a missing drafted_branches.txt. Gate on
`draftposts.outcome == 'success' && drafted not in ('', '0')` instead.
- B2: reset + clean the omnigent-site worktree at the top of each candidate so
a drafter that fails AFTER writing its post can't bleed that untracked file
into the next feature's commit/PR.
- S1: validate the scout's LLM output before it becomes a path/branch/fetch —
require `slug` to be strict kebab-case (blocks ../, slashes, spaces) and
intersect `pr_refs` with the harvested PR set (blocks arbitrary gh pr diff).
- Make the drafter secret-scan fail-closed even when the drafter exits
non-zero (capture rc, scan, then skip) — tee wrote its stdout either way.
Co-authored-by: Isaac
* OMNI-1193: add recurring-task scheduler engine
Add the in-process cron scheduler for Routines (PR2). It decides *when*
each active scheduled task fires and invokes an injected on_fire callback;
creating the agent session is left to a later PR.
- omnigent/server/automations/cron.py: self-contained 5-field POSIX cron
parser, timezone-aware next-fire computation (POSIX DOM/DOW union,
366-day never-fires bail-out), and a validator enforcing a 5-minute
minimum interval and rejecting never-fires / fires-once expressions.
- omnigent/server/automations/scheduler.py: AutomationScheduler holding
one self-rearming timer per active task, loaded on boot from
store.list_active(). SKIP overlap policy (max_instances=1), misfire
grace window, 24-day timer cap with re-arm, and add/update/remove
CRUD-sync methods. Timing seams (now/schedule_call/cancel_call) are
injectable for deterministic tests.
- Wire into the FastAPI _lifespan: start on boot, stop on shutdown,
following the publish_server_metrics_periodically precedent. create_app
takes a scheduled_task_store kwarg; cli.py constructs the store. PR2
supplies a placeholder on_fire seam for PR3 to replace.
Tests: exhaustive cron parsing/next-fire/floor/timezone; scheduler
boot-load/fire/overlap/misfire/CRUD with a fake clock + fake callback;
lifespan wiring against a real store. 52 new tests, all green.
Co-authored-by: Isaac
* OMNI-1193: strip internal phasing from scheduler comments
Reword scheduler/lifespan comments and docstrings to describe what the
code is (an injected on_fire callback whose default is a no-op that
logs) rather than internal PR sequencing. Comment/docstring-only; no
logic change.
Co-authored-by: Isaac
* fix(automations): make cron interval validation deterministic + isolate scheduler boot
The 5-minute minimum-interval floor is the cost-control guarantee for
Routines (each fire spawns a real agent), but validate_cron could be
bypassed two ways: it anchored sampling at datetime.now() (so the same
expression passed or failed depending on the wall-clock minute), and it
only measured the gap between the first two fires (so an irregular
cadence like `0,1 * * * *` hid its 60s pair behind a 3540s first gap).
Anchor the interval check at a fixed UTC instant (a leap year, so
Feb-29 expressions still reach their single fire and are rejected as
"fires only once" rather than "never fires") and take the minimum gap
across every consecutive pair in a bounded 25-hour window. Validation
is now deterministic and DST-agnostic.
Also isolate the scheduler from server boot: wrap
automation_scheduler.start() in log-and-continue so a DB error while
loading the schedule can't take down startup of the whole server.
Drop a false DST-fold comment in get_next_fire_time (the return value
was already timezone-aware; the .replace(tzinfo=tz) was a no-op).
Co-authored-by: Isaac
* feat(automations): raise minimum routine cadence from 5 minutes to 1 hour
Each routine fire spawns a real agent session, so hourly is now the
tightest cadence we allow. Raise MIN_INTERVAL_SECONDS from 300s to
3600s and update the derived error message, DST comment, and floor
tests. The scheduler tests' fixture crons (*/5) and the misfire test's
clock-advance are retuned to a valid hourly cadence, since they are no
longer arm-able under the new floor.
Co-authored-by: Isaac
* fix(automations): use valid uuid agent_id in scheduler lifespan test
The two ScheduledTask fixtures in test_scheduler_lifespan.py hardcoded
agent_id="ag-1", which is not a valid UUID. Local SQLite tolerates the
short string, but the server-integration CI backend validates the id
and rejects anything that isn't a canonical UUID, failing both
test_lifespan_starts_and_stops_scheduler and test_lifespan_skips_paused_task.
Use the file's existing _uid() helper so the agent_id matches the same
UUID form already used for scheduled_task_id.
Co-authored-by: Isaac
* refactor(scheduled): rename automations dir/class to scheduled for consistency with ScheduledTask model
Align the scheduler layer with the already-merged persistence canon
(ScheduledTask / scheduled_tasks / ScheduledTaskStore): move
omnigent/server/automations/ -> omnigent/server/scheduled/ (and the
mirror test dir), rename AutomationScheduler -> ScheduledTaskScheduler,
and the app.state attribute / lifespan var automation_scheduler ->
scheduled_task_scheduler. No behaviour change.
Co-authored-by: Isaac
* docs(scheduled): use "scheduled tasks" naming in comments, drop "Routines"
Omni's canonical name for this feature is "scheduled tasks". Reword the
scheduler docstrings and inline comments to match, dropping the
"(Routines)" parenthetical that referenced another codebase's label.
Comment/docstring text only — no identifiers or behavior changed.
Co-authored-by: Isaac
* feat(scheduled): rewrite scheduler engine to use RRULE via dateutil
Replace the hand-rolled 5-field cron parser with RFC 5545 recurrence
rules evaluated by python-dateutil, matching the product decision to
switch scheduled tasks from cron to RRULE.
- Rename cron.py -> rrule.py; delete the cron parser (parse_cron,
_parse_field, ParsedCron, CronField, _day_matches) and the
minute-by-minute field walk.
- Next-fire now anchors the rule at midnight of the reference day in
the task timezone and uses rrulestr(...).after(); returns None when
a COUNT/UNTIL rule is exhausted.
- validate_cron -> validate_rrule keeps the 1-hour floor, never-fires,
and fires-once rejections, sampled from a fixed 2016 UTC anchor so
the verdict is wall-clock-independent; CronValidationError ->
RRuleValidationError, CronTrigger -> RRuleTrigger.
- Scheduler reads task.rrule (+ task.timezone); timer/overlap/misfire
behavior unchanged.
- Rewrite tests in RRULE terms; scheduler tests use a local fake task
so they don't depend on the entity field rename.
Co-authored-by: Isaac
* refactor(scheduled): unwire cli store; declare python-dateutil dep; note INTERVAL phase drift
PR2 is the pure scheduler engine and must not construct or boot the
scheduler on any entrypoint while on_fire is still a no-op. Remove the
scheduled-task store construction and the create_app kwarg from the CLI
entrypoint (the only entrypoint that was wired); the create_app
dependency-injection seam in server/app.py stays, awaiting the fire-path
PR that wires all entrypoints together.
Also fold in two fixes from the review:
- Declare python-dateutil (>=2.8,<3) as a core dependency. rrule.py
imports it at module top and app.py imports the scheduler at module
level, so dateutil is now on the core server boot path; it was only
present transitively via optional extras, so a base install would
ImportError on boot. Lockfile regenerated (no version churn — the
package was already pinned transitively).
- Document the INTERVAL>1 phase-drift caveat at _anchor_dtstart:
midnight re-anchoring is deterministic for INTERVAL=1 rules, but
biweekly/interval-monthly rules tie phase to the re-arm day and can
slip a period across restarts. Comment only; a proper fix (stable
per-task dtstart) belongs to a later PR.
Co-authored-by: Isaac
* fix(scheduled): make scheduler start() idempotent (guard against duplicate timers)
start() now early-returns when already started instead of re-loading the
store and layering a second set of timers on top of the live jobs. Adds a
regression test proving a second start() arms no new timers and that a
stop() -> start() re-cycle still re-arms cleanly.
Co-authored-by: Isaac
* docs(scheduled): drop internal process verbiage from scheduler comments
Reword two comments to neutral "future work"/"row changes" phrasing so
they don't leak internal process language into the codebase. Comment-only;
no behavior change.
Co-authored-by: Isaac
* perf(web): reduce GET /v1/sessions calls on session detail page
- useConversations: add staleTime 30s so components that mount in quick
succession (AppShell, Sidebar, ChatPage) share the cache instead of
each triggering a background refetch
- useConversations: bump page limit 20 → 30 to reduce second-page fetches
- useAgents: staleTime Infinity (data is driven by explicit refetch only)
- ChatPage: disable useAgents on session detail page (enabled: !urlConvId)
— useSessionAgent covers the bound agent there; useAgents is only
needed on the landing page agent picker
* perf(web): skip list refetch when active session is missing from cache
When opening a session, its updated_at bumps before the initial
conversations fetch returns, causing it to appear in missingIds in
the WS snapshot handler and triggering a second GET /v1/sessions.
The active session's data is covered by useSession and it's pinned
in the sidebar via ActiveChatOverride, so no list refetch is needed.
`session.status: failed` already carries a structured `error` payload
from the server, but the frontend dropped it at every layer: the
`SessionStatusEvent` type had no `error` field, the SSE parser didn't
extract it, and the store handler never synthesized an `ErrorBlock`.
Startup failures (e.g. Databricks OAuth token expiry) never emit a
`response.failed` event, so the transcript stayed blank until the user
reloaded and the server's `lastTaskError` snapshot caught up.
Fix by threading the `error` field through `SessionStatusEvent` →
`sse.ts` parser → `chatStore` `session_status` handler, which now
appends an `ErrorBlock` immediately when `status === "failed"` and no
error block is already visible.
Codex-native sessions emit plan state through `turn/plan/updated`
app-server notifications, which the forwarder previously mirrored only
as an inline assistant message. Map those plan steps to the same
todo-list schema Claude produces via TodoWrite and post them as an
`external_session_todos` event, so the web TodoPanel renders a Codex
plan the same way it renders a Claude todo list. The plan still appears
inline in the transcript as well.
On the web side, the Tasks tab/drawer gate moves from `isClaudeNative`
to a `todosSupported = isClaudeNative || isCodexNative` flag; the panel
itself is already harness-agnostic.
Co-authored-by: Isaac
* fix(policies): show all policies in Add Policy session dialog
Previously, the per-session Add Policy dialog filtered out policies that
were already applied, making it impossible to add a second instance of
the same policy type.
* fix(tests): update AgentInfo test for show-all-policies behavior
* feat(web): add find-in-file to the markdown & notebook preview
Find in file worked in the markdown editor, source view, and Monaco, but did
nothing in Preview mode — the toolbar toggle (and Cmd+F) opened a bar that
nothing consumed on the rendered-preview surface.
The preview is React-owned DOM (react-markdown / notebook output), so matches
can't be wrapped in spans without fighting React's reconciliation. Instead,
locate matches as DOM Ranges and paint them with the CSS Custom Highlight API
(the same approach htmlCommentBridge uses for the HTML preview), which overlays
styling without mutating the node tree.
Matching mirrors the editor's TipTapSearchExtension: text is flattened across
inline nodes so a term split by formatting (e.g. <em>) still matches, while a
block-tag boundary inserts a separator so a match never spans two blocks. Same
length-preserving case-fold so Unicode offsets stay aligned. Where the Highlight
API is unavailable, count/navigation still work and only the paint is skipped.
Co-authored-by: Isaac
* fix(web): recompute preview find ranges post-commit, not during render
findTextRanges ran in a useMemo (during render), so on a content change while
the find bar was open the walker saw the previous render's text nodes and built
Ranges into nodes about to be replaced — leaving stale/misplaced highlights.
Move the computation into useLayoutEffect (post-commit) and hold ranges in
state so the walker always sees the committed preview DOM.
Also import RefObject explicitly in NotebookPreview for consistency with the
sibling preview/search modules.
Co-authored-by: Isaac
A native Codex session routed through a Databricks profile could fail every
turn with a gateway 400 "Invalid Token" even though `databricks auth token
--profile <p>` mints a valid bearer. The gateway base URL was resolved via the
databricks-sdk, which lets a `DATABRICKS_HOST` env var (or a different DEFAULT
section) override the profile host — while the auth command pins `--profile`
and ignores `DATABRICKS_HOST`. On a machine whose environment/DEFAULT points at
another workspace, the base URL and the minted token then targeted two
different workspaces and the gateway rejected the token.
Add `_databricks_gateway_host(profile)`: for an explicit profile, read the host
straight from that profile's config section (env-independent, same source the
token comes from); only fall back to the SDK/ambient chain when the section has
no host (e.g. a Databricks App container authenticating via ambient env/OIDC).
Both Codex gateway call sites now use it.
Co-authored-by: Isaac
* feat(web): add find-in-file to the markdown rich-text editor
Find in file worked in Monaco (code) and the markdown source view, but did
nothing in markdown's default Editor mode — the toolbar toggle wasn't consumed
by the TipTap editor, so clicking Find (or Cmd+F) was a no-op.
Add a ProseMirror search-decoration extension (mirroring the existing comment
extension: matches are Decorations, not marks, so they never touch markdown
serialization and remap through edits) plus a find bar reusing the source-view
UI. Highlights all matches, marks and scrolls the current one, cycles with
Enter / Shift+Enter / arrows, and closes on Escape / ✕ / a second Find click —
syncing the toolbar toggle.
Matching flattens each block's inline nodes into a visible-text map, so a term
split across a formatting boundary (e.g. `Hel**lo**`) is found, while a block
separator prevents matches spanning paragraphs. Editor mode only; preview find
is a follow-up that can reuse this matcher.
Co-authored-by: Isaac
* fix(web): trim the markdown find query in the match count too
The "n / m" count computed matches against the raw query while the plugin
highlighted against the trimmed query, so a query with surrounding whitespace
(e.g. "the ") could show a count that disagreed with the highlighted spans and
threw off the current-match modulo. Trim in the count path so both agree.
Co-authored-by: Isaac
* fix(web): keep markdown find positions aligned across case-fold length changes
findMatches searched a toLowerCase() haystack while mapping match offsets back
through a segment map built in original-text coordinates. For characters whose
lowercase form has a different UTF-16 length (e.g. İ U+0130 → i + combining
U+0307), the two coordinate systems diverge, shifting or invalidating the PM
positions of any match after such a character — producing misplaced or
out-of-range decorations. Fold case without changing length instead, so every
offset stays aligned.
Co-authored-by: Isaac
* Add Electron auto-update main process
Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
* Add desktop update renderer UI
Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
* Fix desktop updater review findings
Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
* Keep updater test compatible with main imports
Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
* Format desktop updater files
Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
* test(e2e_ui): cover desktop auto-update UI (banner + settings)
The auto-update work adds a desktop-only UpdateBanner (mounted in AppShell
above the routed Outlet) and a Settings → Updates section, both gated on the
Electron update bridge (window.omnigentDesktop.updates). Only unit tests
covered these, so the E2E UI Required gate flags the web/** change as lacking
Playwright coverage.
Add tests/e2e_ui/desktop/test_desktop_update.py, which injects a scriptable
window.omnigentDesktop stub (with a full updates bridge) via add_init_script —
the same feature-detection stubbing browser/test_browser_tab.py uses — and
drives the real desktop path in a plain Chromium browser:
- banner renders across the available → downloading → downloaded lifecycle,
streamed through the live onStatus subscriber;
- banner actions (Update now, Restart to update, Skip this version) invoke the
matching bridge calls and update the visible state;
- Settings → Updates exposes the mode selector and a working Check button;
- the banner never appears in a plain (non-Electron) browser.
The shell's transparent absolute ChatHeader overlays the banner's band, so
banner-button interactions use dispatch_event("click") to fire the real React
handler; Settings controls sit below the header and use real clicks.
Verified locally: 5/5 e2e pass; tsc -b clean; ruff check/format clean; focused
web unit tests (UpdateBanner, SettingsPage, settingsNav) 71/71 pass.
Signed-off-by: Jackson Zheng <36802691+zhengwin@users.noreply.github.com>
* refactor(desktop): extract auto-updater into desktop_updater module
Desktop auto-update orchestration was ~300 lines of inline state,
electron-updater event wiring, config normalization, manual
check/download/install orchestration, status broadcast/replay, the
consent dialog, and IPC handler registration scattered through
web/electron/src/main.js.
Move all of it into a cohesive web/electron/src/desktop_updater.js
behind a small factory: createDesktopUpdater({ app, BrowserWindow,
ipcMain, dialog, nativeImage, autoUpdater, loadSettings, saveSettings,
isPinnedOriginSender, pinnedOrigin, iconPath, forceDevUpdateConfig }).
Main-process dependencies are injected rather than reaching back into
main.js globals, so there are no circular deps and the module is
directly unit-testable.
main.js now only composes the updater and wires four thin seams:
init() at startup, checkForUpdates/getStatus/installUpdateNow in the
Updates menu, registerIpc() for the update IPC surface, and
quitAndInstallIfPending() in the before-quit handoff. main.js drops
from 3169 to 2912 lines.
No behavior change: every IPC channel name, the consent handshakes,
dev-feed gating, periodic-check cadence, status union, and install
flow are preserved exactly. preload/renderer contracts, Settings UI,
dev-app-update.yml, and the e2e test are untouched.
Tests: add test/desktop_updater.test.js exercising the module API
directly through in-memory fakes (config persistence, event
broadcast/replay, manual-error surfacing, dev-feed gating, IPC sender
trust + consent, install handoff). Retarget the existing
test/update-main.test.js integration harness onto the composed
updater instance, keeping its regression coverage of main.js wiring.
Move the scheduled_tasks recurring trigger from a cron expression to an
RFC 5545 recurrence rule (RRULE) to match the Codex scheduling model.
- db_models.py: rename column cron_expression String(255) -> rrule String(512)
(RRULE strings are longer than cron), update docstrings.
- New Alembic migration a7b3c4d5e6f7 (down_revision z8a2b3c4d5e6): batch-mode
add rrule NOT NULL, drop cron_expression. The table holds zero rows (the
feature is inert — no create endpoint or fire path yet), so this is a pure
DDL swap with no backfill.
- entities/scheduled_task.py: rename field cron_expression -> rrule.
- scheduled_task_store (abstract + SQLAlchemy impl): rename create/update
params and the row<->entity mapping.
- Update store and migration tests to use RRULE strings.
The store does not validate the trigger string (it did not validate cron
either); next-fire/floor validation is owned by the scheduler-engine PR.
Co-authored-by: Isaac
* fix(pi-native): route non-Claude models to correct providers in models.json
Non-Claude Databricks models need different providers depending on their
API compatibility with Pi's openai-completions/responses clients:
1. Newer GPT models (gpt-5-5, gpt-5-6-*, gpt-5-3-codex) reject function
tools via /chat/completions → use openai-responses at /ai-gateway/codex/v1.
2. Kimi, Llama, GLM, older GPT → use openai-completions at /serving-endpoints
with supportsUsageInStreaming:False (Gemini rejects stream_options).
supportsReasoningEffort:False is also required.
3. Gemini 2.5 thinking models return content as an array with thoughtSignature
when tools are present — Pi's openai-completions handler expects a string
and crashes with [object Object]. Excluded from both providers.
Also fixes:
- --provider arg now points to the correct provider for the selected model
(was always 'omnigent', now uses 'omnigent-openai' or 'omnigent-completions')
- model_override from sys_session_create is now respected by the pi-native
launch path (was always using spec.executor.model)
- Non-Claude models are not appended to the Anthropic provider in models.json
* fix(pi-native): suppress defaultThinkingLevel in managed settings for non-Claude models
In TUI mode Pi applies defaultThinkingLevel from settings.json before the
compat supportsReasoningEffort check fires, sending reasoning_effort to the
Databricks gateway which returns 400 for Gemini and other non-Claude models.
Write defaultThinkingLevel: null in the managed settings so Pi's
getDefaultThinkingLevel() returns null (falsy) and no thinking is applied.
* fix(pi-native): don't register unsupported models under Anthropic provider
Gemini 2.5 models excluded from completions/responses providers were
still being appended to the primary Anthropic (omnigent) provider in
to_models_config() as a fallback, causing Pi to call them via
anthropic/v1/messages which Gemini 2.5 doesn't support (400 error).
Also squashes the two recent pi_native_credentials commits into context.
* fix(pi-native): pass --thinking off for non-Claude models to prevent empty turns
Gemini and other Databricks models return reasoning_tokens in their streaming
responses. In TUI mode Pi activates thinking even with defaultThinkingLevel:null
in settings, causing the agent loop to complete without surfacing the text
content to the Omnigent extension (external_session_status running→idle fires
but no external_conversation_item is posted).
Pass --thinking off for any model routed through omnigent-openai or
omnigent-completions providers.
* fix(spawn): remove uniqueItems from file_ids schema
Qwen3, Gemini, and other non-OpenAI models reject JSON schemas with
uniqueItems on array types with 400 'Invalid JSON schema - array types
do not support uniqueItems'. The Omnigent extension registers sys_session_send
as a tool with file_ids having uniqueItems:true, causing all turns to fail.
* fix(pi-native): skip reasoning blocks in textFromContent for o-series models
gpt-oss-120b and similar models return content as a typed array:
[{type:'reasoning',summary:[...]}, {type:'text',text:'Hello!'}]
textFromContent was joining all blocks including reasoning, producing
'[object Object],[object Object]' as the mirrored assistant message.
Skip blocks with type='reasoning' so only actual text blocks are extracted.
* fix(pi-native): exclude gpt-oss models from completions provider
gpt-oss-120b and gpt-oss-20b return content as a typed array
[{type:'reasoning',...},{type:'text',...}] in streaming responses.
Pi's openai-completions handler does block.text += content where
content is an array, producing '[object Object],[object Object]'.
Exclude these models from both providers (same approach as gemini-2-5).
Also bundled the textFromContent reasoning-block fix into this commit
since it's a related improvement.
* fix(tests): update spawn tests for removed uniqueItems on file_ids
uniqueItems was removed from the file_ids schema to avoid breaking
non-OpenAI models that reject JSON schemas with uniqueItems on arrays.
Update tests to match: remove uniqueItems assertion and change the
duplicate-rejection test to confirm duplicates are now allowed.
* perf(web): drop /health bulk poll from NewChatLandingScreen
NewChatLandingScreen was registering up to 200 sessions into the
shared /health fallback poller via useRunnerHealthRegistration, causing
a batched GET /health?session_ids=<100+ ids> every 10 s even while idle
on the home page.
The conflict-occupancy hint only needs runner_online, which is already
present on the Conversation objects returned by useDirectorySessions.
Read it directly from those objects instead of routing through the
health poll.
Also gates useDirectorySessions on selectedHostId != null so no fetch
fires before a host is auto-selected.
* fix(web): restore liveness check for conflict candidates
runner_online is intentionally absent from GET /v1/sessions list rows,
so reading s.runner_online directly always returned undefined (never
true) and silently broke the directory-conflict warning.
Restore useRunnerHealthRegistration for the narrow conflict-candidate
set (host-matched + workspace-bearing sessions only, not all 200) so
liveness comes from the /health poll as before. The bulk poll with 100+
session IDs is still eliminated because candidates are pre-filtered to
the selected host.
* ci: retrigger checks
* style(web): fix prettier formatting in NewChatDialog
* feat(telemetry): propagate host installation ID to SessionCreatedEvent
Adds `installation_id` to `HostHelloFrame` so the host daemon advertises
its local installation ID on connect. The server stores it in the
`HostRegistry` via a new `get_host_installation_id` helper, then passes
it as `host_installation_id` on `SessionCreatedEvent` so hosted sessions
can be correlated back to a specific host machine in telemetry.
* test(telemetry): add tests for host_installation_id telemetry feature
Cover HostHelloFrame encode/decode roundtrip with and without
installation_id, HostRegistry.get_host_installation_id with and
without a registered host, and _build_record promoting
host_installation_id to top-level data rather than params.
Widens the conversation_items primary key to (workspace_id,
conversation_id, id, created_at) and adds created_at to the unique
position index. Nothing is partitioned here: the change makes the
schema partition-ready, so a deployment that needs
PARTITION BY (created_at) can do it with pure DDL — PostgreSQL and
MySQL both require the partition key in the PK and in every unique
index. created_at trails in both keys, so existing per-conversation
prefix scans are unchanged, and it is already NOT NULL and immutable
(items are insert/delete-only), so the rebuild needs no backfill.
Position uniqueness at the DB level becomes per-second; the
next_position counter under _lock_conversation remains the real
allocator. A new test pins created_at immutability, which a future
partitioned deployment depends on.
Co-authored-by: Isaac
The secure repo's validate job red-flagged its first successful publish:
its runners' only index view is the JFrog mirror, whose omnigent
metadata lags weeks behind PyPI, so a just-published version never
becomes visible from CI. The job is removed there; validation is the
manual clean-venv step it always was (run from a network with a fresh
PyPI view — a mirror works, as the rc2 rehearsal proved).
Co-authored-by: Isaac
* ci(homebrew): auto-PR the homebrew-tap formula on release
On a final GitHub Release, regenerate the omnigent Homebrew formula from
the released PyPI sdist closure and open a PR to omnigent-ai/homebrew-tap.
- .github/workflows/homebrew-tap-pr.yml: triggers on release: published
(+ workflow_dispatch for reruns). Polls PyPI for the released sdist,
runs the generator, mints an omnigent-ci App token scoped to homebrew-tap,
and opens a rerun-safe PR (force-push updates an existing one). The tap's
brew test-bot builds the bottles; a maintainer labels pr-pull to merge.
- .github/scripts/homebrew/generate_formula.py: uv pip compile resolves
omnigent[cursor]==<ver> for the macOS arm+intel matrix; each sdist becomes
a resource stanza via the PyPI JSON API. Brewed packages (certifi,
cryptography, pydantic, rpds-py, cffi, pycparser) are excluded — provided
by the formula's depends_on. No-sdist packages (e.g. cel-expr-python) are
skipped with a warning. --proxy routes resolution + metadata through an
internal mirror while rewriting download URLs to files.pythonhosted.org.
- .github/scripts/homebrew/omnigent.rb.template: hand-tuned formula skeleton
(desc, depends_on, install, test) with placeholders for the volatile parts.
No bottle/revision block — brew pr-pull adds those.
* ci(homebrew): add PR dry-run job to iterate on a branch
pull_request runs the workflow from the PR head, so a dry-run job
triggered on PRs touching the homebrew files generates the real formula
against the latest final release on public PyPI (no cross-repo PR),
ruby -c checks it, and it uploads as an artifact. This is the branch
iteration loop — no merge to main needed — mirroring the CI-test-on-PR
pattern in release-omnigent.yml.
* ci(homebrew): label-gated real tap PR from a branch
Add a homebrew-test label trigger to the pr job so a maintainer can
open a REAL PR on omnigent-ai/homebrew-tap from a feature branch
(without merging) — the tap's brew test-bot then builds the bottles.
Deliberate (label-gated) so it doesn't fire on every push; remove +
re-add the label to retrigger. resolve falls back to the latest final
release when there's no event/input tag (the label path). validate
keeps running the no-PR dry-run on code changes.
* ci(homebrew): drop the PR-test scaffolding, production triggers only
The pull_request dry-run + homebrew-test label path were scaffolding to
iterate on a branch before merge. Now that the release path is verified,
strip it: triggers are release: published + workflow_dispatch (reruns)
only, jobs are resolve + pr. Simplifies the resolve tag fallback and the
concurrency group back to the tag-only form.
Both skip mechanisms failed live because the release runners cannot
read the index (no pypi.org egress): the curl probe never matched, and
twine's --skip-existing pre-checks the same JSON API and crashed every
upload. Rewrite the rehearsal's idempotency step as a no-double-publish
check (re-upload must fail with 'File already exists') and mark the
skip-existing decision withdrawn in the design doc. Partial-publish
recovery stays yank + next version, as every release so far has worked.
Co-authored-by: Isaac
The new release.yml derived branch-X.Y names, but every actual release
branch in this repo is named release/vX.Y.0 (release/v0.2.0 through
release/v0.5.0) — the old RELEASING.md's branch-X.Y wording was doc
drift, not practice. Derive release/vX.Y.0, match it in the ci/lint
push triggers, and update the docs.
Also fold the first rehearsal's lesson into the runbook: the throwaway
version must never have touched the destination index (0.0.1rc1 was
spent reserving the PyPI names in June 2026 — colliding with it is what
failed the first secure-repo publish attempt), and real PyPI is the
preferred rehearsal destination since only it exercises the validate
job.
Co-authored-by: Isaac
The sidebar has an "auto-expand the active session's project" effect so
navigating to a filed session reveals it. It fired for pinned sessions too,
even though a pinned session is already reachable from the Pinned section.
A user who manually collapsed the project then clicked its pinned row saw
the folder pop open again, undoing the collapse (issue #2506).
Guard the effect: if the active session is in `pinnedSet`, skip the
auto-expand. The pinned row still navigates; the folder stays collapsed.
Adds a colocated Vitest regression covering both directions (pinned target
keeps the folder collapsed; non-pinned filed target still opens it), and a
Playwright e2e that drives the reporter's flow end-to-end.
Closes#2506
Signed-off-by: wahajmasood <wahajmasood9@gmail.com>
* feat(ci): deterministic release pipeline (release, finalize, homebrew)
Releases were an LLM/human walking RELEASING.md: ~15 CLI commands across
two accounts, a hand-edited uv.lock, and easy-to-miss steps (the Homebrew
tap froze at 0.2.0 while PyPI reached 0.5.1). This makes each phase two
idempotent workflow dispatches plus explicit judgment gates:
- release.yml: plan -> cut branch-X.Y -> lockstep bump (update_versions.py
+ CI uv lock) -> tag -> App-token push (GITHUB_TOKEN-pushed tags fire no
downstream workflows); dry_run defaults true; maintainer-only authorize
job; rc1 auto-dispatches the main .dev0 bump.
- finalize-release.yml: deterministic gates (PyPI serves all three
packages, CHANGELOG PR merged, no open PRs on the X.Y-docs staging
branch) -> publish-release environment approval -> publish draft as
Latest via the App token so release:published actually fires.
- update-homebrew.yml: on final release publish, rewrite the tap formula's
sdist pin, regenerate resources via brew update-python-resources, and
open the tap bump PR (test-bot + pr-pull take it from there).
- bump-version.yml pushes/opens PRs with the App token so CI runs on bump
PRs; ci/lint run on branch-[0-9]* pushes so the green-CI gate has data
on release branches; lint gains a version-lockstep check.
- RELEASING.md rewritten around the dispatches (manual flow kept as a
break-glass appendix); design + peer survey in
designs/RELEASE-AUTOMATION.md.
Co-authored-by: Isaac
* fix(ci): scope the finalize App token to omnigent-site too
The docs-sweep gate queries omnigent-site, but the checks job minted its
installation token scoped to the omnigent repo only — tokens cannot reach
outside their grant, so the gate would 403 on every real finalize run.
Mint one token scoped to both repos (read-only usage in this job).
Also: anchor the tap sibling-resource assert to the normalized sdist
filename instead of a bare version substring, and note in RELEASING.md
that skip_ci_check also covers base commits that ran no checks (e.g.
paths-ignore'd cherry-picks).
Co-authored-by: Isaac
* feat(ci): TestPyPI rehearsal runbook + bump-main downgrade guard
A full-pipeline rehearsal releases a below-latest throwaway rc (e.g.
0.0.1rc1) and publishes it to TestPyPI via the secure repo's existing
destination input; RELEASING.md now documents the sequence, expected
side effects, idempotency checks, and cleanup.
Guard release.yml's bump-main against that scenario (and old-series
backport cuts): dispatching the post-release bump for a version that
sorts below main's current version would open a PR walking main's
version backwards, so compare first and skip with a summary note.
Co-authored-by: Isaac
* fix(ci): correct ref-existence checks and cancelled-run handling in release gate
Two defects caught by running the plan job's logic locally against the
live repo before merge:
- gh api prints the 404 error body to stdout, so capturing it with
'|| true' and testing non-empty treated "Not Found" JSON as an
existing branch/tag — every fresh cut would have failed as a tag
collision. Gate on the exit code instead.
- Cancelled (superseded) check runs are chronically present on main
head commits, so treating cancelled as failing would block every
release and train operators to reflex-pass skip_ci_check. Cancelled
now warns; real failures and pending runs still block.
Co-authored-by: Isaac
* fix(release): post-release bumps main to the next minor, not micro
next_dev_version mirrored MLflow's micro-bump convention (0.6.0 ->
0.6.1.dev0), but this repo's main carries the NEXT MINOR as .dev0
(the 0.5 cycle left main at 0.6.0.dev0), and post-release only runs
when a new branch-X.Y cycle is cut — patches never move main. The
micro bump would re-freeze main on the released line and point
doc-sync at the docs branch the release already owns: after cutting
branch-0.6 at rc1, release.yml's bump-main would have set main to
0.6.1.dev0 instead of the 0.7.0.dev0 that RELEASING.md promises.
Bump the minor. Caught by Polly's AI review on PR #2580.
Co-authored-by: Isaac
- Derive accessible light and dark tokens from one preset-based configuration
- Persist live accent, tint, contrast, and sidebar translucency controls
- Cover the flow with unit, UI, and browser tests
Signed-off-by: sabhya-db <sabhya.chhabria@databricks.com>
Co-authored-by: sabhya-db <sabhya.chhabria@databricks.com>
Pins live in browser localStorage keyed by the conversation id string.
Before the id-to-binary migration those were prefixed (`conv_<hex>`);
the migration + redeploy made the API return bare `<hex>`, so returning
users' stored pins no longer matched the ids the UI receives.
Two consequences, both surfacing as duplicate sidebar rows:
- `pinnedSet.has(c.id)` missed (`conv_<hex>` vs bare) so the session was
not recognized as pinned and fell into the normal list.
- The pinned-backfill treated the prefixed pin as missing from the loaded
set and re-fetched it via `GET /v1/sessions/conv_<hex>`; the server
resolves it (prefix-tolerant `uuid_to_bytes`) and returns it under its
bare id, which was then merged into the list un-deduped — a second copy.
Migrate stored pins to bare hex on read (durably re-persisted by the
existing write-back effect) so pins match again and the backfill stops
firing spuriously. Also dedupe the merged list by id as defense-in-depth
against any list/backfill collision.
Co-authored-by: Isaac
Convert the 19 opaque uuid id columns (agents, conversations + split
tables, items, labels, comments, files, policies, hosts,
session_permissions) from prefixed varchar(64) strings (conv_/ag_/host_/
pol_/file_/item-type prefixes, dashed comment uuids) to 16 raw bytes via
a Uuid16 TypeDecorator: BYTEA (Postgres), BLOB (SQLite/D1), BINARY(16)
(MySQL). Python keeps the bare 32-char hex form everywhere; the type
converts at the column boundary.
Migration z6a2b3c4d5e6 strips prefixes and retypes in one transaction,
rewrites the embedded resource_event session_id copies (scoped to
type=8 so message prose is never touched), strips the FTS mirror, and
fail-louds on MySQL UNHEX NULLs. Downgrade restores bare-hex varchar.
Backwards compat: uuid_to_bytes strips known legacy prefixes at every
bind (old URLs/clients keep resolving); normalize_uuid guards
Python-side scope compares; _normalize_host_id covers host config.yaml;
native-harness state dirs fall back to the legacy digest; malformed ids
map to 404 (HTTP) or a clean close (host tunnel WS).
Excluded (still strings): response_id (polymorphic harness token),
runner_id, external_session_id, bundle_location (physical artifact
key), account token/hash columns, email identity columns.
Co-authored-by: Isaac
* fix(harnesses): close cold-spawn vs release/shutdown race in process manager
Linearize get_client, release, and shutdown on the per-conversation spawn
lock so a mid-spawn release cannot return early and lose to a late
registration, and discard in-flight spawns once shutdown begins.
* fix(harnesses): invalidate queued get_client waiters on release
Bump a per-conversation release generation under the spawn lock so
get_client calls that queued behind release fail instead of respawning
after teardown, while post-release calls can still spawn. Harden the
barrier tests and cover the queued-waiter race.
* test(harnesses): silence CodeQL ineffectual-await alerts in race tests
Bind await results and use asyncio.wait + task.exception() so the
barrier tests no longer trip github-code-quality's dead-statement rule.
Interpret parser-stringified boolean values explicitly when building the openai-agents spawn environment. Add regression coverage for string and native boolean forms.
Fixes#2501
* feat(web): prefill the new-session composer from the project's newest session
The sidebar's per-project "new session" pencil preselects only the project
chip; host, working directory, and agent still come from global last-used
defaults, so starting a chat in a project means re-picking everything when
juggling more than one repo.
A ?project= visit now seeds the composer from the project's newest session:
its host and agent, its repo resolved back to the main work tree (via the
host worktree listing) when that session ran in a linked worktree, and a
fresh auto-generated branch so a plain Enter starts the session in a new
isolated worktree. Values only fill empty slots — a restored draft or a
user's own pick always wins — and switching to another project's pencil
clears exactly what the prefill itself seeded before reseeding. Projects
with no usable newest session (empty, sandbox-origin, offline lookup,
missing host) fall back to the existing generic defaults.
Frontend-only: reuses GET /v1/sessions?project= and the host worktree
listing; no server changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e-ui): cover the project pencil's composer prefill
Drives the real chain the unit tests mock: sidebar project folder →
hover-revealed pencil → composer seeded with the newest session's host,
agent, and source repo (resolved from its linked worktree via the host
worktree listing) plus a generated worktree branch — beating the
recent-workspace default — through to the create POST body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): keep the composer prefill anchored on live data
Review follow-ups on the project prefill:
- Invalidate the project-newest-session cache from every mutation that
changes a project's session membership (archive, bulk archive, delete,
bulk delete, move to project, delete project) — previously only a
natural refetch cleared it, so the pencil could prefill from a session
that had just been archived, moved, or deleted.
- Require the newest session's host to be online before seeding it (or
its workspace): the picker disables offline hosts, so seeding one set
up a create that could only fail; the prefill now falls back to the
generic defaults instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(web): drive the project prefill with a pure state machine
Review feedback on the prefill: the ref/effect provenance tracking
(applied/auto refs, per-project seeded guards, settle round-trips) was
hard to follow. Replace it with a pure transition function in
projectPrefill.ts — a location track (host → workspace → branch →
settled) plus an independent agent seed — advanced one step per render
by a single driver effect that fills empty slots only.
Switching to another project's pencil now behaves exactly like a fresh
visit: every seedable slot resets and the machine reseeds, instead of
surgically reverting only the values the prefill wrote.
Co-authored-by: Isaac
* fix: guard the workspace seed against a mid-flight host switch + invalidate newest-session on create
- the prefill's workspace phase now settles without writing when the live
host pick (or the sandbox) no longer matches the newest session's host,
so another host's repo path can't land in the working-directory field
- invalidate the project-newest-session cache after the post-create
project filing, so a pencil click within staleTime prefills from the
session just created instead of the previous one
- add pure state-machine tests for the mid-flight transitions the rendered
harness can't sequence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): make the branch seed fill-empty-only via a functional setter
A branch typed between the qualifying render and the prefill effect's
execution was clobbered — the only seed written from closure state
instead of a functional empty-only update like the other slots.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): fall back fully when the newest session is unusable
- host and workspace now seed together in the workspace phase, so a
failed source-repo resolution can't leave the project host seeded
over a generic workspace (half a template)
- an offline/gone host makes the whole session unusable: the agent seed
falls back to the last-used agent instead of the session's, matching
the stated all-or-nothing fallback
- pin both behaviors with state-machine tests and distinct-agent
component tests (the old cases reused the generic agent, masking this)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: merge main and regenerate web/package-lock.json
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(web): regenerate lockfile with --package-lock-only --legacy-peer-deps
The merge's full `npm install` added extra resolved entries that the
repo's canonical lockfile method (npm >= 11.10, --package-lock-only
--legacy-peer-deps) excludes, failing the "lockfile up to date" gate.
Regenerate the CI-canonical way. `npm ci --legacy-peer-deps` installs
clean; type-check and full vitest (4073 passed, Node 20) stay green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com>
* feat(web): add opt-in setting to hide unconfigured harnesses in the picker
The new-chat picker lists every harness and badges the ones that aren't set
up on the selected host ("needs setup" / "binary missing" / "needs auth").
For users who only run a couple of harnesses, that's noise.
Add a per-device "Hide unconfigured harnesses" toggle (Settings > Appearance,
off by default). When on, the picker drops harness rows that report as
unconfigured on the selected host, and the bundle-agent (Polly/Debby)
brain-harness override submenu drops unconfigured brain options too — keeping
the current selection so the radio group stays coherent. Fails open: with no
connected host or readiness map, and for harnesses the readiness logic doesn't
recognize, nothing is hidden.
The filter is data-driven off the host's configured_harnesses map, so newly
added harnesses are handled with no code change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(e2e-ui): cover the "hide unconfigured harnesses" picker filter
Adds a Playwright e2e_ui test driving the flow end to end: stub a host whose
configured_harnesses marks one native harness unconfigured, flip the real
Settings > Appearance toggle, and assert the picker drops the unconfigured
harness row while keeping the configured one. Mirrors the stubbing / fresh-loop
conventions of chat/test_codex_auth_availability.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Apply the active Omnigent card color to Monaco editor and diff surfaces\n- Cover explicit app themes overriding the operating-system scheme
Co-authored-by: Anthony Ivan <anthony.ivan@example.com>
The chat composer IME fix (#132/#243, see #433) didn't cover two other
inline inputs, which still submitted on the Enter used to confirm a
Japanese IME conversion:
- session rename field (Sidebar.tsx) — unguarded in main and v0.5.1
- new-project name input (NewChatDialog.tsx)
Route both keydown handlers through the existing isImeCompositionKeyEvent
helper, matching the chat composer. Adds regression tests (compositionStart
/End and keyCode 229 fallback) to Sidebar.rowActions.test.tsx.
Co-authored-by: Isaac
Co-authored-by: Shin Nakane <shin.nakane@databricks.com>
omnigent host status was slow because it fetched all sessions and made
one HTTP request per runner to check online status. Sessions are now
omitted by default; pass --sessions to include them.
* perf(web): drop 15s poll from child-sessions tree views
SSE invalidation in chatStore already keeps the tree fresh on
session.status events. The 15-second poll is redundant and creates
O(tree-depth) requests per interval.
* test(web): update SubagentsPanel tests for SSE-only child-sessions fetch
* revert: restore 15s poll in SubagentsPanel and SubagentsGraphView
SSE only covers direct children of the bound (active) conversation.
Deeper levels and the root when viewing a descendant have no live
channel, so the poll remains necessary as a staleness floor for those
nodes.
* perf(web): replace child-session poll with watch-set push
Add parent_session_id to SessionListItem so the WS /v1/sessions/updates
stream can identify which child_sessions cache to invalidate when a
child's status changes.
SessionUpdatesProvider now:
- Includes all cached child session IDs in the watch-set so the server
streams their status changes
- Invalidates childSessionsQueryKey(parentId) on changed frames for
child sessions
- Re-pushes the watch-set when child_sessions caches update (newly
rendered tree nodes join the stream)
SubagentsPanel and SubagentsGraphView drop the 15 s poll; the tree is
now kept fresh entirely by the watch-set push stream, covering all
depths including grandchildren and the root when viewing a descendant.
* fix(server): regenerate openapi.json with parent_session_id in SessionListItem
* perf(web): enrich session-discovered agents in background after initial render (#2616)
* perf(web): skip per-session agent enrichment on initial picker load
useAvailableAgents fired N GET /v1/sessions/{id}/agent calls to fetch
description, harness, and skills for each session-discovered agent before
the picker could render. These are all cosmetic and not needed to display
the picker:
- description: subtitle shown on hover — can load lazily via useSessionAgent
- harness: used to derive display_name, but session-discovered agents are
always custom uploads (never native coding agents), so capitalizeAgentName
gives a correct display_name without harness
- skills: feeds the composer's slash menu, only relevant after session start
Replace enrichSessionAgent (async, 1 fetch per agent) with sessionAgentFromScan
(sync, no fetch) that builds the AvailableAgent directly from scan data.
The resolved array is now built synchronously after the initial 2-request
parallel fetch (GET /v1/agents + GET /v1/sessions?kind=any).
* perf(web): enrich session-discovered agents in background after initial render
Previously the picker blocked on N GET /v1/sessions/{id}/agent calls before
rendering. The prior fix (sessionAgentFromScan) eliminated those calls but
dropped harness — which gates the model/effort picker, routing support, and
unconfigured-host warnings for custom agents.
New approach: render the picker immediately with name-only scan data, then
fire enrichment calls in the background via enrichInBackground(). When they
complete, setQueryData patches harness/description/skills into the
['available-agents'] cache, triggering a re-render with full data.
The picker is visible instantly; harness-dependent UI fills in asynchronously
once the per-session fetches land (typically <100ms on a local server).
* style: fix prettier formatting in useAvailableAgents.ts
* perf(web): fetch session agent details on hover instead of background eagerly
Replace the background enrichment approach with on-hover prefetching:
- Add sessionId to AvailableAgent (only set on session-discovered agents)
- Export prefetchAvailableAgentDetails(agent, queryClient): fetches
GET /v1/sessions/{id}/agent on first hover and patches harness,
description, and skills into the ['available-agents'] cache
- Add onMouseEnter to all three renderEntry variants in AgentHarnessPicker
to call prefetchAvailableAgentDetails
Zero fetches on load. Agents the user never hovers cost nothing.
Harness-dependent UI (model picker, routing, host warnings) appears once
the user hovers, giving ~100ms head start before they click.
* fix(web): prefetch session agent details on picker open to avoid lazy knobs chevron
Fetching harness on individual hover caused hasKnobs() to flip mid-render,
making the '>' chevron appear lazily on entries that gained knobs after enrichment.
Instead, fire prefetchAvailableAgentDetails for all session-discovered agents
in onOpenChange when the picker opens. By the time the user reads the list
the enrichment is done and hasKnobs is stable. Remove the per-item
onMouseEnter handlers.
* test(web): add prefetchAvailableAgentDetails to useAvailableAgents mock
* fix(web): fix test failures in re-landed lazy agent enrichment
Three issues from the original CI failure:
1. fetchBuiltinAgents was spreading builtin/created_at as explicit
undefined when absent from the wire, causing toEqual to fail on
tests that omitted those fields. Changed to conditional spread so
absent fields are not present on the object at all.
2. Tests expected eager enrichment (description, harness from
GET /v1/sessions/{id}/agent on load) but the PR defers this to
hover. Updated affected tests to expect scan-only fields with
sessionId, and no enrich fetch calls on initial render.
3. Four test files mocked useAvailableAgents without including
prefetchAvailableAgentDetails, causing runtime errors when
NewChatDialog called it on picker open. Added the export to all
four mocks.
Also adds post-enrichment native-shadow filtering to
prefetchAvailableAgentDetails: if enrichment reveals a session agent
has a native harness (e.g. kiro-naitive typo resolving to kiro-native),
it is removed from the cache when a seeded built-in with the same
native key already exists.
* test(web): add prefetchAvailableAgentDetails unit tests
PR #2097 made build_researcher_spec probe the real host for the
platform-default sandbox binary when the parent has no os_env. The
workflow subagent resolution tests reach that probe (directly and via
_find_spec_by_name), so on a Linux host without bubblewrap three of
them fail with OmnigentError. Add the same autouse shutil.which stub
that #2097 added to tests/tools/builtins/test_web_fetch.py; the probe
itself keeps its dedicated coverage there.
Signed-off-by: Enes Yilmaz <enesyilmaz5157@gmail.com>
The response_end handler ran finalizeActive using the CURRENT activeResponse's
id, without checking that the completing response matched it. A native-terminal
harness can open an empty runner "wrapper" response that completes AFTER a newer
turn's id has already taken over activeResponse (e.g. hermes-native during a
cold start, where the wrapper completes empty during the ~16s the harness is
starting, then the forwarder's per-turn id streams the real work). That stale
terminal then finalized the LIVE turn to "completed" — its tool cards stopped
streaming (no spinner), the session flipped to idle, and the in-flight preview
was pruned.
Guard the response_end side effects on the ended response id matching the
active one: a terminal for a different (superseded) response is ignored. On a
matching or absent active response this is the normal terminal path, so
SDK-streamed harnesses are unchanged.
Adds a deterministic test that feeds the exact interleaving (wrapper opens →
newer turn id takes over → stale wrapper completes) and asserts the live turn
stays streaming.
Co-authored-by: Isaac
Signed-off-by: manffred-calvosanchez_data <manffred.calvosanchez@databricks.com>
The file viewer's "Find in file" opened Monaco's native find widget but
immediately reset the searchOpen flag, so the toolbar toggle never reflected the
widget's real state: re-clicking Find re-opened instead of closing, and a close
from inside Monaco (Escape / the widget's ✕) left the toggle stuck.
Mirror the find widget to searchOpen instead — true opens find, false closes it
via the find controller — and subscribe to the controller's state changes to
reset the toggle when find is closed from within Monaco, keeping the button in
sync. Also suppress Monaco's detached "(Escape)" hint tooltips, which overlap the
small floating find widget and read as flaky.
Co-authored-by: Isaac
* feat(benchmarks): measure real UI cold start via a host daemon
The `session_cold_start` journey pre-spawned a runner, waited for its tunnel,
then bound a session and polled `GET /session` to idle. That skips the window
a real new chat actually pays — where `POST /events` races a still-connecting
runner — and doesn't match the UI's create→attach-SSE→send→await-first-token
sequence, so it can't reflect changes to the connect-grace path.
Replace it with a faithful reproduction:
- BenchEnvironment gains `with_host` (additive over `with_runner`): the boot
runner still serves the warm journeys, and a real `omnigent host` daemon is
spawned so a host-bound session-create fires `host.launch_runner` and the
host launches its own runner on demand. The daemon self-identifies via
OMNIGENT_HOST_ID/OMNIGENT_HOST_NAME so it writes no config and never touches
~/.omnigent; it registers over loopback (single-user owner, no token).
- `create_hosted_session` sends the inline-launch POST (host_id + workspace)
and returns without waiting for the runner — the race is the point.
- `cold_start_first_delta` runs the UI sequence: create → attach the SSE
stream → wait for its ready heartbeat → POST the first message → return on
the first `response.output_text.delta`. The SSE subscribe/gate/await core is
factored out of `time_to_first_delta` and shared by both.
- run.py boots `with_host` when any selected journey needs it (`needs_host`).
The measured span is now host launch + runner boot + reverse-tunnel connect +
first-token pipeline — the true new-conversation cost. Note: the report key is
unchanged but the measurement is not, so the trend line has a step change at
this commit, and historical `session_cold_start` values aren't comparable.
Removes the now-dead spawn_extra_runner / _wait_runner_online / terminate_runner
helpers. Verified: cold ~2.2s vs warm TTFT ~50ms (the delta is the launch race);
all 12 benchmark smoke tests pass; ruff + format clean.
Co-authored-by: Isaac
* fix(benchmarks): address cold-start review — use omni CLI, fix docs, broaden first-response
Review feedback on the hosted cold-start journey:
- Spawn the server and host via the real `omni server` / `omni host` console
scripts instead of `python -m omnigent.cli ...` and an inline
`run_host_process` snippet, so the benchmark drives the same user-facing
commands a developer runs. A new `_omni_executable()` derives the `omni`
script beside the compat-aware interpreter, preserving cross-version compat.
`omni host` gets `--non-interactive` so it never attempts a browser login.
- Give the `_wait_host_online` poll's `except httpx.HTTPError` an explanatory
comment (keep polling through transient/not-yet-up errors) — was a bare pass.
- Correct the cold-start docstring: the server does NOT reap an external-host
runner on idle, so each iteration's runner lingers until the daemon is
SIGTERM'd at teardown (bounded by _RUNNER_MAX_ITERATIONS + warmups). Explain
why per-iteration teardown is deliberately skipped (a stop round-trip would
distort a journey whose point is to time the fresh-launch cost).
Also broadens the first-token signal from `response.output_text.delta` only to
that OR `response.output_item.done`, so the measure returns on the first model
response of any shape (e.g. a leading tool call) rather than treating a
non-text-first turn as a failure.
Co-authored-by: Isaac
The session event stream is snapshot-plus-live-tail with no buffer or
replay: the band's first assertion is served from the snapshot on page
load, which does not prove the browser's live SSE subscription is up
yet. A startup map published in the window before that subscription
exists is dropped, leaving the band stuck on the prior state — the
observed flake (band never advances past "0/3").
Re-publish the idempotent full-state map until the band reflects it via
a new _publish_until helper. A real live-handler regression still never
satisfies the assertion, so this closes the connect race without
weakening the check.
Co-authored-by: Isaac
* feat(web): filter archived sessions by project
The Archived settings view had no filter controls even though
`GET /v1/sessions` already ANDs `include_archived` with `project`.
Add an accessible project picker to ArchivedSection and thread an
optional `project` through useConversations -> fetchConversationsPage
so the archived list scopes server-side via `?project=` (empty string
is never forwarded, since the server reads that as "unfiled only").
Dropdown options are derived from the `omni_project` labels present on
the loaded archived sessions, NOT from useProjects(): the
`/v1/sessions/projects` endpoint (list_projects) excludes projects
whose every session is archived — exactly this page's population — so
those archived-only projects would otherwise be missing from the
filter. Deriving from the loaded set keeps this change UI-only.
The `project` element is appended to the react-query key only when a
filter is active, so the sidebar / rename / push-delta cache paths
keep their existing three-element key byte-for-byte; the shared parser
filtersFromConversationQueryKey now accepts the four-element variant so
those in-place cache merges never throw on it.
Tests: project reaches the request URL (and is url-encoded / omitted
for "all projects"); the four-element query key parses; UI-derived
options surface archived-only projects; project-scoped and empty
states render.
Co-authored-by: Isaac
* fix(web): make project a cache-membership dimension for archived filter
The archived project filter added `project` to the query key and
`ConversationListFilters`, but the push-delta reconciliation still
decided membership on `archived` alone. Two correctness gaps:
- A session relabeled OUT of the selected project (via a remote
`WS /v1/sessions/updates` delta) stayed visible in that project's
filtered cache. `violatesKnownMembership` now evicts a row whose
`omni_project` label no longer matches `filters.project` (and, for
the `""` "unfiled" variant, any row that gained a label).
- A session relabeled INTO the selected project never reconciled: the
filtered variant can't place a row it doesn't hold, and the
unfiltered variant (where the row lives) ignored label changes, so
no refetch fired. `changedFieldsNeedRefetch` now treats a `labels`
change as needing reconciliation; the caller's prefix-wide
`["conversations"]` invalidation then refetches the filtered
variants. This also fixes project folders (["project-sessions", …]),
which the code already assumed reconciled on label moves but didn't.
`PROJECT_LABEL_KEY` moves to this leaf cache module so the membership
check can read it without a value import cycle back to the hooks layer.
Tests: 4-element project key evicts a row moved out of the project and
flags refetch; a move into a project flags refetch on the unfiltered
variant; a matching row survives a non-label change; the unfiled
variant drops a row that gains a label.
Co-authored-by: Isaac
* fix(web): complete archived-project picker options + collision-safe values
Two fixes to the Archived view's project filter (SettingsPage):
FIX 2 — archived-only projects on later pages were undiscoverable.
The picker derived its options from the visible list's loaded first
page (~20 rows), so a project whose only archived sessions sit on page
2+ never appeared — exactly the population this feature filters.
Options now come from `useArchivedProjectNames()`, a dedicated hook
that pages through ALL archived sessions server-side (limit=100) and
collects the distinct `omni_project` labels. It's keyed under the
`["projects", …]` prefix so the existing archive / unarchive / move /
delete invalidations refresh it for free. The archived list itself
also gains a "Load more" control so it's no longer silently capped at
the first page. (Chosen the UI-only approach the review preferred; no
backend/Python touched.)
FIX 3 — the `"__all__"` clear-filter sentinel collided with a real
project of that name (selecting it would clear the filter instead of
scoping to it). Select values are now discriminated: a fixed `"all"`
token for the reset option, and `project:<encoded-name>` for each
project, decoded on change — so no real name can alias the sentinel.
Also dedups `PROJECT_LABEL_KEY` to a re-export from the cache module
(the definition moved there in the prior commit).
Tests: options include an archived-only project absent from the loaded
page; `fetchAllArchivedProjectNames` pages the cursor and returns
distinct sorted names; a project literally named `__all__` filters
correctly and is sent as `project=__all__`; Load more calls
fetchNextPage.
Co-authored-by: Isaac
* fix(web): keep archived "Load more" available when a page has no archived rows
The archived view fetches a mixed page (include_archived=true returns
active AND archived rows) and filters to archived client-side. The
"Load more" pager was rendered only inside the `archived.length > 0`
branch, so a first page containing only active rows (archived sessions
are older and can sort onto later pages) hit the definitive
"No archived sessions" empty state with no way to page forward — the
page-1 cap bug the pagination was meant to close.
The definitive empty state now shows only when `archived.length === 0
&& !hasNextPage`. When there are no archived rows on the current page
but more pages exist, a "No archived sessions on this page" hint plus
the pager are shown instead, and the pager stays visible whenever
`hasNextPage` regardless of the filtered count. Manual paging only —
no auto-fetch loop.
Test: page 1 of only active rows with hasNextPage → no definitive empty
state, Load more rendered; clicking it surfaces an archived row from
page 2. The test mock is now stateful to emulate infinite-query paging.
Co-authored-by: Isaac
* fix(web): make an empty-string project mean "all projects" consistently
The conversations-query contract was internally inconsistent for
`project === ""`: `fetchConversationsPage` omitted the `project=` param
for falsy values (fetching ALL projects), while the query key produced
a four-element `["conversations","",true,""]` entry and
`violatesKnownMembership` treated `""` as the "unfiled" slice (evicting
labeled rows). So the key/membership said "unfiled" while the request
said "all projects".
The Archived view (the only caller that passes `project`) only ever
passes a concrete name or `undefined`, never `""` — the "unfiled" slice
is never requested for this list. So drop the `""` variant: a falsy
project is now "all projects" everywhere. useConversations coalesces a
falsy project into the base three-element key (no distinct "" entry),
the request keeps omitting `project=`, and `violatesKnownMembership`
applies a project constraint only for a truthy name. Key, request, and
cache-membership now agree.
Tests: an empty-string project shares the base key and omits `project=`
(useConversations); the "" variant applies no membership constraint so a
row gaining a label is not evicted (sessionListCache).
Co-authored-by: Isaac
* refactor(web): drop redundant URI round-trip in archived project select values
* perf(web): stop unrelated mutations from re-running the archived-projects scan
The archived-view picker's option set pages through the entire session
list; keying it under the ["projects"] prefix meant every
invalidateQueries(["projects"]) — including ones that can't change
archived membership — re-ran the full scan while Settings → Archived
was open. Move it to a dedicated key, invalidate it explicitly from the
mutations that actually change archived membership or project labels
(archive, bulk archive, delete, bulk delete, move, delete project), and
raise its staleTime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e-ui): cover the Archived view's project filter and pager
Two Playwright tests drive the real chain against the live server: the
picker options come from the archived-only project scan, selecting a
project narrows the list server-side and "All projects" resets it, and
"Load more" pages a project-filtered list past the page size. Seeded
titles and project names carry uuid suffixes so the assertions hold on
the suite's shared server.
Co-authored-by: Isaac
* fix: resolve merge fallout with main and a ruff SIM105
- drop the duplicate ReactNode / Select imports the merge introduced in
SettingsPage.tsx and its test
- unify the two vi.mock("@/components/ui/select") stubs into one that
lifts data-testid off SelectTrigger, serving both the color-theme
dropdown and the archived project filter tests
- use contextlib.suppress for best-effort session cleanup in the
archived-project-filter e2e (ruff SIM105)
- regenerate web/package-lock.json against the merged package.json
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): converge the archived-project picker on remote changes
- the session-updates socket's debounced reconciliation now also
invalidates the archived-project-names scan, so another client
archiving, relabeling, or deleting sessions updates the picker without
waiting for a local mutation or remount
- once the scan settles without the picked project (last archived row
deleted or restored), the filter falls back to All projects instead of
pinning a defunct project over an empty list
- fix the key-shape comment on useArchivedProjectNames (standalone key,
not under the projects prefix)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Bryan Li <15131870+btli@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The project-folder header showed a folder icon plus a trailing chevron on
every viewport. On desktop the chevron now appears only on hover/focus and
takes the folder icon's place in the icon slot, so the resting state is just
folder + name. Mobile (no hover) keeps the folder icon and the always-visible
trailing chevron. Iconless section headers (the "Projects" group) keep their
hover-revealed trailing chevron.
Co-authored-by: Isaac
* perf(web): skip per-session agent enrichment on initial picker load
useAvailableAgents fired N GET /v1/sessions/{id}/agent calls to fetch
description, harness, and skills for each session-discovered agent before
the picker could render. These are all cosmetic and not needed to display
the picker:
- description: subtitle shown on hover — can load lazily via useSessionAgent
- harness: used to derive display_name, but session-discovered agents are
always custom uploads (never native coding agents), so capitalizeAgentName
gives a correct display_name without harness
- skills: feeds the composer's slash menu, only relevant after session start
Replace enrichSessionAgent (async, 1 fetch per agent) with sessionAgentFromScan
(sync, no fetch) that builds the AvailableAgent directly from scan data.
The resolved array is now built synchronously after the initial 2-request
parallel fetch (GET /v1/agents + GET /v1/sessions?kind=any).
* perf(web): enrich session-discovered agents in background after initial render
Previously the picker blocked on N GET /v1/sessions/{id}/agent calls before
rendering. The prior fix (sessionAgentFromScan) eliminated those calls but
dropped harness — which gates the model/effort picker, routing support, and
unconfigured-host warnings for custom agents.
New approach: render the picker immediately with name-only scan data, then
fire enrichment calls in the background via enrichInBackground(). When they
complete, setQueryData patches harness/description/skills into the
['available-agents'] cache, triggering a re-render with full data.
The picker is visible instantly; harness-dependent UI fills in asynchronously
once the per-session fetches land (typically <100ms on a local server).
* style: fix prettier formatting in useAvailableAgents.ts
* perf(web): fetch session agent details on hover instead of background eagerly
Replace the background enrichment approach with on-hover prefetching:
- Add sessionId to AvailableAgent (only set on session-discovered agents)
- Export prefetchAvailableAgentDetails(agent, queryClient): fetches
GET /v1/sessions/{id}/agent on first hover and patches harness,
description, and skills into the ['available-agents'] cache
- Add onMouseEnter to all three renderEntry variants in AgentHarnessPicker
to call prefetchAvailableAgentDetails
Zero fetches on load. Agents the user never hovers cost nothing.
Harness-dependent UI (model picker, routing, host warnings) appears once
the user hovers, giving ~100ms head start before they click.
* fix(web): prefetch session agent details on picker open to avoid lazy knobs chevron
Fetching harness on individual hover caused hasKnobs() to flip mid-render,
making the '>' chevron appear lazily on entries that gained knobs after enrichment.
Instead, fire prefetchAvailableAgentDetails for all session-discovered agents
in onOpenChange when the picker opens. By the time the user reads the list
the enrichment is done and hasKnobs is stable. Remove the per-item
onMouseEnter handlers.
* test(web): add prefetchAvailableAgentDetails to useAvailableAgents mock
On iOS the Chat/Terminal toggle is a native Liquid Glass bar floating over
the web view, so DOM stacking can't hide it — its visibility rides on
isSurfaceFrontmost. Radix drops pointer-events:none on <body> while a menu
is open, so the centre probe falls through to the document root; that is
normally a transient layer we keep the surface "frontmost" through. But the
session kebab menu lives inside the mobile sidebar overlay, so opening it
re-floated the bar over the sidebar.
Probe the open sidebar directly before honoring the transient-menu
exception, treating the surface as obscured when the sidebar covers the
probe point.
Co-authored-by: Isaac
* [examples] Add aws-analyst agent (Redshift + S3 Tables via AWS Labs MCP)
An example agent that answers questions over governed AWS data through the
official AWS Labs MCP servers (awslabs.redshift-mcp-server,
awslabs.s3-tables-mcp-server) wired as type: mcp connectors, read-only by
default. Shows how any AWS Labs MCP server plugs into Omnigent with no custom
connector code.
Co-authored-by: Isaac
* [examples] Add test_example_aws_analyst.py; rename example to aws_analyst
Adds the dedicated structural test hzub requested. The
test_examples_coverage_sync.py drift guard requires every example under
examples/<name>/ to have a matching tests/e2e/omnigent/test_example_<name>.py,
where <name> equals the directory name exactly.
To match the requested underscore filename (test_example_aws_analyst.py) and
the shipped-examples underscore convention (hello_world, agent_with_tools) —
and because pytest's default import mode can't import a hyphenated module —
the example dir is renamed aws-analyst -> aws_analyst (name:, comments, README
run command updated to match).
The test is pure spec-load (expand_env=False, no LLM/credentials/AWS account),
modeled on test_example_remy.py. It asserts the recipe's invariants: single
agent (no sub-agents), claude-sdk with no pinned model/profile, both awslabs
MCP servers wired as uvx stdio connectors, the Redshift tool allow-list, and
the read-only guarantee (no --allow-write, no mutating verbs in the allow-list).
Verified locally: the 5 new cases + test_every_agent_has_a_dedicated_test_file
pass (6 passed).
Co-authored-by: Isaac
* fix(web): bound stream-reconnect 404 retries instead of treating them as permanent
A reverse proxy serves 404 for the stream route for the ~10-60s a backend
container takes to restart, so startStreamPump's "401/403/404 won't fix
themselves" short-circuit was flipping the session to failed mid-restart
instead of riding it out like it already does for 5xx and transport drops.
Retry 404s with backoff up to a cap before giving up, so a transient restart
self-heals while a truly deleted/invalid conversation still terminates.
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
* test(web): add e2e_ui coverage for transient stream-404 recovery
Satisfies the E2E UI Required gate for the stream-reconnect 404 fix.
Simulates a reverse-proxy 404 window on stream-open (404 x3, then
success) and asserts the turn still completes instead of the session
flipping to "failed" . verified to fail against the pre-fix chatStore.ts.
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
* fix(test): stabilize the e2e_ui stream-404 regression test
The test added to satisfy the E2E UI Required gate on the stream-reconnect
404 fix was racing itself: waiting on time.sleep() starves Playwright's
event dispatch (same thread), so the retry loop's progress was invisible
and the assistant reply could arrive before the stream had even
reconnected. Wait via page.wait_for_timeout() instead, and only send the
message once the 404 retries have resolved, so the e2e_ui coverage this
PR needs actually runs reliably in CI.
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
---------
Signed-off-by: Nick Chakre <nickchakre18@gmail.com>
resolve_model_provider had two false-negative paths that made
sys_list_models (and orchestrator preflights built on it) report
perfectly healthy workers as un-bootable:
- a 'cli-config' provider entry fell through to the inline-family loop,
which finds no families (cli-config entries carry none — the
credential is an auth command / env key in the codex CLI's own
config.toml, resolved by codex at launch), so the worker was reported
as 'configures no family with resolvable credentials'.
- the cursor harnesses were absent from _PROVIDER_RESOLUTION_HARNESS,
so they hit the 'harness has no model-provider resolution' dead-worker
note even though cursor-agent always brings its own stored login.
Both now resolve to static, unverified listings (mirroring the
subscription readout): cli-config lists the codex curated ids with a
note that the CLI resolves the credential itself; cursor resolves to a
cursor-agent CLI login serving the curated base-model catalog.
Co-authored-by: Isaac
Co-authored-by: Sam Armstrong <sam.armstrong@databricks.com>
Web UI now sends an explicit X-Omnigent-Client header (web/desktop/ios/android)
on session creation and fork requests; the server prefers it over User-Agent
heuristics when recording the surface in telemetry.
* perf(web): reduce sessions API calls on initial page load
On the landing page, ChatPage fired two redundant GET /sessions calls:
- useConversations() with includeArchived=false, duplicating the sidebar's
useConversations('', true) which uses the same endpoint with a different
cache key
- useAgents() unconditionally, even though the agent picker is only visible
once a session is open
Fix both:
1. ChatPage's useConversations() now passes includeArchived=true, sharing
the cache key with the sidebar and eliminating the duplicate fetch.
2. useAgents gains an option; ChatPage passes enabled=!!urlConvId
so the sessions?limit=100 scan is skipped on the landing screen where
NewChatLandingScreen's useAvailableAgents already covers agent discovery.
Net effect: 5 → 3 GET /sessions calls on initial load.
* fix(web): consolidate useConversations callers to share sidebar cache key
AppShell, usePermissions, RunnerHealthProvider, and useIdleNotifications
all called useConversations() with the default includeArchived=false,
creating a separate cache entry from the sidebar's includeArchived=true
fetch and causing a duplicate GET /sessions?limit=20 call on every load.
Switch all four to useConversations("", true) so they share the sidebar's
["conversations", "", true] cache key. The behavior change is minimal:
these hooks only inspect existing sessions by id or aggregate counts, so
seeing archived sessions in the list is either neutral or beneficial
(e.g. useCanEdit can now resolve permissions on an archived session).
* fix(web): fix CommandPalette cache-key mismatch after includeArchived consolidation
CommandPalette was calling useConversations(query, false), designed to share
AppShell's old useConversations() cache entry. After switching all callers to
includeArchived=true, CommandPalette's false key no longer matched anything,
reintroducing the duplicate fetch.
Switch to includeArchived=true and filter archived rows client-side in the
sessions memo so the palette still only lists active sessions.
* test(web): update CommandPalette test for includeArchived=true
A claude-native cold resume rebuilds Claude Code's local transcript from
Omnigent's stored items. Image tool results (screenshots) are persisted as
a stringified content-block array, and the rebuild dropped that string
straight into the `tool_result` content. On `claude --resume`, Claude sent
the base64 to the API as plain *text*, so a single screenshot cost ~250K
tokens instead of the ~1.5K an image block costs. A conversation that fit
comfortably while live then overflowed the context limit on reconnect
("Prompt is too long"), and the model no longer saw the screenshots as
images.
Rehydrate `text`/`image` block arrays back into real content blocks so the
resumed request sends images as images. Non-block outputs (plain text,
other JSON shapes, API-unsupported block types) stay raw strings, so their
resume behavior is unchanged.
Measured on the reported conversation: base64-as-text drops from ~253K
tokens to 0, with all 6 screenshots restored as image blocks.
Co-authored-by: Isaac
## Related issue
N/A
## Summary
- Adds `omnigent://<hostname>/c/<session_id>` deep links to the Electron desktop shell: an OS-clicked link opens that session on that server, reusing an existing window in-place when one is already on it.
- Window handling is the careful part — a pure, unit-tested `chooseDeepLinkStrategy` picks reuse-in-place (focus + tell the SPA router to navigate, no reload), reuse-with-reload (pinned but mid-SSO), open-known (frictionless new window), or consent-unknown (native dialog, since pinning a new origin is a privilege grant). The workspace mount probe runs only AFTER consent, so a link to an attacker-chosen server makes no pre-consent network request.
- The window's server identity (`serverUrl`, used by `omnigent host --server`) is kept clean of the `/c/<id>` path while the load URL carries it; the mount-aware join keeps `/ml/omnigents` from being dropped.
## Test Plan
- `cd web/electron && node --test` — 195 tests (19 new deep-link decision tests + wiring guards).
- `cd web && npx tsc -b` clean; `npx vitest run src/hooks/useIdleNotifications.test.tsx src/lib/nativeBridge.test.ts src/shell/AppShell.test.tsx` — 160 pass.
- Manual (dev, local server `127.0.0.1:6767`): warm-start reuse-in-place — with the app connected and viewing conversation A, `npm start -- 'omnigent://127.0.0.1:6767/c/<B>'` (second terminal) switches the existing window to B in-place, no reload. Confirmed via the diagnostic logs: `strategy=reuse-inplace ... send open-path /c/<B>`. Requires the web UI rebuilt (`cd web && npm run build`) since the desktop loads the server's built SPA.
## Demo
N/A — no visible UI change beyond in-app navigation triggered by an external link.
## Type of change
- [ ] Bug fix
- [x] Feature
- [ ] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Unit tests cover the pure decision logic (`web/electron/test/deepLink.test.js`: parse + the reuse/reload/open-known/consent-unknown table) and `web/electron/test/main.test.js` wiring guards (open-url/second-instance/argv ingestion, serialized queue, scheme registration, mount-aware path join, clean serverUrl, and the post-consent probe placement). The OS-dispatch + window orchestration can't be unit-tested without an Electron launch, so it was verified manually with the local server (warm-start reuse-in-place confirmed via logs).
## Changelog
`omnigent://<hostname>/c/<session_id>` links open that session in the desktop app, reusing an open window on that server in-place
* fix(spawn): clarify sys_session_send schema to prevent agent/title-in-args confusion
Pi was putting 'agent', 'title', and 'session_id' inside the args object
instead of as top-level fields. It also tried passing 'model' via session_id
mode where it has no effect.
- Tool description now explicitly states that agent/title/session_id are
TOP-LEVEL fields and model/purpose go INSIDE args, with a concrete
correct example.
- args description now warns against putting agent/title/session_id inside
args, and clarifies that model only applies on session CREATE (first named
send), not on continuation or session_id sends.
* revert(pi-native): remove pi_native_credentials change from sys_session_send fix
* fix(pi-native): route non-Claude models to correct provider in models.json and --provider arg
Two fixes for model override with non-Claude models (GLM, GPT, etc.):
1. to_models_config: don't append the selected model to the Anthropic
(omnigent) provider if it already lives in an additional_providers entry
(omnigent-openai/openai-completions). Previously GLM was appended to
the anthropic-messages provider, causing Pi to attempt to call GLM via
the wrong wire protocol.
2. pi_native_provider_launch: pass --provider omnigent-openai (not omnigent)
when the selected model lives in an additional_providers entry. Previously
--provider omnigent was always passed, so Pi couldn't resolve models that
only exist under omnigent-openai.
* refactor(hindsight): rename memory extra to hindsight; gate tools on SDK
## Related issue
N/A
## Summary
- Rename the optional install extra `memory` -> `hindsight` (the extra that
pulls `hindsight-client` for the Hindsight long-term memory tools), so the
extra name matches the tools it enables. Updates `pyproject.toml`,
`uv.lock`, the install hint, docstrings, and `examples/remy/config.yaml`.
- Hide the three Hindsight tools from the builtin list when
`hindsight-client` is not installed: they're now absent from
`BUILTIN_NAMES` / `INSTANTIABLE_BUILTINS` and not instantiable, and the
onboarding `list_builtin_tools` helper no longer advertises them. The
presence probe uses `importlib.util.find_spec` so the SDK and its deps
(aiohttp, ...) stay lazy.
## Test Plan
- `ruff format` + `ruff check` clean; `pre-commit run` passes on all changed
files (including the `normalize-uv-lock-registry` hook).
- `pytest tests/tools/builtins/test_hindsight.py
tests/tools/builtins/test_registry_unified.py tests/spec/test_validator.py`
-> 79 passed; full `tests/tools tests/spec tests/onboarding` -> green (one
unrelated `databricks_sdk_installed` failure was an env artifact from running
`--extra dev` instead of `--extra all`; passes with `--extra all`).
- New `test_hindsight_tools_absent_from_registry_when_sdk_missing` hides
`hindsight_client` from the finder, reloads the registry, asserts the tools
are absent + not instantiable, and restores the finder in `finally` (no
state leakage — verified by running it before the registry-size test).
## Demo
N/A
## Type of change
- [ ] Bug fix
- [ ] Feature
- [ ] UI / frontend change
- [x] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [x] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [ ] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
The extra rename is exercised by the existing registry-size test (which lists
the hindsight names) and the lock line. The gating is covered by the new unit
test. Manually verified `_hindsight_available()` returns True with the SDK and
False when hidden from the finder, in both the registry and the onboarding
helper.
## Changelog
`omnigent[memory]` is renamed to `omnigent[hindsight]`; the Hindsight memory
tools are now hidden from the builtin list when `hindsight-client` is not
installed.
* fix(uv.lock): complete hindsight extra rename in lock metadata
The rename commit updated the requires-dist marker but missed the
provides-extras list and the package optional-dependencies mirror, so
`uv sync --locked` (every CI job's install step) failed.
The session-row kebab / right-click menu opened "Add to project" / "Move
session" as a side-flyout submenu (C.Sub/SubTrigger/SubContent). On mobile
there's no horizontal room for a side flyout, so it overflowed and didn't
work.
On mobile, the project item is now a plain menu item that swaps the menu
body in place: a local `view` state ('main' | 'projects') replaces the main
actions with the existing ProjectPickerMenu (search + list + Create new
project) plus a chevron-left "Back" row that returns to the main view.
Selecting the item and Back both preventDefault so the menu stays open
rather than closing on select. Desktop keeps the native side-flyout submenu
unchanged. Because the menu body is authored once through the shared
MenuComponents bundle, the in-place view works for both the kebab dropdown
and the right-click context menu families.
Co-authored-by: Isaac
## Related issue
N/A
## Summary
- **Reload watcher: skip gitignored files.** The pod supervisor reloaded the
backend on every `*.py` change under `omnigent/`, including gitignored files
the build regenerates (notably `omnigent/_build_info.py`), causing needless
reloads. It now builds a gitignore matcher from the repo's root `.gitignore`
and `.git/info/exclude` and skips ignored paths — including files inside
ignored directories (`build/`, `dist/`, `*.egg-info/`, …), matching git.
- **`--debug` flag.** Logs every observed file change into the combined pane as
`watch: reload trigger <path>` or `watch: skip <path> (<reason>)`, so it's
clear which change triggered (or didn't trigger) a reload. Quiet by default.
- **Pager log panes.** Per-process log panes are now a `less`-style pager with
line/half/full-page movement, top/bottom jumps, follow-tail, line wrap, and
forward/back incremental search (see the README Keys table).
## Test Plan
- `cargo build`, `cargo clippy --all-targets`, `cargo fmt --check` — clean.
- `cargo test` — passes single-threaded (the parallel-only flake in
`create_skips_seed_when_real_config_absent` is a pre-existing env-var race in
pod.rs, unrelated to this change).
- Verified `classify()` against the real repo `.gitignore`: `omnigent/cli.py`
and `omnigent/inner/foo.py` reload; `_build_info.py`, `build/`, `*.egg-info/`,
and `server/static/web-ui/` are skipped as gitignored; `__pycache__` and
non-`.py` are skipped.
- `omnidev --help` shows the new `--debug` flag.
## Demo
N/A — pager-pane UI recording to be attached on the PR.
## Type of change
- [ ] Bug fix
- [x] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Watcher classification is covered by unit tests in `watcher.rs` (.py filter,
`__pycache__`, gitignored file, file inside a gitignored dir). The gitignore
behavior was additionally verified against the real repo `.gitignore`, and the
`--debug`/pager panes were checked manually — the interactive TUI has no
automated harness.
## Changelog
`omnidev` no longer reloads on gitignored files, adds `--debug` to trace reload
triggers, and its log panes are now searchable `less`-style pagers
Co-authored-by: Isaac
* test(e2e-ui): add a populated-sidebar visual snapshot
Seed a fixed session list covering every sidebar row type (Pinned, Projects group with an expanded folder + nested chat and an empty folder, flat Sessions with needs-response and running badges) so the row-alignment surface is gated. The empty-landing baseline stubs sessions empty, so that surface was previously untested — the area PR #2596 touched.
Determinism: page.route stubs, a fixed page.clock so relative time pills don't drift, and a no-op /v1/sessions/updates socket. Baseline PNG generated by CI in the pinned image (label update-ui-snapshot).
Co-authored-by: Isaac
* test(e2e-ui): regenerate visual baselines
---------
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
Long diff lines previously overflowed with no way to wrap them, which is
painful in a narrow file-viewer pane (2–3 side by side). Add a "Wrap lines"
toggle that soft-wraps long lines in both diff panes (Monaco `diffWordWrap`),
persisted like the other view preferences.
Fold Find in file, Download, and the diff-only toggles (wrap lines, hide
whitespace) into a single "View settings" (⋯) menu, mirroring GitHub's
diff-settings menu and freeing toolbar width. Toggles keep the menu open;
actions close it. Active state shows a check mark, except whitespace whose
eye icon already flips open/closed.
Co-authored-by: Isaac
* OMNI-1193: scheduled-task persistence foundation (SqlScheduledTask/SqlScheduledTaskRun + migration + store + tests)
Co-authored-by: Isaac
* OMNI-1193: drop plugins column from scheduled_tasks (reviewer: Omni resolves plugins host-side, no per-task field)
Co-authored-by: Isaac
* OMNI-1193: drop MySQL-illegal TEXT server_default on scheduled_tasks.metadata
Co-authored-by: Isaac
* OMNI-1193: store opaque scheduled_tasks text columns (prompt/metadata/error) as CompressedText
Co-authored-by: Isaac
* OMNI-1193: document Isaac→Omni id migration contract (mint new st_ id, keep isaac schedule_id in metadata) + fix stale metadata-Text comment
Co-authored-by: Isaac
* Make scheduled_tasks.owner_user_id nullable
Permit NULL so a schedule created with no authenticated user (single-user
/ OSS mode) can leave the owner unset, matching how create_session treats
the session owner as optional. Persistence-only: the fire-path resolution
(null -> reserved "local" user) lands in a later PR.
Co-authored-by: Isaac
* Make scheduled_tasks trigger recurring-only (drop run_at_ms one-shot arm)
The z6a2b3c4d5e6 migration is unreleased, so it is edited in place rather
than adding a follow-up migration.
Co-authored-by: Isaac
* Drop completed state from scheduled_tasks (recurring-only has no terminal state)
The z6a2b3c4d5e6 migration is unreleased, so the state CHECK is edited in
place rather than adding a follow-up migration.
Co-authored-by: Isaac
* Refine scheduled_tasks schema: timezone default + index tweaks
- timezone: add server_default="UTC" (model + migration) so raw inserts always get a valid zone
- drop unused ix_scheduled_tasks_agent_id (no query filters by agent_id)
- reshape ix_scheduled_task_runs_scheduled_task_id to (workspace_id, scheduled_task_id, scheduled_at, id) to cover list_runs()' scheduled_at DESC sort
All in-place on the unreleased migration; no follow-up migration.
Co-authored-by: Isaac
* OMNI-1193: trim redundant scheduled_tasks column comments to match sibling tables; reword sandbox_target comment
Co-authored-by: Isaac
* OMNI-1193: fix ruff C416 lint in scheduled_tasks migration test
Co-authored-by: Isaac
* OMNI-1193: genericize external-scheduler references in scheduled_tasks
Comment/docstring only — no functional code, column names, or values changed.
Co-authored-by: Isaac
* OMNI-1193: align sandbox_target width with hosts.sandbox_provider (String(32))
Co-authored-by: Isaac
* OMNI-1193: add nullable error_code to scheduled_task_runs
Short, queryable failure-classification token (String(64), no CHECK) alongside
the compressed error blob, so future retry logic can distinguish retryable vs
terminal failures. Threaded through the entity, migration, store, and tests.
Co-authored-by: Isaac
* OMNI-1193: drop sandbox_target from scheduled_tasks
sandbox_target was a nullable, persist-only column with no consumer.
Removed because Isaac scheduled-task proto has no compute-target field
(no merge-compat value) and compute-agnosticism is expressed by the
task carrying no compute preference at all — the fire path resolver
decides where to run.
Co-authored-by: Isaac
* OMNI-1193: drop harness_override from scheduled_tasks
harness is not an independent knob in Omni — it is a property of the
agent (agent_id); the composer harness/agent picker selects the
agent_id and there is no independent harness-override control. A
routine wanting a different harness points at a different agent_id, so
harness_override on scheduled_tasks was a dead column with no consumer.
Only removes harness_override from the scheduled_tasks feature.
model_override and reasoning_effort stay (real independent knobs), and
conversations.harness_override is untouched.
Co-authored-by: Isaac
* OMNI-1193: align owner_user_id width to String(128)
owner_user_id is written at fire time as a LEVEL_OWNER grant into
session_permissions.user_id, which is String(128). Every user-identity
column in the schema is String(128); the scheduled_tasks 255 was the
sole outlier and, being wider than the column it feeds, a >128-char
value could store but fail the grant write. 128 stays well under the
MySQL utf8mb4 indexed-key ceiling, so index safety is unchanged.
Co-authored-by: Isaac
* OMNI-1193: align workspace width to String(2048)
scheduled_tasks.workspace and conversations.workspace are the same
concept (an absolute filesystem path where the runner starts).
conversations uses String(2048); ours was the lone Text divergence.
Neither is indexed, so this is a consistency change, not functional —
matching conversations makes the mapping obvious.
Co-authored-by: Isaac
* OMNI-1193: fix stale scheduled_tasks doc comments
Documentation-only. No schema/type/logic changes.
- store module docstring: recurring-only (drop stale "or one-shot")
- create() docstring: state enum is active/paused/deleted (drop stale "completed")
- base_branch param docstring: genericize (drop Isaac-person name)
Co-authored-by: Isaac
* OMNI-1193: adapt scheduled_tasks to post-merge db_models split
Upstream #2341 replaced the single class Base with OmnigentBase +
ConversationBase. Repoint SqlScheduledTask/SqlScheduledTaskRun to
OmnigentBase (control-plane/AP tables, siblings of policies/hosts/
user_daily_cost), NOT ConversationBase (conversation data-plane, may
live on a separate physical DB).
Also re-parent our alembic migration: #2341 added two migrations after
z5, so repoint z6 down_revision z5a2b3c4d5e6 -> bb2c3d4e5f6a (the new
head) to linearize the chain to a single head.
Co-authored-by: Isaac
* OMNI-1193: drop scheduled_tasks.metadata column
Per PR review (aravind-segu): the metadata blob's only intended use was
source_schedule_id provenance on rows migrated from an external scheduler
— a single field better expressed as a typed column than a catch-all blob,
and not written by this persistence-only PR (always "{}"). Remove it now;
a typed column can be added if/when the external-scheduler merge lands.
Drops the column across model, migration, entity, store ABC + impl, and
updates the store + migration tests. 82 tests pass; ruff clean.
* OMNI-1193: store scheduled_task ids as Binary(16) UUIDs
Per PR review (aravind-segu): convert the owned scheduled-task id PKs to
16-byte UUIDs, aligning with the in-flight repo-wide Binary(16) UUID
convention. Adds a Uuid16 TypeDecorator (canonical UUID string in Python,
BINARY(16) on MySQL / BLOB/BYTEA elsewhere — same cross-dialect approach as
the existing _CKSUM32 digest column).
Converts scheduled_tasks.id, scheduled_task_runs.id, and the
scheduled_task_runs.scheduled_task_id self-ref. Cross-table reference
columns (agent_id, conversation_id, last_run_conversation_id) stay String
since their referents (agents.id, conversations.id) remain String PKs.
Updates the model, migration, entity + store docstrings, and both test
suites to use UUID-valued ids. 82 tests pass; ruff + mypy clean.
* OMNI-1193: add execution_target + host_id to scheduled_tasks
Persist where a routine fires, for the M2 sandbox/connected-host resolver
(no fire-path logic yet — persistence only, like the rest of this PR):
- execution_target: connected_host | managed_sandbox — the strategy the fire
path resolves at run time (connected_host → owner's live host; managed_sandbox
→ provision/adopt a sandbox). Int-coded enum (connected_host=1,
managed_sandbox=2) matching the state/kind/status pattern, server_default=1,
CHECK IN (1,2). Existing rows default to connected_host (the V1 behavior).
- host_id: nullable String(64) — for connected_host, the specific host to pin
(relates to hosts.host_id; no DB FK, Rule R032). NULL = owner's freshest
online host; always NULL for managed_sandbox (provisioned under a
deterministic id at fire time). Stays String, not Uuid16 — hosts.host_id is
String and this PR doesn't own that table.
No per-routine provider column (provider comes from deploy config) and no auth
columns (identity rides on the resolved host). Threaded through model,
migration, entity, store ABC + impl, and the enum codec, with round-trip +
CHECK + default tests. 90 tests pass; ruff + mypy clean.
* refactor(db): read Uuid16 back as bare hex to match schema-wide UUID convention
Flip Uuid16.process_result_value from the dashed canonical form
(str(uuid.UUID(...))) to the bare 32-char hex string (.hex, no dashes),
aligning #2247's scheduled-task id representation with #2228's bare-hex
form so that PR's rebase is a no-op on representation. The 16 DB bytes
are unchanged — only the Python-side read-back string differs.
Also flip the test id-mint helper and the byte-ordering test literals to
bare hex so round-trip assertions hold, and update Uuid16 / ScheduledTask
docstrings. Includes the staged migration re-chain onto the current
upstream alembic head (down_revision bb2c3d4e5f6a -> 9d820f91deef).
Co-authored-by: Isaac
* docs(routines): strip internal PR/scheduler scaffolding from OSS comments
Remove self-referential PR-sequencing language ("This PR persists …",
"a later PR", "(future) scheduler", "persists the shape only") and
internal migration/merge-roadmap references ("external scheduler",
"reference platforms", MySQL roadmap clause) from docstrings and inline
comments in the Routines feature files.
No code, type, or schema changes — comment/docstring lines only.
* fix(store): resolve three blocking review findings on ScheduledTaskStore
Finding 1: update() could not clear host_id or last_run_conversation_id
to NULL because None was overloaded as both "unchanged" and "set to NULL".
Introduce a module-level _UNSET sentinel; None now means "set to NULL"
for those two nullable fields. ABC kept in sync.
Finding 2: delete() orphaned scheduled_task_runs rows (no DB-level FK per
Rule R032, so cascade is application-owned). Delete the task's runs in
the same session before removing the task row.
Finding 3 (doc-only): two :param id: docstrings in db_models.py said
"canonical UUID string" (dashed) when Uuid16.process_result_value returns
bare 32-char hex (no dashes). Aligned with the entity and Uuid16 docs.
All changes covered by new TDD tests (red → green).
The web client's maybeFlushQueuedHead gate checks s.status === 'streaming'.
That status only clears to 'idle' when the idle session.status SSE carries
the same response_id that set activeResponse at turn start. Pi's extension
generated a new ++sequence id for every event, so the running/idle pair
never matched and status stayed 'streaming' permanently — queued follow-up
messages were never dispatched even after Pi finished replying.
Fix: store the response_id set in agent_start in activeResponseId, and
reuse the captured value in agent_end. The fallback (a fresh id) fires only
when agent_end is reached without a prior agent_start response_id, which
should not happen in normal operation.
The pinned-session project flyout (#2595) opens a Radix HoverCard on a
pinned, project-owned row. On a touch/mobile viewport there is no real
hover, so tapping the row to navigate also opened the HoverCard, which
then lingered over the chat page after navigation.
Gate the flyout off below the `md` breakpoint via useIsMobileViewport().
Forcing `projectFlyoutName` to null on mobile routes the row through the
plain ContextMenu/link path (no HoverCard mounted) and restores the
native `title` tooltip, since every downstream branch already keys off
that value.
Co-authored-by: Isaac
* fix(server): widen host-bound runner-connect grace to 10s
On the first message to a host-bound session, the server waits for the
create-time runner's tunnel to register before forwarding. The grace was
3s, but a freshly-launched runner needs ~5.5s to boot and connect its WS
tunnel. The wait timed out, abandoned the still-booting runner, and
relaunched a second one from scratch — roughly doubling cold-start latency
(~12.7s observed) and orphaning the first runner process.
Widen the grace to 10s so the first message rides the runner that create
already launched instead of relaunching. The wait stays event-driven (it
wakes the instant the runner's hello frame arrives) and still exits early
when the daemon convicts the runner dead, so a genuine startup failure
does not now cost a full 10s.
Co-authored-by: Isaac
* fix(web): keep "Working…" lit when live status beats a stale offline poll
The main chat's "Working…" indicator was suppressed whenever the open
session's runner read offline, checked before the running/waiting status.
The open-session `/health` poll is strict (runner_online true only while a
tunnel is registered) and runs on a 10s cadence, so on a fresh session's
first turn its first request lands while the runner is still connecting and
returns runner_online=false — held for up to 10s. The authoritative
`session.status: running` SSE edge arrives in that window but the gate
ignored it, so the indicator never appeared.
A session actively reporting running/waiting cannot have an offline runner,
so let its live status win over the lagging poll: only suppress on
known-offline when the session is otherwise idle (preserving the
don't-spin-a-dead-session-on-a-background-shell-tally case).
Surfaced by the faster host-bound runner connect (this branch): the turn
now starts inside the poll's stale-offline window instead of after it.
Co-authored-by: Isaac
* fix(web): align sidebar rows to a consistent two-column grid
The sidebar's top nav (New session, Search), section headers, project
folders, and session rows each carried their own horizontal padding, so
icons and labels landed at slightly different X positions down the list.
Pull every row onto one grid: icons on the left column, labels/nested
chats on the label column. New session uses gap-1 px-2, Search moves its
icon to left-2 / pl-7, flat session rows drop to px-2, and nested project
chats indent with pl-3 (footers follow at pl-5).
Co-authored-by: Isaac
* test(e2e-ui): regenerate visual baselines
---------
Co-authored-by: omnigent-ci[bot] <294685417+omnigent-ci[bot]@users.noreply.github.com>
* feat(web): show project name in pinned session hover flyout
Pinning a session lifts it out of its project folder into the flat
"Pinned" sidebar section, which dropped the visual cue for which project
it belongs to. Hovering a pinned, project-owned row now opens a flyout
showing the session title plus a folder icon and the project name,
reusing the existing project label already resolved for the kebab menu.
The flyout uses the shared HoverCard primitive (Cursor-style right /
top-aligned placement, matching AgentHoverCard) and is scoped to pinned
rows — non-pinned rows still convey their project via the folder they
sit in.
Co-authored-by: Isaac
* test(e2e_ui): cover pinned-row project hover flyout
Add a Playwright e2e that files a session into a project, pins it (lifting
it into the flat Pinned section), then hovers the pinned row and asserts the
flyout surfaces the folder icon + project name and the session title. Drives
the real project-move PATCH → label → pinned peel → hover flyout chain the
Sidebar unit tests mock out, and exercises the browser hover that opens the
Radix HoverCard (which jsdom can't).
Co-authored-by: Isaac
* feat(web): show full wrapping title in pinned project flyout
Session titles have no length cap (the server schemas and the rename
input are both unbounded), so the flyout's one-line `truncate` clipped
longer titles with an ellipsis. Clamp to 3 wrapped lines instead so the
full title shows and wraps while the card stays tidy — the complete text
stays in the DOM.
Co-authored-by: Isaac
An idle pi-native session kept queueing web messages client-side instead of
sending them: the composer showed "Send a follow-up (queued)" with a green
(idle) session dot, and only a tab switch unstuck it.
The pi extension minted a fresh response_id on every external_session_status
edge (agent_start running, agent_end idle). The web store clears its local
"streaming" flag only when the idle edge's response_id matches the running
edge that opened the turn (or when activeResponse is already null); with
mismatched ids neither branch fired, so status stayed "streaming" forever.
shouldQueueSend then queued every message and maybeFlushQueuedHead refused to
drain (both bail on status === "streaming"). switchTo hard-resets the store,
which is why a tab switch masked it. claude-native never hit this because its
forwarder reuses one turn-scoped id across both edges.
Mint a per-turn response_id in agent_start and reuse it in agent_end so the
running/idle pair matches, matching claude-native's contract.
Co-authored-by: Isaac
* slack integration initial commit
* fix the issue where slack server preamturely terminates the response
* fix the issue where long responses could cause msg_too_long
* support slack mrkdwn
* address PR feedback
* pass pre-commit
* fix(timer): reject zero-delay repeats and surface HTTP delivery failures
Repeating timers with seconds=0 busy-looped sleep(0)+POST; HTTP 4xx/5xx
wake responses were also ignored because status was never checked.
* style(timer): satisfy ruff format on HTTP error test assert
* fix(timer): reject non-finite seconds so NaN cannot bypass guards
NaN/Inf compare false against every bound, so repeat=true could still
hot-loop. Also align the schema copy with the repeat>0 rule.
* fix(sessions): stop duplicating the kickoff prompt on native sub-agents
A native terminal session (claude-native / codex-native) has a single
writer for its conversation history: the transcript forwarder, which
mirrors every user prompt the CLI logs back into the conversation. The
follow-up message path already respects this via the
_is_native_terminal_session bypass, but the session-create path forwarded
initial_items through _forward_event_to_runner unconditionally, which
persists the prompt AP-side. The forwarder then echoed the same prompt,
so the kickoff rendered twice.
Route create's initial_items through _dispatch_session_event_to_runner so
native sessions take the same single-writer bypass: the prompt is
delivered to the harness but not persisted AP-side, leaving the forwarder
as the sole writer. Non-native sessions still persist-and-forward.
Add an integration test that reproduces the duplication end-to-end: spawn
a native sub-agent with a kickoff, replay the forwarder's echo, and assert
the kickoff appears exactly once. Parametrized over claude and codex; a
non-native control proves the plain path is unaffected.
Signed-off-by: Brandon Hawi <brandonhawi1@gmail.com>
* docs(sessions): explain the native single-writer dispatch at the kickoff call site
Addresses review feedback: the _forward_event_to_runner ->
_dispatch_session_event_to_runner swap reads as a trivial rename but
encodes the whole fix. Add a call-site comment so the intent (native
single-writer bypass) is visible and the change isn't reverted.
---------
Signed-off-by: Brandon Hawi <brandonhawi1@gmail.com>
GLM and DeepSeek stream their output on the reasoning_content channel.
Pi's openai-completions parser only consumes that channel when the
model entry declares "reasoning": true, so the dynamically-registered
bare entry left the stream with no content and the turn failed with
"Stream ended without finish_reason".
Fixes#2560
Co-authored-by: Isaac
* feat(opencode-native): render live tool-call cards in the web chat UI
Extend live tool-call cards (spinner + ticking elapsed timer) to
opencode-native sessions, matching claude-native (#1499). The forwarder
already stamps each turn's assistant messageID as the response_id on its
function_call items but never put it on the status edges, so the server
never learned the in-flight turn id and the web rendered static cards.
- _post_status now stamps an optional response_id on the edge.
- Capture the assistant messageID in _on_message_updated; emit a running
edge carrying it once per turn and stamp the same id on idle.
- Defer the running edge until the id is known (session.status busy can
precede the assistant message.updated).
Closes#1872
* retrigger CI
* retrigger
CI
* Attach response id to the idle edge
* retrigger
CI
* feat(goose-native): live tool-call cards in the web chat UI (issue #1876)
goose_native_forwarder mirrored only assistant prose; tool calls were
invisible in the web chat and the live-card spinner never appeared.
Changes:
- _extract_tool_calls(): parse toolreq parts from assistant content_json
into (tool_id, name, args_json) triples.
- _extract_tool_result(): parse toolresp parts from tool-role rows into
(tool_id, output_text); tolerates both "id" and "tool_use_id" fields.
- _message_to_items() replaces _message_to_item(): returns a list so one
assistant row can produce a prose message + N function_call items; tool
rows produce function_call_output items. _read_new_items() preserved for
backward compat with existing tests.
- _read_new_rows(): new thin helper that returns raw DB rows so the poll
loop can track per-turn state while iterating.
- forward_goose_store_to_session(): per-turn live-card state (in-memory):
* current_turn_response_id minted on the first assistant/tool row of
each turn ("goose:turn:{msg_id}"), reset on the next user row.
* posted_running_response_id dedupe guard fires "running" + response_id
exactly once per turn so the web UI enters the streaming lifecycle.
* "idle" + response_id posted when the next user row arrives (turn
closed), or after _IDLE_AFTER_QUIET_S (8 s) of transcript quiet
(heuristic for the last turn with no following user message).
- Tests: 9 new unit tests covering _extract_tool_calls, _extract_tool_result,
and _message_to_items; existing 5 tests updated for the refactored API.
Signed-off-by: gocoolp <go4java@gmail.com>
* fix(goose-native): precise live-card close + restart replay for the turn lifecycle
Address AI-review findings on the quiescence heuristic:
- The 8s quiet window did double duty as the normal turn close and the
dead-turn backstop, so it could not be both short enough for a snappy
close and long enough to survive a real tool call: any call quieter
than 8s flickered (idle then running again on the result row), and
every final prose reply lingered in running for 8s.
- Goose's agent loop ends a turn on an assistant reply with no tool
calls, so the final prose row now posts the closing idle immediately;
the quiet window survives only as a minutes-scale backstop
(_STALLED_TURN_IDLE_S) for turns that died without a close (TUI
interrupt, Goose crash).
- Turn state is replayed from the store on restart (_replay_open_turn):
resumed rows keep the original turn id instead of splitting the
streaming group, and a running edge left unclosed by a crash is
closed instead of spinning forever.
Loop-level tests drive forward_goose_store_to_session end to end
against a recording poster to pin the lifecycle edges.
Co-authored-by: Isaac
---------
Signed-off-by: gocoolp <go4java@gmail.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
Two E2E-UI shard-0 tests flake on mount-time races, unrelated to any
product change:
- test_search_filters_all_files: `search.fill(...)` can race the rail's
mount-time re-render (?view=explore scope restore + first listing) and
the composer's autofocus, so the typed query is dropped before the
debounced /search fires. The tree then stays unfiltered and the
alpha-count-0 assertion fails (a Playwright trace showed the search box
empty and the text in the composer, with /search never called). Wait
for the initial listing to settle, then assert the query value actually
landed before checking results.
- test_agent_info_copies_session_id: the header info trigger mounts only
after the session binds/hydrates, so clicking it right after goto can
time out. Wait for the trigger to be visible before clicking.
Both also get the repo's @pytest.mark.flaky(reruns=2) marker (as
test_clone_session / test_mobile_workflow already use) as a backstop for
the residual timing race, rather than widening per-action waits.
Co-authored-by: Isaac
The conversations split (#2341) left archived on omnigent_conversation_metadata
while the sort keys (created_at/updated_at) stayed on the AP conversations
table. list_conversations could no longer filter+sort+limit in one query, so it
pre-fetched every non-archived id in the workspace and fed a giant IN(...) into
the AP query. #2562 fixed the kind half; this fixes archived: the list_sessions
sidebar path still prefetched archived from the Omnigent DB.
Move archived onto conversations (migration + backfill), filter it inline on the
AP query, and read/write it on the AP row. Removes the parent-scoped in-memory
archived post-filter and rewrites the ACL prefetch to read session_permissions
directly. After this, list_conversations' Omnigent-side prefetch is ACL-only.
Co-authored-by: Isaac
Stop routing new issues/PRs to ckcuslife-source. Same form as the
dbczumar pause: move the login from `owners` to the inert
`owners_paused` array rather than deleting it, so re-activating is just
moving it back.
policies drops to one active owner (TomeHirata). Rather than draft a new
active owner into the area, the >=2-owners integrity check now counts
owners_paused -- pausing someone shouldn't force adding a new active
owner to keep the file valid.
Co-authored-by: Isaac
The child-session sidebar previews run a per-conversation "newest N message
items" query (list_latest_message_items_for_conversations /
_ranked_latest_message_items) that filters
workspace_id + conversation_id IN (...) + type = 'message', ranked by
position DESC.
The existing unique index (workspace_id, conversation_id, position) covers the
partition and order but not the type filter, so Postgres seeks the
conversation's item range and heap-rechecks type on every row, discarding the
non-message majority (function_call / function_call_output / reasoning items
dominate an agent transcript). Ordering type before position lets the scan seek
to (workspace_id, conversation_id, type) and walk position DESC directly. The
same index also serves list_items(type=...) (e.g. the compaction and
assistant-text lookups), which filter the identical column shape.
Plain (non-partial) index so it builds identically on SQLite, PostgreSQL, and
MySQL — partial indexes were dropped for MySQL compatibility in z5a2b3c4d5e6.
Added to both the model __table_args__ and an Alembic migration so the
migrated (single-DB) and create_all (split AP DB) schema paths stay in sync.
This is a secondary optimization: the full-table-scan pathology in this query
was already fixed by removing the id-only self-join (#2546). This index removes
the residual type heap-recheck and is independent of the conversations/metadata
DB split.
Co-authored-by: Isaac
The conversations split moved `kind` and `archived` to the Omnigent-pool
metadata table while `parent_conversation_id` stayed on the AP-pool
conversations table. Because the two filters could no longer combine in one
SQL statement, `list_conversations(kind="sub_agent", parent_conversation_id=…)`
began prefetching EVERY non-archived sub-agent id in the workspace from the
metadata table, materializing it into Python, and re-injecting it as a giant
`id IN (…)` on the AP query. The child-sessions rail (fired on every SSE
connect with limit=100) and the sidebar status roll-up paid this
workspace-wide scan on every call, which is the post-split slowdown.
`kind` is fully determined by parent-nullness — a conversation is a sub-agent
iff it has a parent — and every writer already couples them. So:
- `_to_conversation` derives `kind` from `parent_conversation_id`, making it
the single source of truth (and correct even for an orphaned row whose
metadata write crashed).
- `list_conversations` expresses the kind filter as `parent_conversation_id
IS [NOT] NULL` directly on the AP table, and skips the metadata prefetch
entirely for parent-scoped queries — the perfect `idx_conversations_parent`
index match, restoring the pre-split single-query plan. `archived` is
applied on the returned page's already-fetched metadata.
- `list_child_conversation_ids_by_parent` drops its workspace-wide sub_agent
prefetch; `parent_conversation_id IN (…)` already implies sub-agent.
Adds split-DB regression tests: kind survives a missing metadata row, and the
parent-scoped listing no longer opens a second (prefetch) Omnigent-pool
session.
Co-authored-by: Isaac
`uv tool install "omnigent[databricks] @ git+..."` resolves fresh from
pyproject.toml (ignoring uv.lock). In that resolve, omnigent's direct
protobuf>=6 pin conflicts with the databricks-vectorsearch that newer
databricks-ai-bridge wants (it pins protobuf 5.x), so the resolver
backtracks ai-bridge to 0.17.0 -> mlflow 3.2.0 -> pyarrow<22 -> 21.0.0.
pyarrow 21.0.0 has no cp314 wheel, so on Python 3.14 uv falls back to
building it from source and fails.
Both floors are required, and neither works alone:
- databricks-ai-bridge>=0.19 is the first release that accepts a
protobuf>=6-compatible databricks-vectorsearch (0.66), lifting mlflow to
3.14 and pyarrow to 24 (which has cp314 wheels).
- databricks-mcp>=0.9.0 stops the resolver from escaping the ai-bridge
floor by dropping mcp to 0.1.0 (which pulls no mlflow/pyarrow at all).
With both, the databricks extra installs from wheels on Python 3.12, 3.13,
and 3.14 (verified end-to-end): databricks-mcp 0.9.0, ai-bridge 0.19.0,
databricks-vectorsearch 0.66, mlflow 3.14.0, protobuf 6.33.6, pyarrow
24.0.0. Matches what uv.lock already resolved, so no version churn.
Co-authored-by: Isaac
* fix(pi): recover post-tool JSON parse errors
* test(pi): cover post-tool JSON parse recovery
* fix(pi): surface post-tool errors at agent_end instead of fabricating success
Returning at an errored message_end leaves pi's turn-terminal agent_end
queued on the persistent RPC session; the next turn reads that stale
event as its own end and every later turn is off-by-one (empty replies,
scrambled ordering). Synthesizing a successful TurnComplete from the
last tool result also reported failed turns as clean successes and fed
raw tool JSON to parents as assistant text.
Instead, record the message_end error, drain until agent_end (pi always
emits it after an errored call; its own rpc-client keys idle on it),
then fail the turn with pi's real error. EOF before agent_end still
surfaces the recorded error. Aborted turns keep their existing
immediate-return path.
Co-authored-by: Isaac
---------
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
* clarify compact unavailable for model-less harnesses
* fix model-less compact test to assert the harness it actually builds
build_agent_bundle injects config.harness=claude-sdk into every executor
that doesn't set one, so the model-less agent under test reported
harness_kind claude-sdk and the agents_sdk assertion could never pass.
Pin an explicit openai-agents harness (the exact scenario from the
linked report) and assert that name in the error message.
Co-authored-by: Isaac
---------
Co-authored-by: C1-BA-B1-F3 <noreply@users.noreply.github.com>
Co-authored-by: Dhruv Gupta <dhruv.gupta@databricks.com>
`_ranked_latest_message_items` selected the whole `SqlConversationItem` row —
including the `search_text` Text column — but the only consumer
(`list_latest_message_items_for_conversations`, feeding the child-session rail
preview) reads just `data` via `_to_item`. On a chatty child, `search_text`
roughly doubles the bytes pulled per row for no benefit.
Project only the columns `_to_item` needs (plus `conversation_id`/`position`
for grouping/ordering and the `row_num` window). No behavior change — the
preview reads `data`, which is retained; the window function and its index
alignment are untouched.
Adds a regression test asserting the ranked subquery does not select
`search_text` (guarding against a refactor back to `select(SqlConversationItem)`)
while previews still resolve from `data`.
Co-authored-by: Isaac
When Goose interruption falls back to terminating the ACP subprocess, clear the cached session, prompt, initialization, and capability state. This ensures the replacement process performs a fresh handshake and session/new instead of reusing state owned by the terminated process.
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
* fix(web): don't switch sessions on Cmd+Arrow while editing the composer
## Related issue
N/A
## Summary
- Cmd+↑/↓ (Ctrl on Win/Linux) switched sidebar sessions even while typing
in the composer, disrupting editing and clobbering the native
caret-to-line-start/end behavior.
- Guard `useSessionSwitchHotkey` to bail when the keydown target is inside a
`textarea`, `input`, or `[contenteditable="true"]`, mirroring the existing
guard on ChatPage's sibling Cmd+Alt+Arrow message-nav handler. Session
switching still works when focus is outside an editable field.
## Test Plan
- `cd web && npx vitest run src/hooks/useSessionSwitchHotkey.test.tsx` — 12 passing.
- Updated the textarea test to assert no navigation while editing and added an
input companion case.
- Manual: focused the composer and pressed Cmd+↑/↓ (caret moves, no switch);
focused the page body and pressed Cmd+↑/↓ (switches with wrap).
## Type of change
- [x] Bug fix
- [ ] Feature
- [ ] Refactor / chore
- [ ] Docs
- [ ] Test / CI
- [ ] Breaking change
## Test coverage
- [x] Unit tests added / updated
- [ ] Integration tests added / updated
- [ ] E2E tests added / updated
- [x] Manual verification completed
- [ ] Existing tests cover this change
- [ ] Not applicable
## Coverage notes
Unit tests cover the guard (textarea and input focus bail out; body-focused
Cmd+Arrow still navigates). Manually verified in the web app that composer
editing is uninterrupted and session switching still works from outside fields.
* test(e2e): composer focus suppresses Cmd/Ctrl+Arrow session switch
The session-switch hotkey bails when the keydown originates inside an
editable field, so the composer-focus case now asserts the route stays
put and a body-focus companion asserts switching still works.
When a Claude Code native session's first interaction is a Skill / slash-command
(e.g. `/my-plugin:my-skill ARG-123`), the session got no title and the sidebar
fell back to the generic "Claude Code" label, so multiple skill-launched
sessions were indistinguishable.
Native sessions start untitled and rely on the server seeding the title from the
first user item that round-trips through the transcript bridge. But a Skill
arrives as a `slash_command` item (SlashCommandData), not a user `message`, and
`_title_content_from_item` only extracted text from user messages — so the title
stayed null.
Extend `_title_content_from_item` to also title from a Skill `slash_command`
(`kind == "skill"`), using the typed command `/<name> <arguments>`. Surfaced CLI
built-ins (`kind == "command"` — `/clear`, `/compact`, `/model`, `/effort`,
`/ultrareview`) are excluded so a built-in never becomes the session title; the
gate exactly matches the bridge's own classification. Seeding remains idempotent
(only untitled sessions, first interaction wins) and does not collide with the
existing REPL/composer skill-title path (a separate event route).
This is the low-risk mechanical fix the issue flags as an interim mitigation
(guaranteeing the sidebar is never just "Claude Code" for skill-launched
sessions); an LLM-generated descriptive title is a possible future enhancement.
Tests: skill slash-command titles from the typed command (with/without args,
whitespace-stripped); a CLI built-in does not title; the user-message path is
unchanged.
Co-authored-by: Sabhya Chhabria <sabhyachhabria@gmail.com>
The os_env helper prepends its own project root to PYTHONPATH at spawn so
`python -m omnigent.inner.os_env` can import omnigent. Because `_shell_impl`
ran the agent's command with no explicit `env=`, that entry leaked into every
sys_os_shell command. Under a `uv tool install` the root is omnigent's
site-packages, which then shadows the project venv's own packages on sys.path
— e.g. a 3.12 `pydantic_core` failing to load under a 3.13 project, silently
turning `importorskip`-guarded tests into false-green SKIPs.
Strip only omnigent's own `_project_root()` entry from the env handed to shell
commands (preserving any other PYTHONPATH the caller set). The helper's own
startup import is untouched, so uninstalled-worktree runs and the active-
sandbox suite are unaffected.
Closes#1860
* fix(runner): per-uid harness tmp parent on POSIX for multi-user hosts
On a multi-user Linux host (one Unix account per developer sharing one
omnigent server), the shared /tmp/omnigent parent breaks runner startup:
whichever user's runner starts first creates the parent 0700, and every
other user's runner then dies in _sweep_orphans (unhandled PermissionError
on iterdir before v0.4.0). Loosening the parent to 1777 only moves the
failure: the sweep then stat()s other users' 0700 ap-* instance dirs
(handled since v0.4.0, but the sweep still walks foreign dirs and all
harness sockets share one world-writable directory). The documented
OMNIGENT_HARNESS_TMP_PARENT override cannot express a per-user path for
host-daemon-spawned runners because the daemon launch environment does not
carry operator env vars through.
Suffix the POSIX parent with the uid: /tmp/omnigent-1007. Socket paths
stay short and predictable, each user's sweep only ever sees their own
instance dirs, and single-user behavior is unchanged apart from the path
name. Windows already uses the per-user gettempdir().
Verified on a shared Ubuntu 24.04 host with concurrent native-codex
sessions from two Unix accounts (against 0.3.0 with this change applied
as a local patch, and 0.4.0).
Signed-off-by: Cas Steigstra <cas.steigstra@gmail.com>
* test(runtime): per-uid tmp parent regression + fix stale docstring
Adds tests/runtime/harnesses/test_process_manager.py::
test_default_tmp_parent_is_per_uid_on_posix — asserts the POSIX default
socket parent is /tmp/omnigent-<uid>, fails against the pre-fix bare
/tmp/omnigent. Also updates the _default_tmp_parent docstring to match.
Signed-off-by: Cas Steigstra <cas.chainfill@gmail.com>
---------
Signed-off-by: Cas Steigstra <cas.steigstra@gmail.com>
Signed-off-by: Cas Steigstra <cas.chainfill@gmail.com>
Co-authored-by: Cas Steigstra <cas.chainfill@gmail.com>
* fix(routing): infer openai-agents harness for xai/grok-* models (#1927)
xAI is classified OPENAI_FAMILY in configure_models.py and exposes an
OpenAI-compatible endpoint. The harness prefix table had entries for
every other OPENAI_FAMILY provider but nothing for xai/grok-* or bare
grok-*, so specs without an explicit harness failed validation.
Adds xai/grok- and grok- to _HARNESS_FOR_MODEL_PREFIX mapping to
openai-agents, matching the existing gpt- -> openai-agents pattern.
Closes#1927
* fix(routing): drop bare grok- entry, require xai/ prefix
bare grok-* has no provider prefix, so parse_model_string defaults it
to provider="openai" -- the harness would be right but the request
would hit api.openai.com instead of api.x.ai.
Only xai/grok- is kept. Two bare-grok test cases removed.
Three improvements to handle the ucode Codex app setup where the
model_provider lives in a sibling config file (e.g. ~/.codex/config1.toml)
and the gateway URL is workspace-hosted rather than dedicated-subdomain:
1. Scan sibling config*.toml files when the primary ~/.codex/config.toml
has no matching [model_providers.X] table. The Codex app writes config1.toml
for profile-switched setups (e.g. ucode profile).
2. When the provider table has no auth command (ucode uses ambient SDK auth),
derive a !command from resolve_databricks_workspace + _databricks_codex_auth_command
so Pi can refresh the bearer token per request.
3. Accept workspace-hosted gateway URLs (e.g. workspace.cloud.databricks.com/
ai-gateway/...) in _is_databricks_ai_gateway_url. Previously only dedicated-
subdomain URLs (id.ai-gateway.cloud.databricks.com) were accepted. For the
model-listing API call, extract the workspace URL directly from the transport
base_url hostname instead of requiring a ~/.databrickscfg DEFAULT profile.
The aa1b2c3d4e5f + bb2c3d4e5f6a migrations split agent_id and model
settings out of conversations into a new agent_configuration table.
get_conversation() was then doing two serial session.get() calls — one
for SqlConversation, one for SqlAgentConfiguration — before the meta
and labels fetches. Since both tables are in the AP DB with the same
PK (workspace_id, conversation_id), replace the two calls with a single
LEFT OUTER JOIN, cutting one round-trip per get_conversation() call.
get_conversation() is called on every authenticated request, so this
directly addresses the 10-23x latency regression observed after the
2 AM migration deploy (GET /v1/sessions/{id} 6.4ms→149.9ms,
GET /v1/sessions 11.5ms→140.6ms, PATCH 6.6ms→75.5ms, etc.).
2026-07-15 00:34:43 +09:00
1690 changed files with 293576 additions and 106351 deletions
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/`.
existing="$(gh pr list --head "$branch" --base "$BASE" --json number --jq '.[0].number')"
@@ -122,6 +144,6 @@ jobs:
--title "Bump version to ${resolved}" \
--body "Automated version bump via \`.github/workflows/bump-version.yml\` (mode: \`${MODE}\`, input: \`${NEW_VERSION}\`).
Rewrote \`[project].version\` and sibling \`==\` pins across all three packages (\`pyproject.toml\`, \`sdks/python-client\`, \`sdks/ui\`), the runtime \`VERSION\` constant in \`omnigent/version.py\`, and regenerated \`uv.lock\`.
Rewrote \`[project].version\` and sibling \`==\` pins across all four packages (\`pyproject.toml\`, \`sdks/python-client\`, \`sdks/ui\`, \`integrations/slack\`), the runtime \`VERSION\` constant in \`omnigent/version.py\`, and regenerated \`uv.lock\`.
Generated by \`scripts/update_versions.py\`. CI does not auto-trigger on GITHUB_TOKEN PRs — re-open or push to run it."
Generated by \`scripts/update_versions.py\`. Opened via the omnigent-ci App when configured (CI runs automatically); on the GITHUB_TOKEN fallback, re-open or push to kick CI."
# Fail fast on HTML comments in the MDX: `<!-- ... -->` is invalid in
# MDX (only `{/* ... */}` works) and would break the site's `next build`
# only after the PR is opened. Catch it here so we never ship a red PR.
if [ -n "$post" ] && grep -qF '<!--' "${SITE}/${post}"; then
echo "::error::Drafted ${post} contains an HTML comment (<!-- -->); MDX requires {/* */}. Aborting."
exit 1
fi
if [ -n "$post" ]; then
printf '\n---\n\n**Enjoying Omnigent?** If this is useful to you, [give us a star on GitHub ⭐](https://github.com/omnigent-ai/omnigent). Come say hi on [Discord](https://discord.gg/omnigent), or [check the latest release](https://omnigent.ai/releases).\n' \
>> "${SITE}/${post}"
fi
# Generate the hero illustration from the drafter's IMAGE_PROMPT (the
# per-feature subject) plus a fixed brand style suffix, via the image
# model on the same gateway host. Fail-soft: any error leaves heroArt
# blank (the index falls back to a placeholder card), never blocking
# the draft. The scene is machine-drawn from the prompt, so no secret
# can reach it; the drafted-file secret scan above already ran.
image_prompt="$(sed -n 's/^IMAGE_PROMPT:[[:space:]]*//p' "/tmp/drafter_out_${i}.txt" | head -n1)"
if [ -n "$post" ] && [ -n "$image_prompt" ]; then
# GATEWAY_BASE_URL is scoped to THIS invocation only (not the step
# env), so the unsandboxed drafter run above never sees it and it
# 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)"
if [ -n "$(gh pr list --repo "$TAP_REPO" --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then
echo "Formula 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**.\n\nRegenerates the stable `url`/`sha256` and every `resource` stanza from the PyPI dependency tree of `omnigent==%s` (resolved with `uv pip compile` for macOS arm + intel), spliced into the hand-tuned template in `omnigent-ai/omnigent` (`.github/scripts/homebrew/omnigent.rb.template`). The structural parts (`depends_on`, `install`, `test`) are unchanged.\n\nOnce `brew test-bot` builds the bottles, label this PR **`pr-pull`** so the tap'"'"'s `brew pr-pull` workflow commits the `bottle do` block and merges.\n\nGenerated by `omnigent-ai/omnigent` `.github/workflows/homebrew-tap-pr.yml` on the **%s** release.' "$VERSION" "$VERSION" "$TAG")"
gh pr create \
--repo "$TAP_REPO" \
--base main \
--head "$BRANCH" \
--title "omnigent $VERSION" \
--body "$body"
- name:Note skipped (no App token)
if:steps.app-token.outputs.token == ''
run:|
echo "::warning::OMNIGENT_BOT_APP_ID/KEY missing, or the omnigent-ci App isn't installed on $TAP_REPO with contents:write + pull-requests:write. The formula was generated (see the job summary) but the PR was not opened."
echo "### Homebrew tap PR skipped" >> "$GITHUB_STEP_SUMMARY"
echo "The omnigent-ci App token couldn't be minted — install the App on \`$TAP_REPO\` with contents:write + pull-requests:write and rerun." >> "$GITHUB_STEP_SUMMARY"
# An already-open PR just picks up the force-pushed update.
if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty')" ]; then
@@ -126,7 +150,7 @@ jobs:
# 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:"
- name:Open or update the release-post PR (omnigent-site)
if:env.DRY_RUN != 'true'
working-directory:site
env:
GH_TOKEN:${{ steps.app-token.outputs.token }}
@@ -158,7 +482,12 @@ jobs:
echo "Release-post PR already open for ${RELEASES_BRANCH} — force-push updated it."
exit 0
fi
body="$(printf 'Publishes the **%s** release post at `/releases/%s`, mirroring the curated GitHub Release notes.\n\nGenerated by omnigent `.github/workflows/publish-changelog.yml`. Edit the GitHub Release, not this file.' "$TAG" "$VERSION")"
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)")"
fi
gh pr create \
--repo "$SITE_REPO" \
--base main \
@@ -173,6 +502,7 @@ jobs:
# when the branch doesn't exist or carries nothing beyond main (e.g. a patch
# release with no staged docs).
- name:Open docs-branch → main PR (omnigent-site)
if:env.DRY_RUN != 'true'
working-directory:site
env:
GH_TOKEN:${{ steps.app-token.outputs.token }}
@@ -206,3 +536,39 @@ jobs:
--head "$DOCS_BRANCH" \
--title "docs: publish ${VERSION%.*} docs to the live site" \
--body "$body"
# ::add-mask:: redacts rendered logs, not artifact files — scrub the key
# from artifacts (incl. the unscanned stderr) before upload.
# `dry_run` defaults TRUE (repo convention, same as the vscode release
# workflows): the plan job prints exactly what would happen; nothing is pushed.
name:Release
on:
workflow_dispatch:
inputs:
version:
description:"Version to release, e.g. 0.6.0rc1 or 0.6.0 (no leading v)."
required:true
type:string
ref:
description:"Branch/tag/SHA to cut release/vX.Y.0 from. Only consulted when the branch does not exist yet (rc1); later phases build from the existing branch head."
required:false
default:main
type:string
dry_run:
description:"Plan only: validate + print what would happen, push nothing."
required:false
type:boolean
default:true
skip_ci_check:
description:"Skip the green-CI assertion on the base commit (flaky-check escape hatch — use deliberately)."
required:false
type:boolean
default:false
skip_benchmark:
description:"Skip the pre-cut benchmark regression check (escape hatch — use deliberately)."
required:false
type:boolean
default:false
# Nothing here writes with GITHUB_TOKEN; pushes use the App token.
permissions:
contents:read
# Serialize all release runs: two concurrent cuts (even of different versions)
# could race the same release/vX.Y.0 head.
concurrency:
group:release
cancel-in-progress:false
jobs:
# Releases are maintainer-only. `workflow_dispatch` is open to anyone with
# write access, so gate on the dispatcher's actual repo role instead of a
# hand-kept list. `github.actor` on a dispatch is the dispatcher.
authorize:
if:github.repository == 'omnigent-ai/omnigent'
runs-on:ubuntu-latest
timeout-minutes:5
steps:
- name:Require admin/maintain role
env:
GH_TOKEN:${{ github.token }}
ACTOR:${{ github.actor }}
run:|
set -euo pipefail
role="$(gh api "repos/${GITHUB_REPOSITORY}/collaborators/${ACTOR}/permission" --jq .role_name)"
case "$role" in
admin|maintain)
echo "Dispatcher ${ACTOR} has role ${role} — authorized." | tee -a "$GITHUB_STEP_SUMMARY" ;;
*)
echo "::error::Release workflows require the admin or maintain role (dispatcher ${ACTOR} has '${role}')."
exit 1 ;;
esac
# Resolve everything and validate BEFORE mutating anything. Runs checkout-free
# (pure API reads) and also serves as the whole dry run.
case "$VERSION" in *rc[0-9]*) prerelease=true ;; esac
{
echo "version=${VERSION}"
echo "tag=v${VERSION}"
echo "branch=release/v${major}.${minor}.0"
echo "prerelease=${prerelease}"
} >> "$GITHUB_OUTPUT"
- name:Resolve branch, base commit, and tag state
id:state
env:
GH_TOKEN:${{ github.token }}
VERSION:${{ steps.derive.outputs.version }}
TAG:${{ steps.derive.outputs.tag }}
BRANCH:${{ steps.derive.outputs.branch }}
REF:${{ inputs.ref }}
run:|
set -euo pipefail
# `gh api` prints the error body to STDOUT on 404, so capturing with
# `|| true` would treat the "Not Found" JSON as an existing ref —
# gate on the exit code instead.
if branch_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${BRANCH}" --jq .object.sha 2>/dev/null)"; then
branch_exists=true
base_sha="$branch_sha"
# `ref` only applies at branch creation. An explicit non-default ref
# that disagrees with the branch head is a mistake, not a retarget.
if [ "$REF" != "main" ]; then
ref_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${REF}" --jq .sha)"
if [ "$ref_sha" != "$branch_sha" ]; then
echo "::error::${BRANCH} already exists at ${branch_sha}; ref=${REF} (${ref_sha}) would not be used. Re-dispatch without ref, or delete the branch if this is recovery."
exit 1
fi
fi
else
branch_exists=false
base_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${REF}" --jq .sha)"
fi
# Tag state: absent -> normal; at the converged release commit ->
| sed -n 's/^version = "\(.*\)"$/\1/p' | head -1)"
if [ "$tag_sha" = "$base_sha" ] && [ "$stamped" = "$VERSION" ]; then
already_done=true
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 "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."
else
echo "2. Merge the CHANGELOG PR, curate the ${TAG} draft notes, then dispatch finalize-release.yml (tag=${TAG})."
fi
} >> "$GITHUB_STEP_SUMMARY"
# First cut of a cycle (rc1) immediately moves main to the next .dev0 so main
# never re-freezes and doc-sync keeps deriving the right X.Y-docs branch.
if ! python3 -c 'import os, sys; from packaging.version import Version; sys.exit(0 if Version(os.environ["VERSION"]) > Version(os.environ["MAIN_VERSION"]) else 1)'; then
echo "Released ${VERSION} sorts below main's ${MAIN_VERSION} — skipping the main bump." \
| tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
gh workflow run bump-version.yml --repo "$GITHUB_REPOSITORY" \
--body "🔁 \`/rerun\`: no failed CI runs on the current head (\`${SHA:0:7}\`) to re-run. If a check is stuck *pending*, it needs a push or a maintainer, not a re-run."
exit 0
fi
RERAN=""
while IFS=$'\t' read -r id name; do
[ -n "$id" ] || continue
echo "• Re-running failed jobs in '$name' (run $id)"
# --failed: re-run only the failed jobs (cheapest path for a flake).
# --repo is REQUIRED: this job has no checkout, so `gh run rerun`
# cannot infer the repo from a git remote and would fail client-side.
if gh run rerun "$id" --repo "$REPO" --failed; then
RERAN="$RERAN"$'\n'"- $name"
else
echo "::warning::Could not re-run '$name' (run $id) -- may be in progress."
RERAN="$RERAN"$'\n'"- $name ⚠️ (skipped: already running or not re-runnable)"
fi
done < <(printf '%s\n' "${FAILED[@]}")
NOTE="The \`Merge Ready\` gate re-evaluates automatically when these complete."
gh pr comment "$PR_NUMBER" --repo "$REPO" \
--body "🔁 \`/rerun\`: re-running failed jobs on \`${SHA:0:7}\`:${RERAN}"$'\n\n'"$NOTE"
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,477 @@ 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 fix] Image and file attachments now survive session relaunches on remote host runners; attachments that fail to load show a visible marker instead of silently disappearing. (#2085)
- [UI / Feature] Voice dictation in the composer now works in Electron, Firefox, and Chromium via optional server-side transcription (`omnigent[dictation]`) — local models, live streaming partials, audio never leaves your server. (#2093)
- [UI / Bug fix / Test/CI] Hide Claude task completion control messages from conversation history while preserving them for resume context. (#2104)
- [Bug fix / Feature / Docs] Operators can mount pre-created PersistentVolumeClaims (NFS/SMB/SAN) into Kubernetes sandbox runners via `sandbox.kubernetes.pvc_mounts` (read-only by default) (#2435)
- [Bug fix / Test/CI] `/compact` no longer races when multiple compact requests hit the same session at once (#2585)
- [Bug fix / Test/CI] `sys_call_async` / `sys_cancel_async` now consistently use `handle_id` as the cancel identifier. (#2586)
- [Bug fix / Test/CI] Runner idle timeout no longer kills sessions waiting on async tools, timers, or approval prompts (#2588)
- [Bug fix] Misconfigured runner tool policies deny tool calls instead of silently allowing them (#2589)
- [UI / Feature] Slash-command menus now match any part of a command's name, so `/using-superpowers` finds `/superpowers:using-superpowers` (#2655)
- [Bug fix] Host-launched runners now reuse delegated credentials instead of repeating Databricks authentication during startup. (#2762)
- [Feature] Projects are now a first-class entity with a `/v1/projects` CRUD API (create, list, rename, delete) and per-session membership. (#2765)
- [Feature] `omni usage` reports your LLM cost for today / the last 7 / 30 days, with a per-session per-model cost breakdown (#2787)
- [Feature / Chore] Native Claude sessions start faster by coalescing runner initialization into one handshake. (#2793)
- [UI / Bug fix / Feature / Docs / Test/CI] Claude-native launch and in-session pickers now share the selected host's live model catalog, including Claude Code's managed routes. (#2831)
- [Bug fix] Managed BoxLite sandboxes remain available after provisioning so agent launches can execute commands reliably. (#2846)
- [Feature] Server-side smart routing can now call an external `routes:select` router via `routing.provider: external`, with provider-agnostic auth (`api_key`) and model-name mapping (`model_prefix`) (#2864)
- [UI] Chat code blocks no longer load the syntax-highlighter engine until the first (#2886)
- [UI / Bug fix] The main chat "Working…" indicator now clears reliably when the session goes idle, instead of occasionally staying lit after a reply completes. (#2900)
- [Bug fix] The performance benchmark harness now records HTTP failures and continues the rest of the suite instead of aborting, and excludes fully-failed runs from the summary averages. (#2917)
- [Feature / Test/CI] Scheduled tasks can now be created without a workspace or a pinned host for non-code work (research, summaries, chat-only, MCP-only); an unset host runs on your live host at fire time, and an unset workspace defaults to the host's home directory. A pinned host is now authorized (existence + ownership) at create time rather than only at fire time. (#2946)
- [Feature] Set `OMNIGENT_CONTAINER_RUNTIME=podman` to use Podman (or another supported runtime) globally instead of Docker, without editing every agent's YAML. (#2949)
- [Bug fix] Sending a message to a session whose Claude Code terminal crashed no longer (#2951)
- [Bug fix] The desktop app now always quits within a few seconds even if its background cleanup stalls or the OS re-quit is dropped. (#2972)
- [UI / Bug fix] Messages send immediately when a session's only remaining work is a background job, instead of being held in the queue until it finishes (#2974)
- [UI / Feature] Desktop update notifications now appear in a native corner toast that works (#2975)
- [Bug fix / Chore] Runner startup no longer waits several seconds for Git's optional untracked-file cache probe. (#2976)
- [UI / Bug fix] Pi sessions now show reasoning while it streams and after conversation history reloads. (#2979)
- [Feature] The runner log now records why the runner exited (crash traceback, signal, idle timeout, tunnel close, or parent death) (#2985)
- [UI / Feature] Set up a missing agent from the New Chat dialog with a guided, step-by-step checklist (#2987)
- [UI / Bug fix / Feature] HTTP headers can now be set and edited for HTTP MCP servers in the session agent info panel. (#2989)
- Capped unbounded DB list queries in the permission store and reduced session opens in `check_access`/`get_permission_level` from 2–3 to 1. (#2995)
- Deleting a conversation with many descendants now issues a single FTS DELETE instead of one per descendant. (#2999)
- [UI / Bug fix] Android auto theme and system-bar icons now stay readable with both device themes and explicit in-app theme overrides. (#3006)
- [UI / Feature] 3D model files (STL, 3MF, OBJ) now render an interactive preview in the file browser (#3007)
- [UI / Bug fix] Subagents panel Graph View now shows the same status dot colors as List View (#3009)
- [Feature / Test/CI] Scheduled-task runs now transition to a terminal state (`succeeded`/`failed`) as soon as the dispatched turn finishes, instead of staying `running` forever; run history is readable at `GET /v1/scheduled-tasks/{id}/runs`. (#3014)
- [Feature / Chore] When enabled, new sessions receive concise semantic titles in the background without adding work or latency to the active agent turn. (#3024)
- [Feature] Offload dictation speech-to-text to a remote worker with (#3025)
- [Bug fix / Docs / Test/CI] Codex-native subagents now appear in the Agents panel with their live conversations. (#3028)
- [Bug fix] Credential proxy no longer attaches injected credentials to TRACE/OPTIONS requests, and the egress proxy now honors Max-Forwards as a conformant intermediary. (#3029)
- [Feature] Import local Qwen, Kiro, Pi, and Kimi coding chats into Omnigent (#3032)
- [UI / Feature] Press ⌘⌥V (Ctrl+Alt+V) to toggle voice dictation from anywhere; while dictating, Enter keeps the text and Esc discards it (#3044)
- [UI / Feature] Added: "Auto · smart routing" harness option in the new-chat picker — lets the intelligent router pick both harness and model based on the task description (#3045)
- [Feature] Import existing OpenCode chats, including files and tool activity, with `omnigent import` (#3046)
- [Bug fix] Dictation streams now reliably release their worker slot when a browser disconnects abruptly. (#3048)
- [UI] New-session composer moves harness configuration into a gear-icon modal, with a cleaner agent picker (needs-setup and custom agents folded into flyouts) and Smart Routing offered as a model option. (#3050)
- [Bug fix / Feature] The Slack bot can now run against an Omnigent server deployed on Databricks Apps, (#3051)
- [Feature] Sessions can now be filed into first-class projects via `PATCH /v1/sessions/{id}` and listed with `GET /v1/sessions?project=<name>`, which dual-reads first-class membership and legacy project labels. (#3053)
- [Bug fix / Test/CI] Fixed SDK session telemetry always recording `harness: null` in server deployments not started via the CLI. (#3054)
- [Bug fix] Databricks OAuth CLI profiles no longer fail with a misleading "malformed profile" error; the message now explains the real fix (install `omnigent[databricks]` or refresh the OAuth session). (#3059)
- [Bug fix] An idle runner shutting down after inactivity no longer shows a scary "disconnected" error — just send a message to wake it back up. (#3060)
- [UI / Feature] The sidebar now uses first-class projects: create empty projects, rename and delete them, and file sessions into them — while existing label-based projects keep working. (#3061)
- [UI / Bug fix] The "Host is offline — click to reconnect" prompt now appears in the composer's host badge instead of a separate banner below the composer (#3062)
- [Test/CI] N/A (test-only change) (#3063)
- [Feature / Docs / Test/CI] New `databricks_cli` credential-proxy type lets sandboxed agents use the Databricks CLI without the real token entering the sandbox (#3080)
- [UI / Feature / Test/CI] Polly sessions running on Claude SDK can start Goal mode from the chat composer. (#3084)
- [UI / Bug fix] The Configure agent modal's footer no longer shows a gray background band behind Cancel/Save (#3089)
- [UI / Feature] The Sidebar is more compact and polished, with clearer status indicators and richer session details on hover. (#3092)
- [UI] Reordered the project-folder header buttons (new-session before the menu), (#3096)
- [Chore / Breaking] `omni server start` is removed; use `omni server --background` to launch the (#3105)
- [Bug fix] `omnigent server --host 0.0.0.0` now enables accounts (login) mode automatically instead of silently 401-ing every request (#3107)
- [Feature] Projects can store default session settings (host, workspace, harness, model, …) via a new `config` field on the projects API. (#3108)
- [Bug fix] Databricks-served Claude models no longer break non-streaming responses (prompt-policy and smart routing) when returning typed content blocks (#3109)
- [Feature] `omnidev omnigent <args…>` runs an omnigent command against the current checkout's pod via `uv run omnigent`, with the pod's isolated env applied (#3110)
- [UI / Feature] Configure a session's model, effort, and smart routing mid-chat from a new gear icon in the composer (#3111)
- [UI / Feature / Test/CI] Add the `/tasks` Scheduled Tasks page with sidebar navigation, task rows, empty states, suggestion chips, create-dialog entry points, and Playwright E2E coverage. (#3112)
- [Bug fix] Reading image files in a Claude Code native session no longer bloats conversation history and breaks resume/compaction on large sessions (#3113)
- [Bug fix / Test/CI] The iOS app no longer follows cross-origin redirects when probing a newly approved server for the Databricks workspace mount, so a consented host can't redirect the probe to a different origin. (#3115)
- [Bug fix] Forked native sessions (Claude Code, Codex, Pi, Qwen) again resume with their prior conversation history. (#3116)
- [UI / Feature / Test/CI] Workspace pane icons now explain themselves on hover, with a cleaner right-side session layout and compact Share action. (#3122)
- [UI / Bug fix / Feature] Add a dialog for creating recurring scheduled agent tasks. (#3123)
- [UI] Sidebar session hover flyouts and rows now align with the project rows — matching flyout style, title size, and right-edge padding. (#3124)
- [Bug fix] Resuming a session with large images stored in history no longer overflows the context window or breaks compaction, on both the SDK and native Claude Code paths (#3133)
- [Feature] `omnigent session import` loads a `session export` JSONL back into a server as a new session (#3141)
- [Feature] Telemetry now records the agent name for Polly and Debby sessions. (#3152)
- [Chore / Breaking] `omni integration slack start` is removed; use `omni integration slack --background` to launch the (#3153)
- [UI / Bug fix / Docs / Chore] Slack device login now requires a fresh password at the consent screen, closing a device-code phishing gap where an already-signed-in user could approve a login by reflex. (#3156)
- [Feature] `omnigent claude` keeps tool search enabled when launched with `CLAUDE_CODE_USE_GATEWAY=1`. (#3161)
- [Test/CI] Fix `test_session_stream_emits_heartbeat_on_idle` after `_session_labels_for_runner_spawn` was extracted into `omnigent.runner.native.orchestration`; patch the heartbeat cadence on `omnigent.runner.app` where it is defined and consumed. (#3163)
- [Bug fix] Crash reports are no longer lost when a process crashes more than once in the same second. (#3173)
- [Bug fix] Custom codex-native agents launch on the model declared in the agent spec (`executor.model`) instead of silently falling back to the provider default (#3175)
- [Bug fix] Single-file agent YAMLs that nest the executor under `type:`/`config:` (the bundle config.yaml shape) now fail at load time with the correct flat spelling, instead of silently running a harness inferred from the model prefix (#3178)
- [UI / Bug fix / Feature] Use native Codex goal mode from Polly's Goal control (#3181)
- [UI / Bug fix] Renaming a session now updates the name in the sidebar instantly instead of after a short delay. (#3185)
- [UI / Bug fix / Feature / Test/CI] Edit scheduled tasks and type exact run times, with a scrollable time picker and a consistent, fully-visible dialog. (#3186)
- [UI / Feature] Pinned sessions now persist server-side per user, so pins follow you across devices and browsers. (#3189)
- [Feature] New sessions now receive concise semantic titles automatically without additional configuration. (#3191)
- [Test/CI] `/rerun` PR comment re-runs failed CI on the current commit without dismissing approvals (#3195)
- [Feature] Native Codex sessions can now receive concise automatic background titles. (#3199)
- [Bug fix] Qwen3, inkling, and other non-OpenAI models now work in the Pi SDK executor harness (#3203)
- [Chore / Breaking] Slack-on-Databricks deploy: renamed `OMNIGENT_SLACK_WEBAUTH_BASE_URL` to `OMNIGENT_SLACK_DATABRICKS_APP_URL` (`--app-url`), removed the `WEBAUTH_PORT` / `DATABRICKS_WORKSPACE_HOST` overrides, and dropped deploy-time `uv lock` in favor of in-container `uv run`. (#3206)
- [UI / Bug fix] Sidebar session titles use the available space cleanly and reveal branch and action details only when needed. (#3208)
- [UI / Bug fix] Removed the redundant "Create new project" option from the sidebar project picker — create projects with the + icon next to Projects (#3210)
- [Feature] Smart routing now activates automatically when a server `llm:` block or an external `routing:` block is configured — no `OMNIGENT_SMART_ROUTING` env var needed (#3215)
- [UI / Feature / Test/CI] Scheduled task rows now show when each task will next run ("Next run in 15h") and a "Run now" action in the ⋯ menu to fire a task immediately, with refreshed row styling. (#3218)
- [UI / Feature] Projects now carry default session settings (host, working directory, agent, optional git worktree) that pre-fill the new-session composer. (#3221)
- [Bug fix] Fixed per-model cost attribution for native harnesses so a session's per-model (#3223)
- [UI / Bug fix] Codex task plans now stay in Tasks instead of being duplicated in chat. (#3249)
- [UI / Bug fix] Smart Routing no longer appears in the model dropdown for native terminal sessions (Claude Code, Codex, Pi), where it had no effect (#3259)
- [Bug fix] Sandboxed agents can now run tools managed by update-alternatives (awk, python3, editor, and similar) on Linux. (#3263)
- [Bug fix] Egress proxy now trusts corporate/MDM CA roots installed under the system `capath` directory, so TLS to hosts behind a corporate MITM works from a sandboxed agent. (#3264)
- [Bug fix] Large historical attachments no longer inflate replay and compaction context as inline base64 text. (#3267)
- [Docs] Contributors can now use `omnidev` as the documented worktree-safe local testing flow. (#3277)
- [Bug fix] Custom OpenAI Agents can use Unity AI Gateway Model Services with fully qualified model names when a Databricks provider or profile is configured. (#3288)
- [UI / Bug fix / Feature] Configure recoverable dangerous shell commands to ask for approval or deny execution, while always blocking catastrophic operations. (#3297)
- [Bug fix / Feature] Pi harness now routes kimi, inkling, GLM, qwen3, Gemini 3+, and Llama through the correct AI Gateway endpoints, fixing "Stream ended without finish_reason" errors and ensuring `system.ai.*` ids are used throughout. (#3307)
- [Feature] Add a top-level `justfile` with recipes for launching the iOS Simulator, running the Android debug build, starting the omnigent dev pod, and running pre-commit/lockfile normalization. (#3310)
- [UI / Bug fix] Sidebar header action buttons are now vertically centered with section titles. Session row hover is smoother, and active items no longer flash when hovered. (#3311)
- [UI / Bug fix] Aligns project folder icons and color with the rest of the sidebar. (#3317)
- [Bug fix] Unsupported Claude Code slash commands are now escaped and sent as regular user messages instead of leaving the native terminal in an undriveable state. (#3319)
- [Feature] `omnigent server` now tells users they can set `OMNIGENT_AUTH_ENABLED=0` to override automatic multi-user mode when binding to a non-loopback interface. (#3320)
- [Chore / Breaking] [Breaking] The deprecated `OMNIGENT_ACCOUNTS_ENABLED` environment variable has been removed; use `OMNIGENT_AUTH_ENABLED` instead. (#3322)
- [UI / Bug fix] Fixed pinned sessions being lost when the web UI was updated before the server. (#3323)
- [UI / Feature] Automations list: tasks now render as cards and show a live-updating relative next-run time ("Next run in 3 hours"). (#3324)
- [UI] Settings → Appearance no longer has a separate Sidebar font size control, and a new "Reset to defaults" button restores every appearance preference after confirmation. (#3326)
- [UI] Starting a session inside a project now names the project in the new-session (#3327)
- [UI] The Files workspace tab now uses a stacked-files icon. (#3329)
- [UI / Feature] Automations: scheduled tasks can now pick a model and reasoning effort in the create/edit dialog (defaults to the agent's settings). (#3331)
- [UI / Bug fix] Fixed sessions pinned in the updated web UI being lost after the server was updated. (#3332)
- [UI / Feature] Native harness setup now checks the installed CLI version and prompts to upgrade if it is too old. Cursor's missing-binary case is normalized to the same structured `binary-missing` signal as the other CLI-backed native harnesses. (#3335)
- [Bug fix] Pi sessions now show a clear error when their Databricks login has expired, instead of silently accepting messages with no reply (#3336)
- [Test/CI] N/A (internal CI change). (#3338)
- [Bug fix] claude-sdk harness now surfaces harness-level failures (expired login, auth error) as structured errors instead of storing them as assistant messages. (#3342)
- [UI] Align the sidebar brand row with the rest of the navigation. (#3346)
- [UI / Bug fix] Short links like `#3090` in chat markdown tables no longer stack one character per line (#3350)
## [v0.6.0] — 2026-07-21
- [Bug fix] `sys_os_shell` commands no longer inherit omnigent's own `PYTHONPATH` entry, so project subprocesses resolve their own installed packages instead of omnigent's. (#1861)
- [Bug fix] Stopping a Goose turn no longer leaves stale ACP session state behind when the subprocess is terminated. (#1928)
- [Bug fix] Cancelling or deleting a session during agent cold start no longer leaks the harness subprocess. (#1982)
- [Bug fix] macOS sign-in with a hardware security key (e.g. YubiKey) works again. (#2036)
- [UI / Bug fix] Tool-call cards keep their live spinner when a superseded response terminates mid-turn (fixes a first-turn "no spinner" on native-terminal harnesses). (#2045)
- [Bug fix] hermes-native tool-call cards now show a live spinner and elapsed timer while a tool runs, including on the first turn. (#2046)
- [Bug fix] Fix blank white page on Safari/iPadOS older than 16.4 caused by regex lookbehinds on the web UI boot path (#2105)
- [UI / Feature / Chore] A project's "new session" pencil now prefills the composer from the project's latest session — host, repo, agent, and a fresh git worktree branch — so starting a new chat in a project is one prompt away. (#2133)
- [UI / Feature] Filter the Archived sessions view by project (#2134)
- [Bug fix / Docs / Test/CI] Polly now verifies pytest test-count discrepancies against collected cases before recording miscount/fabrication claims. (#2140)
- [Bug fix] Evict the cached claude-sdk client when a turn is cancelled so the session recovers on the next turn (#2169)
- [UI / Bug fix] Tapping an Android notification now opens the waiting session (or the inbox). (#2210)
- [Bug fix] Fixed the headless Hermes harness registering no Omnigent MCP server (agent had no builtin tools). (#2216)
- [Feature / Breaking] Ids are now bare 32-character hex (no `conv_`/`ag_`/`host_` prefixes) stored as 16-byte binary; existing prefixed ids in URLs, configs, and clients keep working. (#2228)
- [Bug fix] `sys_list_models` no longer misreports codex `cli-config` providers and cursor workers as having no usable model provider. (#2237)
- [Bug fix] Cursor sessions now run shell tools in the declared workspace directory instead of the runner's working directory (#2244)
- [UI / Bug fix / Feature] Conversation view gains a turn-rail minimap for jumping between messages (#2285)
- [Feature / Docs] `sandbox.host_config` in the server config injects verbatim host `config.yaml` content (e.g. a `providers:` gateway block) into managed sandboxes before the host starts (#2306)
- [Bug fix] `omnigent server` now accepts documented boolean server-config values like `sandbox.kubernetes.in_cluster: false` instead of rejecting them at startup. (#2314)
- [Bug fix] Chat sessions now recover automatically from a brief reverse-proxy 404 during a backend restart, instead of freezing until a manual page refresh. (#2316)
- [UI / Bug fix] Bulk select-all/delete/archive in the sidebar now correctly handles collapsed sections (#2377)
- [Bug fix / Feature] `omnigent run --harness <name>-native` now launches every registered native harness with matching prompt and model behavior. (#2379)
- [Feature / Docs / Test/CI] Harness bench now reports Omnigent MCP relay support separately from vendor-native tool calling. (#2380)
- [Bug fix] Sessions are now stopped server-side before archive or delete, so SDK and API callers get the same stop-first behavior the web UI provides. (#2400)
- [UI / Bug fix / Test/CI] Sidebar session tabs no longer overflow their background on narrow widths. (#2425)
- [Bug fix] Local server startup now works when a system proxy intercepts loopback traffic. (#2433)
- [Bug fix] Native TUI harnesses (opencode, pi, hermes) no longer render multibyte UTF-8 as mojibake on deployments whose environment lacks a UTF-8 `LANG`/`LC_ALL`. (#2440)
- [Bug fix] Claude native sessions now back off rejected cost updates instead of retrying on every poll. (#2453)
- [Feature] Server now collects opt-out usage telemetry for session created, stopped, and deleted events to help understand product usage patterns. (#2457)
- [UI / Bug fix] Confirming a Japanese IME conversion with Enter no longer submits the session rename or new-project name inputs (#2459)
- [Feature] Server `llm:` config accepts an optional `fallback_models:` list; LLM-backed policies now retry alternate models before failing closed. (#2462)
- [Feature] Harden the LLM prompt-classifier policy against prompt injection by spotlighting untrusted content behind an unguessable per-evaluation marker. (#2463)
- [Feature / Docs / Test/CI] Harness bench now reports whether forked sessions retain and replay their source conversation history. (#2472)
- [Bug fix / Test/CI] Harness benchmark live tables now distinguish not-applicable capabilities from skipped probes. (#2475)
- [UI / Bug fix] New sessions respond faster to their first message, and the "Working…" indicator now appears immediately on the first turn instead of staying dark while the runner finishes connecting (#2478)
- [Bug fix] A policy-denied message (e.g. hitting the cost budget) now shows immediately instead of only after a page refresh (#2481)
- [Bug fix / Feature / Docs / Test/CI] Codex sessions using Databricks custom model aliases can surface reasoning summaries, and the harness benchmark now reports observable reasoning support. (#2482)
- [Feature / Docs / Test/CI] The harness benchmark can run selected capability dimensions and pin an exact model directly in each harness argument. (#2485)
- [Bug fix / Test/CI] Polly Cursor workers no longer ask for tool permissions by default (YOLO / auto) (#2493)
- [Feature] Polly defaults to Sonnet 5 for its brain and Claude Code workers, and Cursor Grok 4.5 for Cursor workers (#2500)
- [Bug fix] Polly Cursor workers default to grok-4.5 (#2503)
- [Bug fix] Polly Claude brain and Claude Code workers default to the sonnet alias (#2504)
- [Bug fix] Polly Claude brain and Claude Code workers inherit the provider default model again (#2507)
- [Bug fix] Codex-harness sub-agents no longer crash on macOS when the runner inherits a read-only working directory (e.g. `/` from the desktop app). (#2512)
- [UI / Feature / Test/CI] New chats can start with the Workspace panel collapsed via Appearance settings (#2516)
- [Bug fix] `sys_advise_models` is only offered to agents when intelligent routing is configured on the server. (#2517)
- [UI / Feature / Test/CI] OpenCode-native sessions now show their available models in the web model picker. (#2519)
- [Bug fix] Pi's `/model` command now shows all LLM models available on your Databricks workspace (fetched live from the serving-endpoints API) instead of a hardcoded list (#2525)
- [UI / Feature] The changed-files panel now shows a per-file line-change count (`+N −M`) beside each file. (#2526)
- [Bug fix] Telemetry rollout percentages now honor 0% and 100% exactly. (#2528)
- [Bug fix] Pi no longer shows a blocking "Trust project folder?" dialog when launched via Omnigent in a project with `.pi/` settings or extensions (requires Pi 0.79+) (#2529)
- [Feature / Docs / Test/CI] Harness metadata can now declare resume, steering, live queue, image, and compaction support for capability conformance checks. (#2530)
- [UI] Admin Settings pages (Members, Policies, Sharing) now share the same left and top alignment as the rest of Settings. (#2532)
- [Bug fix] Pi's `/model` command now shows GLM, Llama, Qwen, and other non-GPT Databricks models alongside Claude and GPT models (#2534)
- [UI / Bug fix] The Members and Sharing settings pages and the Share buttons are hidden in single-user mode (#2536)
- [Bug fix] Pi's `/model` command now correctly lists all available Databricks models when using the AI Gateway (cli-config) setup — the NXDOMAIN error that caused single-model fallback is fixed (#2540)
- [UI / Feature] Native Pi sessions can now switch model mid-session from the web composer, with the picker scoped to your logged-in models and synced to in-terminal `/model` changes (#2543)
- [UI / Feature] Add an opt-in "Hide unconfigured harnesses" setting that filters the new-chat picker to harnesses set up on the selected host (#2544)
- [Bug fix] Sending a message to an idle native Pi session no longer gets stuck in the "queued" state until you switch tabs (#2545)
- [Feature / Docs / Test/CI] Add `omnigent uninstall` for safely removing the CLI, installer-managed shell/config edits, and optionally local Omnigent state. (#2550)
- [Bug fix] Pi's `/model` command now shows all available Databricks models for ucode / Codex app profile-switched setups (#2552)
- [UI / Feature] Added a Nord color theme (arctic frost-blue palette) to the Appearance settings palette picker. (#2561)
- [Bug fix] Fix `uv tool install` of the `databricks` extra failing to build pyarrow from source on Python 3.14 (#2563)
- [Bug fix] Sessions list no longer does a workspace-wide id pre-fetch for the archived filter, restoring the single indexed query on the sidebar path. (#2568)
- [Bug fix] The CLI now shows a clear error instead of a raw traceback when the server is too slow or unreachable while starting a session. (#2572)
- [Bug fix] Pi harness: Databricks GLM/DeepSeek reasoning models no longer fail with "Stream ended without finish_reason" (#2573)
- [UI / Bug fix] Clicking a pinned session that belongs to a project no longer re-opens the project section after you've collapsed it. (#2583)
- [Bug fix] Claude-native messages that fail to reach Claude (or stall unsent) now surface the failure instead of silently disappearing. (#2591)
- [UI / Bug fix] The file editor now follows Omnigent's selected web and desktop color theme instead of the operating-system theme. (#2594)
- [UI / Feature] Hovering a pinned session now shows its project name in a flyout (#2595)
- [UI] Sidebar rows, section headers, and nav items now line up on a consistent grid (#2596)
- [Bug fix] Queued follow-up messages in the Pi web UI now dispatch immediately when Pi finishes replying (#2597)
- [UI / Bug fix] Pinned-session project flyout no longer opens (and gets stuck over the chat) when tapping a session on mobile (#2599)
- [UI / Feature] Diff viewer gains a "Wrap lines" toggle, and Find / Download / diff toggles now live in a single "⋯" View settings menu (#2600)
- [UI / Bug fix] Mobile session menu opens the project picker in place instead of an off-screen side flyout (#2602)
- [Bug fix] `sys_session_send` now reliably accepts model overrides when spawning named sub-agent sessions with a specific model (#2603)
- [Chore / Breaking] `omnigent[memory]` is renamed to `omnigent[hindsight]`; the Hindsight memory extra is now the canonical name. (#2605)
- [Bug fix] `omnigent host status` now hides the session table by default (use `--sessions` to show it), making the command significantly faster. (#2606)
- [Feature] `omnigent://<hostname>/c/<session_id>` links open that session in the desktop app, reusing an open window on that server in-place (#2607)
- [Bug fix] Fix native Claude sessions overflowing the context limit on reconnect when the conversation contained screenshots (#2609)
- [Feature] Session creation requests from the web UI now send an explicit `X-Omnigent-Client` header, enabling the server to record the exact client surface (web/desktop/ios/android) in telemetry without relying on User-Agent parsing. (#2615)
- [UI / Bug fix] Mobile: the Chat/Terminal toggle no longer reappears over the sidebar when opening a session's kebab menu (#2617)
- [UI] Project sidebar headers show a chevron in place of the folder icon on hover instead of a persistent caret next to the name (#2618)
- [UI / Feature] PDF files now preview inline in the file viewer, with scrollable pages and zoom (#2619)
- [UI / Bug fix] Find in file now toggles the editor's find bar closed on a second click, and its close button no longer shows a stray keyboard-hint tooltip (#2621)
- [UI / Bug fix / Feature] Find in file now works in the markdown editor — highlights and cycles matches, including terms split across bold/italic formatting (#2628)
- [Bug fix] Clicking an Omnigent session URL printed inside the embedded terminal now stays in the web app instead of opening a duplicate browser tab/window. (#2639)
- [Docs] Clarify optional install extras in the quick-start docs. (#2640)
- [Bug fix] `use_responses: false` now correctly selects the Chat Completions API in standard `config.yaml` bundles. (#2641)
- [Bug fix / Feature] Import existing Claude Code or Codex chats with `omni import`. (#2649)
- [UI / Feature] Customize a built-in color theme with shared light/dark accent, tint, contrast, and two-rail sidebar translucency controls. (#2650)
- [UI / Bug fix] Pinned sessions no longer show duplicate rows after upgrading — legacy pin ids are migrated to the new id format automatically. (#2651)
- [UI / Feature] Long-running Codex commands now stream their output into the web tool card while they run. (#2652)
- [UI / Feature] Randomize accent and background tint colors with a playful new dice button. (#2653)
- [Feature] `omnigent://<hostname>/c/<session_id>` links open that session in the iOS app, reusing the open window on that server in-place (#2661)
- [Bug fix] Pi's `/model` command now shows and correctly routes GPT, Kimi, Llama, GLM, and Gemini 3.x models alongside Claude (#2665)
- [UI / Bug fix] The Add Global Policy dialog now shows all available policies, including ones already applied. (#2668)
- [UI / Bug fix] The Add Policy dialog for sessions now shows all available policies, including ones already applied. (#2670)
- [Bug fix] Deleting or archiving a session now also stops any still-running sub-agent children, instead of leaving them running as unreachable orphans. (#2673)
- [UI / Feature] Find in file now works in the markdown and notebook preview — highlights and cycles matches in the rendered document (#2674)
- [Bug fix] Codex sessions through a Databricks gateway profile no longer fail with "Invalid Token" when the environment's `DATABRICKS_HOST` points at a different workspace than the profile (#2675)
- [UI / Feature] The file panel (changed files, browsing, diffs, and file contents) stays viewable when a session's runner is offline but its host is still connected — no need to send a message to wake it. (#2676)
- [UI / Feature] Select text in a PDF preview to add review comments with inline highlight overlays. (#2677)
- [UI / Feature] Codex plans now show up in the Tasks panel, the same way Claude Code todos do (#2678)
- [UI / Bug fix] Token expiration and other harness startup errors now appear immediately in the chat transcript instead of requiring a page reload. (#2681)
- [UI / Feature] Switching back to a recently-opened chat now renders instantly from cache instead of blanking and reloading over the network. (#2688)
- [UI / Bug fix] Fixed the comment box being hidden behind the keyboard when commenting on a file in the iOS app (#2694)
- [Bug fix] Host-bound sessions relaunch immediately after a Stop or host restart instead of waiting out the runner-connect grace. (#2699)
- [Test/CI] Add a `checkout_sha` input to the Benchmark workflow so nightly benchmarks can be run ad-hoc against a specific commit. (#2700)
- [Bug fix / Test/CI] Fresh installs no longer resolve an OpenAI SDK combination that crashes every OpenAI Agents harness turn. (#2713)
- [Feature] `omni import --harness <claude|codex> --last N` can now import up to 50 recent local chats at once. (#2724)
- [UI / Bug fix] Cursor setup now distinguishes CLI readiness from SDK API-key configuration and shows install/login guidance before web chat launch. (#2733)
- [UI] The Gateway option in model setup no longer cites OpenRouter as an example — use the dedicated OpenRouter option instead (#2738)
- [Bug fix] Sandboxed (`darwin_seatbelt`) agents now boot when the helper interpreter or the wrapped CLI is reached through symlinks — uv-installed Pythons and the standalone Claude CLI no longer die with `Operation not permitted`. (#2743)
- [Bug fix] A sandboxed claude-sdk agent no longer dies at connect time when its sandbox can't wrap the Claude CLI — it starts with native tools disabled (file/shell access stays sandboxed via the `sys_os_*` tools) and logs why. (#2749)
- [Feature] `omnigent setup` can now install Hermes directly before configuring its model provider. (#2751)
- [Bug fix] Sandboxed (`darwin_seatbelt`) agents no longer die at startup with "No usable temporary directory" — the private scratch tmpdir is now granted in the sandbox profile before the process is jailed. (#2759)
- [Feature / Docs / Test/CI] Benchmark reports now measure cold restarts of existing sessions whose runner is offline. (#2761)
- [Docs / Chore / Test/CI] Website release posts now use the narrative, prose-driven MLflow style (#2764)
- [Bug fix] opencode-native carries the user config's model default into the synthesized config when no model is pinned (#2775)
- [Feature] New sessions automatically get concise, useful titles generated by the agent already handling the conversation. (#2778)
- [Bug fix] Forking a Claude Code session onto pi (or other CLIs) no longer fails to start with `required_terminal_exited` (#2780)
- [Bug fix] Native Codex and Claude now resolve their CLI from common global install dirs and an `OMNIGENT_CODEX_PATH` / `OMNIGENT_CLAUDE_PATH` override when it isn't on the host daemon's PATH (#2788)
- [Feature] Nightly benchmark report now includes server CPU% and RSS memory usage (mean/min/max) alongside latency metrics. (#2795)
- [Bug fix] The changed-files panel now shows per-file line-change counts even when the list is served by the host (runner offline). (#2802)
- [Bug fix] Harness readiness now finds CLIs installed in common global dirs (nvm/npm/homebrew) that aren't on the host daemon's PATH, matching what the launch resolves (#2805)
- [UI / Bug fix / Feature] Config-file policies (declared via `omni server -c`) now appear in the admin Global Policies page as read-only entries labeled "Config". (#2807)
- [Bug fix] Codex sessions now inherit global `AGENTS.md` instructions. (#2809)
- [UI / Feature] Switching back to a recently-opened chat now renders instantly from cache instead of blanking and reloading over the network. (#2810)
- [Bug fix] Allow the ACP prompt deadline to be configured without changing its default. (#2817)
- [Feature] The Slack bot now handles tool approvals and questions inline — Approve/Deny (#2820)
- [UI / Feature] The Share dialog now shows a QR code so you can open a session in the mobile app by scanning it with your phone. (#2824)
- [Bug fix] Harness setup warnings now clear automatically without reconnecting the host. (#2828)
- [UI / Feature] Android app shows a floating server switcher pill with a dropdown menu for quick server switching (#2829)
- [Feature] Crash reports now show a calm, branded screen with a compact traceback and a one-tap prompt to file a pre-filled GitHub issue (#2841)
- [UI / Bug fix] Parallel sub-agent dispatch now explains when to use distinct task titles instead of accidentally reusing and serializing one child session. (#2860)
- [Bug fix] Fixed a context-window overflow on a live-streamed turn, leaking its harness process instead of ending the turn cleanly. (#2869)
- [Bug fix] Clicking "Stop session" no longer shows a spurious "runner disconnected" error (#2903)
- [UI / Bug fix] New-session Send button shows a spinner while the session is being created, instead of looking frozen (#2907)
- [Feature / Breaking] Per-harness startup command/args overrides via a polymorphic `harness:` config key + `OMNIGENT_<NAME>_PATH` env-var standardization (legacy `HARNESS_*_PATH` deprecated, slated for v0.8.0 removal) (#2933)
- [Bug fix] `omnigent[memory]` is restored as a backwards-compat alias for `omnigent[hindsight]` (the `memory` extra was dropped by #2605); it will be removed in 0.70. (#2938)
- [Feature] `OMNIGENT_SESSION_RENAME=on` re-enables the automatic first-turn session rename (disabled by default) (#2944)
## [v0.5.1] — 2026-07-10
- [UI] The desktop Browser tab is now hidden on older desktop builds that don't support the embedded browser, instead of showing a tab that does nothing (#2393)
## [v0.5.0] — 2026-07-10
- [Bug fix] Messaging a long-idle session no longer risks the new turn being killed mid-flight by the idle reaper (#1834)
Contributions are welcome. See [CONTRIBUTING.md](https://github.com/omnigent-ai/omnigent/blob/main/CONTRIBUTING.md) for how to set up your environment, run the checks, and open a pull request.
@@ -463,4 +543,3 @@ Thanks to all of our amazing contributors!
| `OMNIGENT_ACCOUNTS_COOKIE_SECRET` | secret | `openssl rand -hex 32` (pin it: ephemeral disk would otherwise drop sessions on restart) |
4. The Space builds + boots. Admin password is in the Space **Logs** on first
boot. The base URL is auto-detected from `SPACE_HOST`, so it needs no manual
set.
4. The Space builds + boots. No admin credential is auto-generated: first boot
prints a "No admin yet" line to the Space **Logs**, and the Space serves a
web Create-admin form where you pick your own username + password. The base
URL is auto-detected from `SPACE_HOST`, so it needs no manual set. To create
the admin directly instead, add `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD` as a
Space secret before first boot.
5. **Log in via the direct URL** `https://<user>-<space>.hf.space` in its own
tab — not HF's embedded preview. The session cookie is `SameSite=Lax`, which
browsers won't send inside HF's cross-origin iframe, so logging in from the
embedded view loops back to `/login`. The direct URL is top-level
(same-site), so login sticks. Make the Space **Public** so the direct URL
isn't gated.
isn't gated — but note the Create-admin form is unauthenticated until the
first admin is claimed, so a public Space can be claimed by the first
visitor. Pre-seed `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD` (step 4) or claim
the admin immediately after it goes public.
## Want persistence / multi-user later?
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.