Commit Graph
8260 Commits
Author SHA1 Message Date
Anthony Ruiz e3db8f7e01 fix: settle steer on every terminal path and preserve recovery drafts 2026-09-20 17:07:58 +00:00
Anthony Ruiz 1ee4fc1601 fix: resolve durable compressed sessions before WebUI resume 2026-09-20 17:07:58 +00:00
Anthony Ruiz 034e3edef7 fix(streaming): publish initial run and cancellation signal atomically 2026-09-20 17:07:57 +00:00
Anthony Ruiz a98d0c86d0 fix(steer): fence worker registration and final pending drain 2026-09-20 17:07:57 +00:00
Anthony Ruiz 26277f933b fix(steer): revalidate the active-run backend on the cache-only path
The cache-only Steer path only special-cased backend == "gateway" and sent
every other value — including a missing, empty, or unknown backend — to the
cached local agent. Another runtime could own the run while guidance was
enqueued on a stale local agent.

Fail closed instead: the in-process worker now registers its active run with
the explicit local backend tag (WEBUI_LOCAL_CHAT_BACKEND, the same "legacy"
value webui_chat_backend_mode() returns), and cache-only Steer only enqueues
agent.steer() on that tag. Gateway keeps its gateway_steer_queued outcome; a
missing, empty, or foreign backend returns stream_dead without steering.

Extends the positive-ownership regression with backend cases and adds a
worker-side check that the local backend tag is actually registered.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 20c16e9167 fix(steer): require positive ownership on the cache-only path
The cache-only Steer branch (no AGENT_INSTANCES worker) only rejected
*conflicting* ownership, so a missing stream owner or a missing
active-run session_id was treated as non-conflicting and let
agent.steer() run and report accepted without proving the requesting
session owns the run.

Fail closed instead: both the stream owner and the active-run session
must be present and strictly equal to the requesting session (backend
and phase are still revalidated) under the same STREAMS_LOCK ->
ACTIVE_RUNS_LOCK edge; any absent identity returns stream_dead without
calling agent.steer(). No cache eviction, no agent closing, HTTP write
still outside the locks.

Regressions: missing owner, missing/empty run session, both missing,
missing run entry -> stream_dead + steer never called; both present and
equal -> accepted, cache untouched. Legacy happy-path fixtures now
register the ownership they rely on.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 84d66fe45f fix(steer): make cache-only delivery atomic with cancellation
The cache-only compatibility path selected the matching cached agent,
checked stream liveness under STREAMS_LOCK, released the lock, and only
then called agent.steer(). A Stop running in that gap published
phase='cancelling' and detached STREAMS/AGENT_INSTANCES while the
earlier Steer still enqueued into the cached worker, so the HTTP
response reported accepted guidance that cancellation already owned.

Revalidate stream membership, owner, and active-run
session/backend/phase and call agent.steer() under the same
STREAMS_LOCK -> ACTIVE_RUNS_LOCK edge Stop uses; interrupt,
persistence, and HTTP writes stay outside the lock. Cache eviction and
agent closing remain absent.

Extend tests/test_steer_cancel_ordering.py with a cache-only reverse-
order test (AGENT_INSTANCES absent, matching cached worker, decoy cache
entry) gating both unlocked edges around the stream-lock reacquire.
Stop-first must return stream_dead without calling agent.steer(); the
selected cache object is asserted to be the intended worker. Proven
red on the pre-fix code (order ['cancel','steer']) and green after.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 1655113a81 fix: serialize cancellation ownership with active-run steer 2026-09-20 17:07:57 +00:00
Anthony Ruiz 91bd3e03b9 fix(steer): honor Gateway ownership before local cache fallback 2026-09-20 17:07:57 +00:00
Anthony Ruiz b89a3b344c test(steer): isolate the retained cache with monkeypatch 2026-09-20 17:07:57 +00:00
Anthony Ruiz 8869f34da0 test(steer): preserve mismatched entries in cache ownership regression 2026-09-20 17:07:57 +00:00
Anthony Ruiz c6152e4182 fix(steer): target the owned active worker after compression 2026-09-20 17:07:57 +00:00
nesquena-hermes f6a37b2381 Merge pull request #7678 from nesquena/stage/6962-dispatchable-autocomplete
Release: slash autocomplete announces only WebUI-dispatchable commands (#6962)
2026-09-20 06:20:16 -07:00
nesquena-hermes dc9911739d test(goal): assert /goal membership, not the exact passthrough list literal
#6962 adds 'stop' to the busy-command passthrough list in messages.js so a busy
/stop cancels immediately. test_frontend_has_goal_slash_command_and_status_event_handler
asserted the exact list literal, so it failed on a change that does not affect /goal.

Assert that 'goal' is a member of the list instead. Verified the assertion still
fails when 'goal' is actually removed from the list.
2026-09-20 12:55:11 +00:00
nesquena-hermesandwebtecnica 70dd8e4a4b docs(changelog): stamp #6962 dispatchable-only slash autocomplete
Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-09-20 12:33:32 +00:00
n 207d165184 Merge PR #6962: fix(commands): autocomplete only announces webui-dispatchable slash commands 2026-09-20 12:32:05 +00:00
webtecnica 813b293c82 fix(commands): remove duplicate top-level const breaking messages.js load
static/commands.js and static/messages.js both declared
const _BACKEND_EXECUTED_AGENT_COMMANDS at file scope. static/index.html
loads them consecutively as classic defer scripts in the same global
lexical realm, so the second declaration threw
SyntaxError: Identifier '_BACKEND_EXECUTED_AGENT_COMMANDS' has already
been declared, aborting messages.js entirely.

The constant was unused in both files (a duplicate authority mirror, not
a genuinely shared source), so both declarations are removed. The
existing three-way parity test is preserved, and a new regression
evaluates commands.js followed by messages.js in one shared
classic-script vm realm, asserting both files evaluate cleanly and the
dispatch consts of both files are bound.
2026-09-20 11:59:09 +00:00
webtecnica ff06f6d032 fix(commands): exact cross-authority parity for announced slash commands 2026-09-20 11:59:09 +00:00
webtecnica a85c1d1f62 fix(commands): exact cross-authority parity for announced slash commands (#6951) 2026-09-20 11:59:09 +00:00
webtecnica d03891a307 fix(commands): autocomplete only announces webui-dispatchable slash commands (#6951) 2026-09-20 11:59:09 +00:00
nesquena-hermes 173e673c76 Merge pull request #7674 from nesquena/stage/6989-async-wakeup-admission
Release: atomic per-origin async-delegation wakeup admission (#6989)
2026-09-20 04:13:34 -07:00
nesquena-hermesandwebtecnica f103dd4ca1 docs(changelog): stamp #6989 atomic per-origin async wakeup admission
Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-09-20 10:44:04 +00:00
n 1800233b5c Merge PR #6989: fix(async): atomic per-origin wakeup admission prevents budget exhaustion 2026-09-20 10:43:48 +00:00
webtecnica 06fc0575d5 fix(async): close pre-ACTIVE_RUNS stream window, never strand wakeup admission (#6989) 2026-09-20 10:36:36 +00:00
webtecnica b7f536b006 fix(async): atomic per-origin wakeup admission prevents delivery budget exhaustion (#6959) 2026-09-20 10:36:36 +00:00
nesquena-hermes d4a21baa43 Merge pull request #7670 from nesquena/stage/7654-codex-commentary-media
Release: session MEDIA auth + snapshots inspect Codex commentary sidecars (#7654)
2026-09-20 02:26:44 -07:00
nesquena-hermesandhappy5318 7d2d40833b docs(changelog): stamp #7654 Codex commentary MEDIA auth + snapshots
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
2026-09-20 09:01:29 +00:00
n 8bef9c44f5 Merge PR #7654: fix(media): session auth and snapshot capture inspect Codex commentary sidecars 2026-09-20 09:01:03 +00:00
happy5318 bc37675bc2 fix(tests): drop unused sys/types imports in #7565 codex commentary test
The test file imports `sys` and `types` but never references either.
Ruff F401 flags them. No test or runtime behavior change.

- Remove the 2 unused import lines
- 11 tests still pass
- No new ruff violations
2026-09-20 08:49:36 +00:00
happy5318 8e31ae9f93 # Session MEDIA auth and snapshot capture inspect Codex commentary sidecars (#7565)
## Thinking Path

The Agent's OpenAI Codex Responses adapter persists user-visible
assistant progress in `codex_message_items` with `phase:
"commentary"` while the outer `content` field is empty. The Agent
already stamps these items with `role: assistant` and a normalized
phase (`agent/codex_responses_adapter.py::_message_item` and
`_OutputScan._message`), and the rendering lane is owned by #6828.

The WebUI's two consumers of those persisted rows, however, only
read the outer `content` field:

- `api/routes.py::_session_media_token_allows_path` flattens
  `message.get("content")` through `_message_content_text()` and
  scans it for `MEDIA:` tokens.
- `api/media_snapshots.py::annotate_media_snapshots` does the
  same when annotating the settled transcript so previews can
  survive an in-place overwrite of the source file.

For a safe image emitted only inside commentary, the browser
request hits the real `_handle_media` route, owns the correct
session_id and exact path, but is denied 403 by the auth predicate
because it cannot see the assistant's persisted `MEDIA:` token.
Symmetrically, the snapshot annotator never captures commentary
artifacts, so a subsequent in-place overwrite rewrites history
without a recoverable snapshot.

The fix is the maintainer's narrow recommendation: one shared
fail-closed public-assistant-text iterator in
`api/media_snapshots.py` (`codex_commentary_text`), consumed by
both the auth predicate and the snapshot annotator. Keeping the
extraction in one neutral helper means the auth and capture paths
cannot drift apart on a future change. The fail-closed predicate
is a strict chain — outer role is `assistant`; item type is
`message`; item role is `assistant`; item phase normalizes to
`commentary`; content is a list; the part is textual `output_text`
with a string `text` value. Every step that fails returns the safe
empty string, so the helper is safe to concatenate with the
existing top-level extraction. User rows, user sidecars, reasoning
items, analysis/final phases, malformed parts, non-string
`text` values, and arbitrary nested fields are all skipped.

## What Changed

- **`api/media_snapshots.py::codex_commentary_text`** — new
  module-level helper that returns the concatenation of all
  public-text `output_text` parts inside assistant-owned
  `codex_message_items` with `phase: "commentary"`. The outer
  row role, item type, item role, item phase, content shape, and
  part type are all gated, so the helper is fail-closed and
  safe to concatenate with the existing top-level text extraction.
- **`api/routes.py::_session_media_token_allows_path`** — inside
  the existing per-message loop, the per-row `text` is now the
  concatenation of `_message_content_text(message.get("content"))`
  and the new `codex_commentary_text(message)`. The exact-path,
  owning-session, safe-MIME, URL-rejection, hard-denied, and
  symlink guards are unchanged. The commentary helper is imported
  lazily inside the loop so this file does not pick up a new
  top-level dependency.
- **`api/media_snapshots.py::annotate_media_snapshots`** — the
  per-row `text` is now the concatenation of the existing
  top-level string `content` and the new commentary helper
  output. The existing `media_re.findall()` and resolve/allow
  paths are unchanged, so otherwise-capture-eligible commentary
  previews are now snapshotted at settle time. The string
  pre-filter on `"MEDIA:" in text` is kept so the helper
  short-circuits before regex extraction when neither source
  contains a token.
- **`tests/test_issue7565_codex_commentary_media.py`** — new
  regression module with eleven tests: a positive
  valid-shape test, six fail-closed tests (user outer role,
  wrong item type, wrong item role, wrong phase, non-output_text
  parts, non-string output_text text), a case-insensitive
  commentary-phase test, a malformed-input test, and two
  integration static tests pinning that the auth predicate and
  the snapshot annotator both consume the new helper.

## Why

- A user-visible artifact that the agent emitted only inside
  commentary was returning 403 at the real `_handle_media` route
  boundary even with the correct owning session_id, and the
  artifact was never snapshotted at settle time. The fix restores
  parity with top-level public assistant content for the
  commentary phase specifically.
- Symmetry between auth and capture matters: a token that auth
  admits must be one capture can stamp, otherwise the served
  bytes can desynchronize from the snapshot pointer and the
  in-place-overwrite survivor feature (#6922) silently breaks.
  Sharing the helper makes drift impossible.

## Verification

```
$ .venv/bin/pytest \
    tests/test_issue7565_codex_commentary_media.py \
    tests/test_media_inline.py \
    tests/test_media_message_snapshots.py \
    -q --no-header
…
============================= 115 passed in 6.16s ==============================
```

Eleven new tests cover the helper contract and the integration
points. The 104 surrounding media_inline / media_message_snapshots
tests stay green.

## Contract

| Outer role | Item shape | Helper result | Auth result | Snapshot result |
| --- | --- | --- | --- | --- |
| `assistant` | `{type:"message", role:"assistant", phase:"commentary", content:[{type:"output_text", text:"…MEDIA:…"}]}` | text surfaced | `MEDIA:` token found; existing exact-path / safe-MIME / owning-session guards apply | `MEDIA:` token snapshotted at settle time |
| `assistant` | `{type:"message", role:"assistant", phase:"analysis", content:[{type:"output_text", text:"…"}]}` | empty | unchanged from existing top-level-only behavior | unchanged |
| `assistant` | `{type:"message", role:"assistant", phase:"final", content:[{type:"output_text", text:"…"}]}` | empty | unchanged | unchanged |
| `assistant` | `{type:"reasoning", …}` | empty | unchanged | unchanged |
| `assistant` | item role `user` | empty | unchanged | unchanged |
| `assistant` | item content not a list | empty | unchanged | unchanged |
| `assistant` | part type `tool_call` / `image` / `input_text` (even with text containing `MEDIA:…`) | empty | unchanged | unchanged |
| `assistant` | `output_text` with non-string `text` (dict / list / number) | empty | unchanged | unchanged |
| `user` (outer role) | any sidecar | empty | unchanged (user rows already skipped) | unchanged |
| `assistant` | no `codex_message_items` | empty | unchanged | unchanged |
| malformed (`None`, `""`, list, string, missing role) | n/a | empty | unchanged | unchanged |

The fix only widens the inspection surface for the one
public-text sidecar shape; everything else stays exactly as it
was, so existing safe-MIME, hard-denied, exact-path-equality,
owning-session, URL-rejection, and symlink guards remain intact.

## Risks

- **Commentary widening the inspection surface**: yes, by design.
  The widening is narrowly scoped to the outer-assistant
  / item-message / item-role-assistant / phase-commentary /
  part-output_text / string-text chain. The new tests pin every
  step in that chain and every out-of-chain shape that must
  remain rejected.
- **Existing safe-MIME / hard-denied / exact-path / symlink
  guards weakened?** No. The `media_capture_allowed` predicate,
  the `MIME_MAP` lookup, the `Path(ref).expanduser().resolve() ==
  target_resolved` equality check, the `if "://" in ref` URL
  rejection, the hard-denied state/secret paths, and the symlink
  protections are all unchanged. The only change is what text
  the regex sees.
- **Agent producer change**: the helper reads the same shape
  the agent already stamps (`role: assistant`, `phase:
  "commentary"`, `content: [{type: "output_text", text: "..."}]`).
  A future producer change that adds a new phase or part type
  requires an explicit follow-up PR; the helper's comments
  document the precise chain.
- **Helper duplication drift risk avoided**: by placing the
  helper in `api/media_snapshots.py` (the storage half) and
  having `api/routes.py` import it lazily inside the loop, both
  consumers go through one definition. A follow-up change to
  the helper's contract automatically applies to both call sites.

## Model Used

- Generated with `MiniMax-M3` via `happy5318/hermes-webui` worktree
  `/tmp/webui-worktrees/pr7565-codex-media`, branch
  `fix/codex-commentary-media` (off `origin/master` via `ghfast`
  mirror — `origin` was still TLS recv-failing at PR creation
  time; the mirror was used for `git fetch` and `git worktree add`
  only; push goes through the `fork` remote as usual).

## Related

- Closes #7565
- Surface code:
  - `api/media_snapshots.py` (new) `codex_commentary_text` helper
  - `api/routes.py::_session_media_token_allows_path` (modified
    to concatenate the helper output)
  - `api/media_snapshots.py::annotate_media_snapshots` (modified
    to concatenate the helper output)
- Adjacent lanes (intentionally not folded in):
  - #7294 / #7297 (safe-MIME, session_id, snapshot query
    parameters) — independent contract
  - #6828 (persisted-commentary rendering / recovery) — frontend
    UI; the WebUI auth predicate does not assume or expand its
    rendering scope
  - Open PRs #7367 and #6923 — overlap the token-extraction sites
    but neither implements this commentary ownership rule, per
    the maintainer's open-PR search
- Test surface: new `tests/test_issue7565_codex_commentary_media.py`
2026-09-20 08:49:36 +00:00
nesquena-hermes b09fded7b8 Merge pull request #7668 from nesquena/stage/7401-colon-model-labels
Release: keep custom model names containing colons whole in the picker (#7401)
2026-09-20 00:08:16 -07:00
nesquena-hermesandwebtecnica 47cbba48a4 docs(changelog): stamp #7401 colon-bearing custom model labels
Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-09-20 06:43:08 +00:00
n 9960d09d97 Merge PR #7401: fix(picker): keep custom model names containing colons whole 2026-09-20 06:42:46 +00:00
webtecnica da4e4ef316 fix(picker): catalog is authority for @custom labels; keep plain-lane colon models whole
The #7240 fix peeled the leading @custom: segment on first-colon split, but a
plain-lane custom id whose model carries a tag/variant colon
(@custom:ollamacloud/qwen3.5:397b) was still misread as slug=ollamacloud/qwen3.5,
rendering only "397b". Two changes:

- _dynamicModelLabels (the backend-populated catalog) is now consulted BEFORE
  the string peel: the backend knows the true provider/model split and ships an
  exact m.label per routing id, so catalogued ids — including plain-lane colon
  models and collision cases — render verbatim.
- The legacy string fallback now treats a slash-bearing first segment as the
  model itself (a provider slug is a config key or host:port authority and
  never contains '/'), mirroring the '/' means routable rule api/config.py
  applies when building ids.

Tests: add plain-lane @custom:ollamacloud/qwen3.5:397b controls, a
post-catalog dynamic-label control, and a collision case proving labels stay
distinct by full routing id. Driver now accepts an optional pre-hydrated
catalog map.
2026-09-20 06:33:58 +00:00
webtecnica e71c3e1f97 fix(picker): stop truncating model names with colons 2026-09-20 06:33:58 +00:00
nesquena-hermes 10003f46d4 Merge pull request #7666 from nesquena/stage/7124-compaction-visibility
Release: keep context compactions visible in long transcripts (#7124)
2026-09-19 21:15:13 -07:00
nesquena-hermesandruizanthony 48520d6473 docs(changelog): stamp #7124 compaction card visibility
Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-09-20 04:10:20 +00:00
n fe47d3f4ac Merge PR #7124: fix(ui): keep context compactions visible in long transcripts 2026-09-20 04:10:00 +00:00
Anthony Ruiz 8fe3325e12 fix(ui): keep current summary visible next to stale compaction markers
The settled current-summary card was only created when no compaction
marker was loaded at all. A transcript with older (stale) markers plus a
newer `compression_anchor_summary` that no loaded marker references
therefore lost the current summary, which master still rendered.

Decide the fallback from `_latestCompressionReferenceMessage()`
(`referenceMessageRawIdx < 0`) instead of marker presence, and feed that
decision into `_selectCompactionCardPlacements()` before any card node is
built so the fallback is the single preserved-task owner and marker cards
never attach the task list a second time.

Add a render-level regression that executes the real `renderMessages()`
over 220 messages, three stale markers, one preserved task-list message
and a newer summary: the current summary is visible and exactly one task
owner is rendered, in full, virtualized and server-truncated windows.
2026-09-20 03:48:19 +00:00
Anthony Ruiz 44b2a57109 fix(ui): preserve all pre-window compaction cards 2026-09-20 03:48:19 +00:00
Anthony Ruiz fab4968b17 test: remove private compaction fixture path 2026-09-20 03:48:18 +00:00
Anthony Ruiz 3eb06c192c fix(ui): preserve compaction cards across virtual windows 2026-09-20 03:48:18 +00:00
Anthony Ruiz c13819b6b0 test: stub _loadedCompactionMarkerRawIdxs in anchor-ownership node harness
The renderMessages node harness added in origin/master (#7125 lineage)
evals renderMessages in isolation; the new loaded-compaction gate calls
_loadedCompactionMarkerRawIdxs, which must be stubbed like the other
render helpers (_postProcessWithAnchorSuppression, _usedModelTurnChipLabel).
2026-09-20 03:48:18 +00:00
Anthony Ruiz ac83c94637 fix(ui): keep context compactions visible in long transcripts 2026-09-20 03:48:18 +00:00
nesquena-hermes 4aa92dedbd Merge pull request #7664 from nesquena/stage/7649-7630-cron-projects
Release: cron schedule round-trip (#7649) + system-project profile tagging (#7630)
2026-09-19 20:41:33 -07:00
c12cb38295 docs(changelog): stamp #7649 cron schedule round-trip + #7630 system-project profile tagging
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: carlotestor <carlotestor@users.noreply.github.com>
2026-09-20 03:15:11 +00:00
n 2ce84d9216 Fold gate fix: keep natural-language recurring schedules verbatim (#7649) 2026-09-20 03:14:48 +00:00
nesquena-hermesandhappy5318 aaae80292a fix(cron): keep natural-language recurring schedules verbatim on edit/duplicate
Gate follow-up to #7649. _cronScheduleForEdit() consulted sched.expr before
schedule_display, so opening or duplicating a job scheduled as
`every monday 9am` populated the field with the canonical `0 9 * * 1`. The
Agent rebuilds schedule_display from whatever the WebUI submits, so the
rewrite stuck and the user silently lost their phrasing.

Only the `once at ...` label is unparseable; every other schedule_display
form round-trips through parse_schedule(), so prefer it and fall back to
expr/expression.

The existing tests asserted on the SOURCE TEXT of the helper and stayed green
through this defect. Adds four tests that EXECUTE the real helper out of
panels.js via node; the natural-language case is red before this fix.

Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
2026-09-20 03:14:42 +00:00
n bc9390da63 Merge PR #7649: fix(cron): round-trip a parseable schedule on edit/duplicate; 400 on bad input 2026-09-20 03:14:22 +00:00
n ac787e5ebb Merge PR #7630: fix(projects): tag scan-minted system projects to the scanned profile 2026-09-20 03:14:22 +00:00