[fork CI] Archives project filter — real test run - #5
Open
btli wants to merge 11 commits into
Open
Conversation
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
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
…alues 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
…ived 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
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
…ects 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>
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
- 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>
- 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>
btli
pushed a commit
that referenced
this pull request
Jul 12, 2026
* feat(sharing): add OMNIGENT_SHARING_MODE server gate (on / read_only / off)
Adds a tri-state session-sharing policy to create_app, defaulting from
the top-level OMNIGENT_SHARING_MODE env var (on / read_only / off) and
failing open to ON. When off, grant_permission is rejected (403) and the
SPA shows a "sharing disabled" dialog; when read_only, new grants are
capped at read (edit/manage rejected) and the Share modal offers only
read. GET /v1/info reports sharing_mode so the web app gates its Share
controls to match. Revoke/list and self-ownership grants are unaffected
in every mode.
Also accepts a static SharingMode or a per-request callable, so a
deployment can flip the policy at runtime (e.g. a Databricks SAFE flag)
without a restart.
Tests: 29 new server tests (coerce fail-open, create_app wiring incl.
the env var, /v1/info, and the 403/200 grant gate against a seeded
store) plus 3 web tests for the modal's off / read_only / on states.
Co-authored-by: Isaac
* feat(sharing/web): gray out Share affordances when sharing_mode is off
Extends the existing shareDisabled pattern so both the ChatHeader Share
button and the sidebar row's Share menu item render disabled (with a
tooltip) when /v1/info reports sharing_mode "off". read_only keeps them
enabled — the modal caps the grant level. Fails open (enabled) while the
capability probe is still loading.
Existing collaboration surfaces ("Shared with me", presence, fork) are
intentionally untouched: turning sharing off blocks *new* grants but does
not revoke existing access, so those must keep working.
Adds AppShell + Sidebar.rowActions tests for the off (disabled) and
on / read_only (enabled) states.
Co-authored-by: Isaac
* feat(sharing): add restricted_read_only tier (blocks home/root-cwd sessions)
Adds a fourth OMNIGENT_SHARING_MODE tier, restricted_read_only: it caps new
grants at read like read_only, but additionally rejects ALL grants (even read)
on a session whose working directory is a user home directory or the filesystem
root — that cwd exposes an entire home/filesystem, so it must not be shared.
- auth.py: SharingMode.RESTRICTED_READ_ONLY + workspace_sharing_blocked() helper
(recognizes /, /root, direct children of /home and /Users, and the server's
own ~; subdirectories of a home and an unset cwd stay shareable).
- routes/sessions.py: the grant gate looks up the session workspace and 403s a
home/root-cwd session entirely; other sessions fall through to the read cap.
- web: capabilities.ts recognizes the value; the Share modal presents the same
read-only UI as read_only. The per-session home/root block is enforced
server-side and surfaces as an error on the grant attempt.
Tests: coerce + /v1/info round-trip the new value, a workspace_sharing_blocked
truth table, and the gate (home/root cwd -> 403 even read; normal cwd -> read
ok / edit 403; no cwd -> read ok), plus a modal test for the read-only UI.
Co-authored-by: Isaac
* feat(sharing): admin panel control for the server-wide sharing mode
Makes OMNIGENT_SHARING_MODE runtime-configurable from Settings → Sharing, so an
admin can pick among the four tiers (on / read only / read only restricted /
off) without a redeploy. The env var remains the boot default; the admin choice
is a per-server override that wins when set.
Persistence follows the OSS operator-editable-state convention (no DB
migration): the override lives in <data_dir>/sharing_mode next to the admins
roster, read mtime-cached per request so a change takes effect immediately and
survives restarts.
- server/sharing_settings.py: file-backed override read/write (atomic,
mtime-cached), falling back to the env default when unset/unrecognized.
- server/app.py: the create_app default resolver now reads override-else-env
and marks app.state.sharing_mode_writable; an explicit static/callable mode
(managed/embedded, e.g. a SAFE flag) stays authoritative and non-editable.
- routes/sharing_mode.py: admin-gated GET/PUT /v1/sharing-mode reporting the
current mode + an `editable` flag + the tiers; PUT strictly validates (400 on
an unknown value, no fail-open) and 403s when not file-backed.
- web: a new admin-only Settings → Sharing section (SharingPage + useSharingMode
hooks + settingsNav entry) with a 4-tier picker, read-only when the server
reports editable:false.
Tests: file-override roundtrip + create_app precedence over the env default, the
admin route (GET state, PUT persist reflected in /v1/info and the gate, 400 on
unknown, 403 for non-admin and for a deployment-managed mode), and a SharingPage
suite (tiers render, choosing calls the mutation, read-only notice, non-admin
gate).
Co-authored-by: Isaac
* feat(sharing): add OMNIGENT_PUBLIC_SHARING switch for public (link) access
Adds a server-wide switch for public (anyone-with-the-link) read access,
independent of the sharing tiers: an org can keep normal user-to-user sharing
on while disabling public links. Controlled at the top level by the
OMNIGENT_PUBLIC_SHARING env var (default enabled, fails open) and, like the
sharing mode, overridable at runtime from Settings → Sharing.
When disabled, granting the __public__ sentinel is rejected (403), /v1/info
reports public_sharing_enabled: false, and the Share modal hides the "Public
access" toggle. User-to-user grants are unaffected.
- sharing_settings.py: file-backed public_sharing override (<data_dir>/
public_sharing) + env default parse, sharing the mtime-cached reader with the
sharing_mode override (cache refactored to a per-path dict).
- app.py: create_app gains a `public_sharing` param (bool / callable / None),
normalized to app.state.public_sharing + a public_sharing_writable flag;
/v1/info reports public_sharing_enabled.
- routes/sessions.py: the grant gate rejects a __public__ grant when public
sharing is off, independent of the sharing_mode gate.
- routes/sharing_mode.py: GET now also reports public_sharing_enabled +
public_sharing_editable; PUT accepts an optional public_sharing boolean
(each field independently writable, 400 when the body updates nothing).
- web: capabilities.ts carries public_sharing_enabled (fail-open true); the
Share modal hides the public toggle when off; the Sharing admin page gains a
"Public access" switch (read-only when deployment-managed).
Tests: server coverage for the env default / static / file-override wiring,
the public grant gate (blocked when off, user grants still allowed), /v1/info
reporting, and the admin GET/PUT (persist, reflected in /v1/info and the gate,
403 when not writable); web tests for the modal hiding the toggle and the
admin page's public switch.
Co-authored-by: Isaac
* test(sharing): regenerate openapi.json + update Admin-nav test
CI drift from the sharing work:
- openapi.json was stale — regenerated via scripts/dump_openapi.py to include
the /v1/sharing-mode GET/PUT routes and the SetSharingModeRequest body
(sharing_mode + public_sharing). Fixes test_openapi_json_matches_generator_output.
- settingsNav.test.tsx asserted the Admin group was exactly [members, policies];
the Sharing section added a third item. Updated the expectation to
[members, policies, sharing].
Co-authored-by: Isaac
* refactor(sharing): host-agnostic workspace block + rename endpoint to /v1/sharing
Addresses PR review:
#4 — workspace_sharing_blocked no longer resolves the server process's ``~``
(meaningless on a remote runner whose home lives on another host). It now
matches purely on path shape and covers the common home layouts: the
filesystem root (/), root's home (/root), and any direct child of /home,
/Users, or /var/home (ostree). Project-workspace roots (/workspace,
/workspaces/<repo>) are deliberately NOT blocked — they hold a single
checkout, not a whole home. Tests updated accordingly (drops the ~ case, adds
/var/home + a /workspaces project-dir shareable case).
#5 — the admin endpoint/resource now governs two settings (mode + public
access), so ``/v1/sharing-mode`` → ``/v1/sharing``, object ``"sharing_mode"``
→ ``"sharing"``, create_sharing_mode_router → create_sharing_router,
SetSharingModeRequest → SetSharingRequest, and the web hook useSharingMode.ts
→ useSharing.ts (useSharing / useSetSharing, SharingState / SharingUpdate).
The response's ``sharing_mode`` field (the tier value) and the SharingMode
enum are unchanged. openapi.json regenerated.
Co-authored-by: Isaac
* refactor(sharing): atomic admin PUT + docstring/copy accuracy
Follow-up on PR review:
- routes/sharing.py: validate AND authorize both fields before writing either,
so a both-fields PUT where only one setting is file-backed (mode editable,
public deployment-managed, or vice-versa) can no longer persist one override
and then 403 on the other. Adds test_admin_put_is_atomic_across_mixed_
writability (403 + the writable half is not persisted).
- app.py: create_app docstrings — sharing_mode now lists restricted_read_only;
public_sharing describes the env var as "enabled unless explicitly falsy
(0/false/no/off)" (matching public_sharing_env_default, not env_var_is_truthy)
and notes existing public grants are unaffected.
- SharingPage.tsx: surface the non-retroactive behavior — changes affect only
new shares; existing grants (including already-public sessions) keep working
until revoked.
Co-authored-by: Isaac
* test(sharing): e2e_ui share-button gray-out + harden grant-gate state reads
- sessions.py (#2 from review): the grant gate now reads app.state via
getattr(..., default) — getattr(request.app.state, "sharing_mode",
lambda: SharingMode.ON)() and the public equivalent — so a router mounted
without create_app (a focused test) can't AttributeError. Behavior-preserving
for every production path (create_app always sets both).
- tests/e2e_ui/collaboration/test_sharing_mode_off.py: a Playwright test for
the server-side kill switch surfacing in the SPA. Spins up a dedicated server
with OMNIGENT_SHARING_MODE=off (the shared live_server is session-scoped/on,
and the admin route is admin-gated for the headerless local identity),
creates a session, and asserts the header Share button is disabled with the
"Sharing has been disabled…" tooltip — served via the public-loopback alias
so the local-server disable doesn't mask it. Mirrors the assertion shape of
test_permissions_modal.py::test_local_server_disables_share_button_with_tooltip.
Co-authored-by: Isaac
btli
pushed a commit
that referenced
this pull request
Jul 23, 2026
…mnigent-ai#2997) * 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fork-internal PR to run the real CI suites (web Tests / vitest + E2E UI Tests / Playwright) in the
btli/omnigentnamespace againstpolly/archives-project-filter.Lives ENTIRELY within the fork (base
btli:main) — does NOT target upstreamomnigent-ai/omnigentand pings no maintainer. Exists only to get a genuine green CI signal.pull_request_targetgovernance gates (Maintainer Approval, E2E UI Required) will red-X here — expected (no maintainer entry / gateway secret on the fork). The signal that matters: web Tests + E2E UI Tests.Not for merge.