Skip to content

[fork CI] Archives project filter — real test run - #5

Open
btli wants to merge 11 commits into
mainfrom
polly/archives-project-filter
Open

[fork CI] Archives project filter — real test run#5
btli wants to merge 11 commits into
mainfrom
polly/archives-project-filter

Conversation

@btli

@btli btli commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Fork-internal PR to run the real CI suites (web Tests / vitest + E2E UI Tests / Playwright) in the btli/omnigent namespace against polly/archives-project-filter.

Lives ENTIRELY within the fork (base btli:main) — does NOT target upstream omnigent-ai/omnigent and pings no maintainer. Exists only to get a genuine green CI signal.

pull_request_target governance 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.

btli and others added 7 commits July 7, 2026 10:49
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>
@github-actions github-actions Bot added the size/XL Pull request size: XL label Jul 8, 2026
btli and others added 4 commits July 8, 2026 21:21
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Pull request size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant