Commit Graph
2792 Commits
Author SHA1 Message Date
carlotestor 4e17333516 fix: fingerprint .env key names with the provider .env loader 2026-09-23 15:52:14 +00:00
carlotestor 7f2e812280 fix: fingerprint .env key names and provider plugins; drop a profile's models cache on delete/create
A retained per-profile snapshot is now rejected after a switch when the target
profile's .env credential set or model-provider plugins changed, and a
recreated same-name profile no longer inherits the deleted profile's catalog.
2026-09-23 15:31:44 +00:00
carlotestor 78bb366560 fix: keep the target profile's models disk cache on the first /api/models after a per-client switch
switch_profile(process_wide=False) skips reload_config(), so _cfg_mtime still
held the previous profile's nonzero mtime. The first /api/models for the new
profile took the config-reload branch and _refresh_config_cache() treated
_old_cfg_mtime != 0.0 as a config edit, unlinking the *target* profile's
models_cache.<name>.json - defeating delete_disk=False on the very next request.

Only delete the disk snapshot when the same config.yaml path was already loaded
(_old_cfg_path == config_path); a path change (profile switch) keeps it. Executed
two-profile end-to-end test (real homes, configs, cache files) plus a guard that
a same-profile config edit still deletes the snapshot. Doc table updated.
2026-09-20 11:19:41 +00:00
carlotestor 7bb12d0aab fix: keep per-profile models disk cache across /api/profile/switch
POST /api/profile/switch called invalidate_models_cache(), whose
test-isolation contract also unlinks models_cache.<profile>.json. On a
switch nothing about the sources changed, so the delete only forced a
full cold rebuild (live provider fetch_models HTTPS calls, ~490k deepcopy
calls) on the very next /api/models — several seconds on every switch.

The disk cache is already keyed per profile and rejected on read when
_models_cache_source_fingerprint() differs, so it is safe to keep. Add a
delete_disk keyword (default True — every existing caller keeps the old
behaviour) and have the switch route pass delete_disk=False.

Regression tests cover: memory-only drop preserves the disk file, default
call still unlinks it, the next get_available_models() serves the disk
snapshot without a live rebuild, and the switch route opts out. Two
existing zero-arg stubs of invalidate_models_cache accept **kwargs now.
2026-09-20 11:19:41 +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
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
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
happy5318 3163a91d1a # Cron edit/duplicate must round-trip a parseable schedule; bad input returns 400 (#7352)
## Thinking Path

The WebUI populates the editable schedule field from `schedule_display`,
which is the human-readable label `once at 2026-08-28 16:00` for one-shot
jobs. `saveCronForm()` then submits that exact string back to
`POST /api/crons/update`. The Agent's `cron.jobs.parse_schedule()`
rejects the display form with `ValueError` (it only accepts the canonical
ISO timestamp). The route's earlier `try/except ValueError` block
covered input normalization but not the `update_job()` call, so the
parser exception escaped as HTTP 500.

There are two independent failures to fix:

1. **Frontend** ships the unsafe display label as editable input. Both
   `openCronEdit()` (`static/panels.js:1593`) and `duplicateCurrentCron()`
   (`static/panels.js:1532`) use the same `schedule_display ||
   schedule.expression` fallback. The cleanest fix is a shared helper
   that returns the canonical Agent-parseable value by schedule kind,
   matching the Agent's own schema:
   - `once` → `schedule.run_at`
   - recurring cron → `schedule.expr` (current schema; `expression`
     accepted for legacy payloads)
   - interval / legacy → the stored value, with `schedule_display`
     kept as a final fallback only when it isn't the `once at …` label.
2. **Backend** converts `ValueError` from `update_job()` into HTTP 400
   with the parser message, so the WebUI can show a real validation
   toast instead of an opaque Internal Server Error. The 404 path
   (job not found) is preserved.

## What Changed

- **`static/panels.js`** — add `_cronScheduleForEdit(job)` helper and
  use it in both `openCronEdit()` and `duplicateCurrentCron()`. The
  helper returns `schedule.run_at` for one-shot jobs,
  `schedule.expr` (or legacy `schedule.expression`) for recurring
  cron, and falls back to `schedule_display` only when it is not the
  `once at …` presentation form. `saveCronForm()` is unchanged — it
  still submits `schEl.value`, which is now the canonical field
  instead of the display label.
- **`api/routes.py::_handle_cron_update`** — wrap the `update_job()`
  call in `try/except ValueError` and return HTTP 400 with the parser
  message. The earlier normalization try block stays untouched; the
  404 check (`if not job`) sits outside the new try block so a missing
  job still returns 404.
- **`tests/test_cron_edit_round_trip.py`** — new regression module
  with seven tests covering the helper contract (helper exists, both
  call sites use it, `saveCronForm` still submits the field by name),
  the route's 400-on-`ValueError` behavior, the preserved 404 path,
  and the happy-path 200.

## Why

- Editing a one-shot cron job's scheduled time has been returning
  HTTP 500 since the WebUI's initial public release (per #7352
  historical note), because the editable field round-trips the
  `once at …` display label straight to the Agent parser.
- The backend regression is independent: even with a correct
  frontend, any user-typed invalid schedule text (or a programmatic
  caller) used to produce a 500 instead of a validation response.
  Both halves are required to deliver a "no more 500" user
  experience.

## Verification

```
$ .venv/bin/pytest \
    tests/test_cron_edit_round_trip.py \
    tests/test_cron_no_agent_edit.py \
    tests/test_cron_model_override.py \
    tests/test_cron_model_provider_picker.py \
    tests/test_issue617_cron_profile_selector.py \
    -v --no-header
…
============================== 27 passed in 4.84s ==============================
```

Seven new tests cover both layers; twenty surrounding cron tests
(no-agent edit, model override, model/provider picker, profile
selector) stay green.

## Contract

| Scenario | Before | After |
| --- | --- | --- |
| Open one-shot job for edit, change time, save | 500 (display label round-trips to parser) | 200, persisted with new `run_at` |
| Open recurring cron job for edit | 200 (legacy `schedule.expression` chain) | 200, unchanged canonical field (`schedule.expr` first, `expression` as legacy fallback) |
| Duplicate a one-shot job | 500 on first save | 200, populated with `schedule.run_at` from the source job |
| Submit a `once at …` schedule value (programmatic / pre-fix frontend) | 500 | 400 with the parser message |
| Submit any other unparseable schedule | 500 | 400 with the parser message |
| Update a job that does not exist | 404 | 404 (preserved) |
| Update a job with a valid canonical schedule | 200 | 200 (preserved) |

## Risks

- **Frontend helper change**: I switched the editable field source
  for both edit and duplicate in one commit. `saveCronForm()` is
  untouched, so anything already rendered with the legacy chain
  (rare in practice — the helper is only called from these two
  functions) keeps working. The regression `test_save_cron_form_still_submits_schedule_value`
  pins the submit field name.
- **New `try/except ValueError` in `_handle_cron_update`**: the
  earlier normalization try block (lines 24986–24998) is left
  exactly as-is. The new try block sits directly around
  `update_job()` only, so a `ValueError` from input parsing still
  surfaces before any persistence side effect. The 404 check is
  outside the new try, so a missing-job path cannot be misreported
  as 400. Both `test_cron_update_preserves_404_when_job_missing` and
  `test_cron_update_valid_schedule_still_succeeds` pin those
  contracts.
- **Legacy `schedule.expression` payloads**: accepted as a
  back-compat fallback in `_cronScheduleForEdit` and in the route
  (the Agent schema is `expr`, but the parser still handles
  `expression` per the existing contract). If you prefer to drop
  the legacy fallback, the helper can be trimmed to `sched.expr`
  only.

## Model Used

- Generated with `MiniMax-M3` via `happy5318/hermes-webui` worktree
  `/tmp/webui-worktrees/pr7352-cron-edit`, branch
  `fix/cron-edit-round-trip` (off `origin/master`).

## Related

- Closes #7352
- Surface code:
  - `static/panels.js:1510` `duplicateCurrentCron` (uses helper)
  - `static/panels.js:1585` `openCronEdit` (uses helper)
  - `static/panels.js:1905` `saveCronForm` (submit path, unchanged)
  - `api/routes.py:24979` `_handle_cron_update` (new try/except)
  - `cron/jobs.py:785` `parse_schedule` (rejects the display form;
    Agent-side, not modified in this PR)
  - `cron/jobs.py:2199` `update_job` (the call that raises)
- Test surface: new `tests/test_cron_edit_round_trip.py`
2026-09-20 03:05:14 +00:00
n 3da9a0975c Merge PR #7641: fix(redaction): validate MPF JPEG attachments before the native-image exemption 2026-09-20 02:25:35 +00:00
n 9dbd647f40 Merge PR #7643: fix(logging): preserve access logs during agent stdout capture 2026-09-20 00:45:40 +00:00
n 5bf257aa9a Merge PR #7636: perf(sessions): call-scoped profile-home resolve cache 2026-09-20 00:39:42 +00:00
n e74aa7ec3a Merge PR #7276: perf(redaction): stop re-doing constant redaction work on every /api/session response 2026-09-20 00:02:06 +00:00
n eb7f5df284 Merge PR #7394: fix(tts): honor the persisted TTS engine when requests omit it 2026-09-20 00:02:05 +00:00
danpark 1a7b82d41e fix: preserve access logs during agent stdout capture (#2684)
Own a private stdout descriptor before agent imports and serialize request log writes. Keep logging failures isolated from HTTP responses. Add successful chat/start subprocess delivery regressions and document header-latency semantics and cron ownership.
2026-09-19 07:26:12 -04:00
George Byers II 34bc4b9cd0 fix: validate MPF JPEG attachments before redaction 2026-09-18 20:35:31 -04:00
hermesandClaude Sonnet 5 93814b9629 fix(webui): use the memoized workspace closure in the webhook chip pass
The webhook second-pass loop in _load_cli_sessions_uncached called
get_last_workspace(profile=_cli_profile) directly per row instead of the
_cli_workspace() closure the main/cron/kanban passes already use, so it
paid its own redundant file reads/is_dir() probe on every webhook row even
though the loop already sits inside the profile-home resolve cache scope.
Flagged in PR #7636 review (greptile): this is the one concrete gap in an
otherwise-correct fix.

Also adds an integration-level regression test proving the actual N+1 this
PR targets -- Session.__init__ constructed via
_state_projection_sidecar_metadata's Session.load_metadata_only() fallback
on a cold sidecar-metadata cache -- is deduped by
@profile_home_resolve_cache_scope() on _load_cli_sessions_uncached, with a
companion test showing the same fixture scales linearly (one filesystem
resolve per row) when the scope is bypassed. This directly answers the
review claim that the cache scope doesn't reach any Session construction:
it does, verified by counting real _safe_resolve() filesystem calls rather
than call counts on the memoizing wrapper alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 00:43:06 +01:00
hermesandClaude Sonnet 5 8a335d5af2 fix(webui): scope profile-home resolve cache to one session-list build
The process-lifetime cache from the previous commit was flagged in PR
review (nesquena-hermes): Path.resolve() is a filesystem call, not a
pure function of process-startup constants. A profile-home symlink can
be retargeted while the long-running webui process stays up, and
_safe_resolve() deliberately falls back to the unresolved input after a
transient OSError/RuntimeError/ValueError -- a process-lifetime cache
would serve either a stale symlink target or a stale fallback forever.

Redesign: replace the module-level dict with a contextvars.ContextVar
that defaults to None (no active scope -> resolve fresh, exactly like
pre-PR behavior). profile_home_resolve_cache_scope() sets it to a fresh
dict for the duration of one call and resets it in a finally block, so
no cache entry survives past that call. It now wraps only
_load_cli_sessions_uncached as a decorator -- the actual N+1 hot loop
(hundreds of Session.__init__ calls per sidebar build, each resolving
the same profile argument). All other call sites of
_resolve_profile_home_param (confirmed via repo-wide grep: all internal
to api/workspace.py, none elsewhere) get fresh resolution on every call,
same as before this PR existed.

Rewrote tests/test_profile_home_resolve_caching.py to prove: (a)
repeated resolves within one scope still hit the cache once, (b) two
separate scopes (simulating two requests) do not share state -- a
symlink change between them is observed on the second scope, and (c)
calls outside any scope never cache at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 00:20:05 +01:00
hermesandClaude Sonnet 5 e781542bf4 Cache profile home resolution to fix N+1 slowdown in session list
_resolve_profile_home_param() called Path.resolve() on the profile's home
directory with no caching. The webui session list builds one Session per
CLI session row, and Session.__init__ calls this function once per row, so
with ~484 sessions on the same profile it redundantly re-resolved the
identical path hundreds of times per request. Individually cheap (~1ms),
but under host load this compounded into multi-second sidebar hangs,
confirmed via journalctl showing repeated 'Slow WebUI request still
running' warnings (5-17s) pinned to this exact call site.

The mapping from a profile argument to its home path is a pure function of
process-startup constants (_DEFAULT_HERMES_HOME / _INITIAL_HERMES_HOME)
that are resolved once at import time and never mutated afterwards, so
memoizing per profile argument for the life of the process is safe. Profile
creation only creates the directory at the already-deterministic path; it
does not change what path a profile argument resolves to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 16:36:46 +01:00
carlotestor 46e60616f6 fix(projects): tag scan-minted system projects to the scanned profile
The all-profiles sidebar scan walks every profile's state.db in one
request. When it lazily minted a "Webhooks" / "Cron Jobs" project for a
profile that had webhook/cron rows, ensure_webhook_project() and
ensure_cron_project() tagged the new entry with get_active_profile_name()
-- the UI-selected profile -- instead of the profile whose rows triggered
it. A webhook-only profile therefore leaked a "Webhooks" project onto
whichever profile was selected; deleting it did nothing because the next
sidebar poll re-minted it, and switching profiles minted one per profile.

ensure_webhook_project / ensure_cron_project / _profile_has_user_projects
now take an explicit profile (default: active profile), and the scan
passes _cli_profile through, so the opt-in gate and the tag both key on
the scanned profile. Existing stubs in tests updated to accept the kwargs.
2026-09-18 09:13:09 +00:00
Anthony Ruiz fc7ae2ddd1 fix(redaction): fail closed on large strings, byte-bound the prefilter cache
Gate found two blocking defects in the new caches:

1. The large-string LRU was keyed on input text only. The installed
   agent.redact registry can extend the secret matcher at runtime via
   register_redaction_patterns() and exposes no policy generation or
   invalidation hook, so a >16k string primed before a registration was
   served with the new secret intact (reproduced against the installed
   registry). Drop the large-string cache: strings above 16,384 chars run
   the redactor uncached again. Regression primes a large string, registers
   a runtime pattern on the real registry and proves the next
   redact_session_data() response is redacted.

2. The prefilter LRU was count-bounded (8192) but its per-entry gate was a
   character cap, which is not a byte bound: 16k four-byte code points is
   ~64 KiB per key, ~512 MiB at capacity, and the workload died with
   MemoryError under 96 MiB of headroom. Gate each entry on
   sys.getsizeof(text) <= 2048 so the retained-key ceiling is a hard
   16 MiB. Regressions cover max-width Unicode, sensitive verdict pairs,
   eviction past the cap, and a low-memory RLIMIT_AS subprocess.
2026-09-17 23:35:54 +00:00
Anthony Ruiz 86fe1a5746 perf(webui): make redaction containers copy-on-write 2026-09-17 23:21:46 +00:00
Hermes Agent b7f30c6365 perf(webui): memoize the sensitive-text prefilter
Once large strings stopped being re-redacted on every response, cProfile
showed the cheap prefilter had become the dominant cost of the redaction
pass: 2.98s of 3.7s, spent testing 71 literal markers against each of the
~74k strings (~19MB) of a large session, i.e. ~3.1M interpreted iterations
per request — repeated by every tab and every poll for a constant verdict.

Memoize the verdict behind a bounded LRU, mirroring the existing redaction
caches: entry count and per-entry size are both capped, so retained bytes
stay bounded and oversized blobs simply run the scan uncached. Non-strings
never reach the cache, so an unhashable value cannot raise.

Measured on a real 22MB session, warm: full redaction 2.07s -> 1.07s (-49%),
output byte-identical. Combined with the large-string cache, the recurring
redaction cost drops from ~8.4s to ~1.1s.

Rejected alternative, kept in a comment: collapsing the 71 `in` scans into
one compiled alternation is the obvious-looking fix but measured 38% SLOWER
(2.60s vs 1.88s) — CPython's substring search is already a tuned C routine.

Security: the prefilter gates the entire redaction pass, so equivalence is
pinned by a differential test suite against a copy of the previous
implementation — every marker individually, case variants, embeddings,
near-misses, unicode lowercase traps, 4000 fuzz cases, plus oversized and
unhashable inputs. Verdicts are identical on all 73,658 strings of a real
session (1935 flagged before and after).

Broad selection: 22 failures before and after, identical set — all
pre-existing.
2026-09-17 23:21:46 +00:00
Hermes Agent 6f05ff9657 perf(webui): stop re-redacting large strings on every response
`_redact_fn_cached` skipped memoization for any string over 16KB, so those
strings re-ran the full ~15-pass redactor on every single request. On a real
22MB session that is 59 large strings (29 unique, 0.76MB) costing 1.68s per
request — 99.9% of the recurring redaction cost once the small-string cache is
warm — repaid by every tab and every poll for a byte-identical result.

Large strings now get their own bounded cache, separate from the 4096-entry
small-string one so a large blob can never evict the thousands of small
recurring strings. Entry count and per-entry size are both capped; anything
above the ceiling stays uncached, keeping retained bytes bounded.

`_redact_value` additionally stops rebuilding containers whose descendants
were unchanged. Most of a transcript carries no credential marker, so the
unconditional dict/list comprehension deep-copied the whole payload to
reproduce an identical structure — pure CPU under the GIL, which serializes
concurrent tab loads on a threaded server. Callers treat redacted output as
read-only (`_public_message_projection` and `redact_session_data` both build
fresh containers), so sharing an unmodified subtree cannot leak a later
mutation back into session state. As soon as one string is masked, every
container on the path to it is rebuilt and the caller's original is untouched.

Measured on the live 22.6MB session, steady state: redact_session_data 3.78s
-> 1.96s single tab (-48%), 7.93s -> 4.06s with two concurrent tabs (-49%),
byte-identical output.
2026-09-17 23:21:46 +00:00
nesquena-hermesandmikemchargue 818c392ed4 fix: resolve Hermes Agent names moved by the Sep 2026 decomposition (#7574)
The Agent's September 2026 decomposition moved several names the WebUI uses into
sibling modules. The old import paths resolved only through PEP 562 __getattr__
compat pointers whose removal date (2026-09-14) has already passed. Every affected
WebUI call site is wrapped in a broad except, so once those pointers go away nothing
crashes -- features silently degrade: MCP tools vanish from turns, the per-turn
approval session key never binds, Claude Code credential linking stops, and the
kanban bridge raises AttributeError.

Adds api/agent_compat.py::agent_attr(owner, name, home, default), which resolves a
moved name via the original module's own namespace (pre-split Agents and test stubs),
then the new home module (split Agents, with or without the pointers), then plain
attribute access -- and routes the 8 affected call sites through it.

Gate: Codex SAFE TO SHIP. All 12 mappings verified against the installed Agent's
authoritative compat_manifest.json; owner-wins precedence is correct because PEP 562
pointers do not populate vars(owner), so current pointers are bypassed without
warnings; the only explicit defaults are the pre-existing optional kanban fallbacks
and remain semantically equivalent; turn-approval identity, MCP discovery/reload/status,
OAuth credential reading, LM Studio probing, and all three kanban paths preserve their
previous values and control flow; cached import_module costs ~1.36us and the hottest
site is a 0.3s kanban poll, not per-token streaming (caching the callable would break
supported module replacement/monkeypatching). Tests are non-vacuous across pre-split,
pointer-present, and pointer-removed shapes. Full suite 15,597 pass with only the known
box-baseline singleton. Maintainer proof: agent_attr resolves the moved name with zero
HermesPluginCompatWarning while a clean-master control still resolves via the pointer
and warns.

Co-authored-by: mikemchargue <mikemchargue@users.noreply.github.com>
2026-09-17 21:57:25 +00:00
Mike McHargueandClaude Opus 5 1f7e5e78da fix: resolve Hermes Agent names moved by the Sep 2026 decomposition
Hermes Agent moved several names the WebUI uses into sibling modules
(tools.approval_context, tools.mcp_tool_discovery/_lifecycle,
hermes_cli.kanban_db_connect/_dispatch, hermes_cli.models_local,
agent.anthropic_credentials). The old paths only resolved through
PLUGIN-COMPAT __getattr__ pointers that warn on every use and are removed
on schedule by reverting one commit.

With the pointers removed, every affected WebUI call site is wrapped in a
broad except, so nothing crashes; features silently degrade instead:
per-turn approval session-key binding is skipped, MCP discovery/status
return nothing, /reload-mcp reports "MCP runtime unavailable", Claude Code
credential linking returns None, every kanban connection raises
AttributeError, and the dispatcher reports "unavailable".

Add api.agent_compat.agent_attr, which resolves a moved name from the
original module's own namespace first (pre-split Agents and existing test
stubs), then from its new home (split Agents, with or without pointers),
then plain attribute access (non-module doubles). Importing only the new
modules would have broken pre-split Agents and bypassed the sys.modules
stubs the test suite installs on the original modules.

Names still native to their original module (_servers, _lock,
has_blocking_approval, is_session_yolo_enabled, register/unregister
gateway notify, the rest of kanban_db) are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:26:50 +00:00
carlotestor 8c5ee95dab fix(media): serve .zip archives through /api/media
MEDIA:/path/to/file.zip links always failed to open in chat. .zip was
absent from MIME_MAP, so it resolved to application/octet-stream, which
_SESSION_MEDIA_TOKEN_TYPES rejects — the browser rendered a download
link that the server then refused.

Map .zip to application/zip and add an _ARCHIVE_TYPES set to the session
MEDIA: token allow-list. Archives are deliberately kept out of
_INLINE_PREVIEW_TYPES and _INLINE_IMAGE_TYPES so they are always sent as
Content-Disposition: attachment and never rendered inline.
2026-09-17 20:56:30 +00:00
Matt Van Horn a375aa9356 fix: honor the persisted TTS engine when requests omit it
Fixes #7391
2026-09-16 09:04:24 -07:00
Skawsk59andSkawsk59 723146cbfe fix: serve SPA for /sessions route (404 after login with auth enabled) (#7598)
Serve the SPA shell for /sessions so the post-login redirect (when password auth is enabled) no longer 404s.

Co-authored-by: Skawsk59 <Skawsk59@users.noreply.github.com>
2026-09-16 00:24:09 -07:00
leosiedlerandLeonardoLGDS 81f18a1f72 fix(agent_runtime): fail closed when Git identity disappears (#7407)
Fail closed when the Agent Git identity disappears or is unreadable, including when the revision read itself raises — a missing revision is indistinguishable from changed source and must never be treated as proof the loaded code is current.

Co-authored-by: LeonardoLGDS <LeonardoLGDS@users.noreply.github.com>
2026-09-15 22:14:09 -07:00
Rodrigo Gomes da Silvaandrodrigogs 66423ad2dc fix(sessions): stop one oversized metadata field from re-parsing the whole sidecar (#7573)
Read the metadata prefix in staged geometric reads so one oversized metadata field no longer forces a full re-parse of the whole sidecar.

Co-authored-by: rodrigogs <rodrigogs@users.noreply.github.com>
2026-09-15 21:25:27 -07:00
b3nw fb2e8b0088 fix(custom-providers): route @custom:<slug> models to their own base_url (#7319)
Route @custom:<slug> models to their owning custom provider's base_url so a model picked from a second custom provider group resolves correctly instead of 400ing.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-09-15 20:05:40 -07:00
totalitarianandtotalitarian 963e1bae8a Decode state.db content sentinel in the WebUI message projection (#7492)
Decode the state.db content sentinel in the WebUI message projection so structured/multimodal content renders instead of a placeholder, with an out-of-band tuple identity that no scalar can forge and a per-call ContextVar memo for the canonical serialisation.

Co-authored-by: totalitarian <totalitarian@users.noreply.github.com>
2026-09-15 17:12:45 -07:00
webtecnicaandwebtecnica 68008e824b fix: classify Signal gateway sessions as messaging (#7572)
Classify Signal gateway sessions as messaging so they group, label, and list like other gateway platforms.

Co-authored-by: webtecnica <webtecnica@gmail.com>
2026-09-15 16:16:57 -07:00
alfred-rootson b71c88f292 fix(workspace): preserve remote POSIX paths across Session & streaming callsites (#7131) (#7168)
Closes the remote POSIX workspace path preservation gap across Session and streaming. Profile-aware runtime path resolution.

Co-authored-by: alfred-rootson <alfred.wojtasordzik@gmail.com>
2026-09-15 15:37:11 -07:00
94fd2da830 Release: semantic fingerprint for the Codex catalog dependency (#7556) (#7558)
* fix(config): semantic fingerprint for the Codex catalog dependency (#7540)

* fix(#7556): guard the recursive codex-cache strip against RecursionError

Codex found a valid-but-deeply-nested models_cache.json crashes
_codex_models_cache_fingerprint with RecursionError (the recursive strip ran
outside the fallback try), 500ing /api/models. Move _strip_volatile_codex_cache_fields
inside the existing encode try/except so a transform failure degrades to the
stat-based fingerprint. +1 mutation-proven regression (forces the strip to raise;
fails with RecursionError if the guard is reverted).

* docs(changelog): stamp #7556 (semantic fingerprint for Codex catalog dependency)

---------

Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-13 17:39:39 +00:00
e2ce040f1f Release: actionable ids for raw id-less pending approvals (#7510) (#7551)
* fix(approvals): mint actionable ids for raw id-less local pending entries

A raw id-less approval entry in _pending (agent-side _pending_result drops
{command, pattern_key, pattern_keys, description} verbatim when no gateway
notifier is registered for the session) is unactionable by contract: the
frontend owner-capture requires an approval_id, so the approval card can
neither be approved nor dismissed - the 1.5s poll re-serves it forever.

reconcile_gateway_pending_mirror_locked already mints gwlocal:<token> ids
for id-less _gateway_queues producers; extend the same contract to raw
non-mirror _pending entries with a stable gwlocal-mirrorless:<uuid> id
minted once onto the stored entry dict, so the id persists across polls
and frontend dismiss markers stick.

The exact-id respond path needs no further change: once the entry carries
an id, _resolve_approval_legacy pops it (also clearing the stale
stream-pointer 409 guard), stale explicit ids still fail closed (#527),
and the yolo Skip-all release drains it.

* docs(changelog): stamp #7510 (actionable ids for raw id-less pending approvals)

---------

Co-authored-by: CharlesMcquade <6466275+CharlesMcquade@users.noreply.github.com>
Co-authored-by: n <a@n>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-09-13 03:07:49 +00:00
e4d9b799e4 Release: manual title regeneration for queued-user transcripts (#7545) (#7547)
* fix(api): scan to first complete exchange for title regeneration (#7543)

_first_exchange_snippets() aborted at a second consecutive user message
before collecting assistant text, so transcripts opening with queued user
rows (channel-backed imports) hit the missing_exchange rejection and the
deterministic fallback got persisted with HTTP 200 — invisible in the UI
and unrecoverable by retrying.

Keep scanning until the first complete user+assistant pair instead, and
fall back to the last complete exchange when the opening user message
yields no user text (privacy scrubbing) instead of failing with
empty_user_message.

Tests: tests/test_issue7543_title_first_exchange.py (8 cases, incl. the
issue-pinned transcript shape and walker-mocked discriminator tests for
both fixes).

* test(api): drop local-machine notes and sys.path hack from 7543 test module

- Docstring described the change as a local-only uncommitted hotfix with
  machine-specific paths — wrong for an upstream PR and unsafe to publish.
- Drop the defensive REPO_ROOT sys.path insert; the repo conftest already
  puts the package root on sys.path (matches the neighboring test modules).

* fix(api): drop unreachable latest-exchange fallback from title regeneration

Greptile flagged the mocked-walker fallback test (P2) as fabricating
internal state, and an exhaustive enumeration of realistic transcript
shapes (5460 combinations, lengths 1-6) confirms it: both walkers share
the same text extraction and the forward walker scans every message, so
'first walker empty while latest walker yields a pair' is unreachable
from real transcripts. The fallback branch was dead code.

Remove it together with its two walker-mocked tests; the fixed first-
exchange walker alone resolves the reported issue shape. Replace the
mocked tests with an observable E2E error-path test through the real
walkers, and make the aux mock guard-faithful (empty assistant_text ->
missing_exchange per the generate_title_raw_via_aux contract) so the
fail-before proof actually exercises the endpoint contract.

* fix(#7543): scope first-exchange scan-past to the manual-regen path only

The full suite caught that broadening the SHARED _first_exchange_snippets also
activated the automatic in-stream background-title path for consecutive-user-row
transcripts, moving stream teardown from the synchronous stream_end branch to the
background-title-thread branch (test_issue3929 emits_done regressed: stream_end
was no longer in the synchronous queue). #7543 is specifically about the MANUAL
'Regenerate title' endpoint, so gate the scan-past behavior behind an opt-in
scan_past_consecutive_users flag (default False = master behavior) and pass True
only from generate_session_title_for_session. Auto in-stream path is now
byte-identical to master; added a default-behavior regression guard test.

* docs(changelog): stamp #7545 (manual title regen for queued-user transcripts)

---------

Co-authored-by: Raylands <Raylands@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-12 21:12:52 +00:00
b1286878a4 Release: models_discovered/discover_models live-catalog fix (#7409) (#7538)
* Apply reviewed change

* fix(models): honor discover_models opt-out + preserve discovered catalog on live-probe failure

Codex gate found two CORE edge cases in the #7404 fix:
1. discover_models:false must override models_discovered:true (explicit opt-out
   pins the configured allowlist; honor Agent string forms false/no/0).
2. a transient empty live-catalog probe dropped a discovered provider's models
   (cached empty ~24h); now falls back to configured discovered IDs merged with
   any static catalog (deduped, ordered first).

Mirrors the Hermes Agent canonical semantics (model_switch_providers._discover_flag,
model_switch._models_config_is_allowlist). Adds 3 mutation-proven regressions.

* docs(changelog): stamp #7409 (models_discovered/discover_models live catalog)

---------

Co-authored-by: evgenyponomarev <712802+evgenyponomarev@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-12 05:32:16 +00:00
n c196306fcb Stage: PR #7337 — chat-attachment upload anchored O_EXCL (TOCTOU) by @zicochaos 2026-09-12 01:26:51 +00:00
Sebastian B f7523f63a2 fix(api): create chat-attachment uploads with anchored O_EXCL (TOCTOU)
handle_upload deduped filenames with an exists() check and then wrote with
plain write_bytes — two concurrent uploads of the same name both passed the
check and the last writer silently won. Mirror the #3398 workspace-upload
hardening: O_CREAT|O_EXCL|O_NOFOLLOW anchored open from the session
attachment dir; a raced duplicate now 409s instead of overwriting and a
raced symlink cannot redirect the write.
2026-09-12 00:55:45 +00:00
Hermes c076fe9e09 fix: detect MIME types for static binaries 2026-09-12 00:55:23 +00:00
nesquena-hermesandnesquena-hermes f79325498e fix(cron): restore messaging platforms in the delivery picker (#7525)
* fix(cron): restore messaging platforms in the delivery picker

The Agent moved _KNOWN_DELIVERY_PLATFORMS from cron.scheduler to
cron.scheduler_delivery. _handle_cron_delivery_options imported the old
path inside a bare except and fell back to an empty frozenset, so the
import error was swallowed and GET /api/crons/delivery-options silently
returned only local and origin — telegram, discord, slack, feishu and
every other messaging platform disappeared from the cron delivery picker
with no error surfaced anywhere.

Resolve the authority across both module paths (current first, legacy
fallback) so the picker keeps working across Agent versions, and add a
regression test that pins the resolution order and cross-checks the
endpoint output against the resolved authority.

* test(cron): skip the authority-resolution test where the Agent is absent

CI runs agent-free (tests/browser_smoke.py), so neither cron.scheduler_delivery
nor cron.scheduler is importable there and the authority resolves empty. The
new regression test asserted non-empty and therefore failed shard 3 on both
Python versions. Skip instead: with no cron package there is no authority to
resolve and nothing meaningful to assert. The test still fails loudly wherever
the Agent IS installed, which is where a future relocation would bite.

* test(cron): register the new test in _AGENT_DEPENDENT_TESTS

Use the project's existing skip mechanism (tests/conftest.py) rather than an
ad-hoc inline pytest.skip, matching the five sibling delivery-options tests.
CI runs agent-free, so there is no cron package and no authority to resolve
there; with the Agent installed the test still asserts loudly, which is the
environment where a future relocation would actually bite.

Verified both directions: 6/6 pass with the Agent present, and forcing
AGENT_MODULES_AVAILABLE=False skips all 6 (was 5) as CI will.

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-09-10 22:30:57 -07:00
webtecnica 4359ed4fee fix(onboarding): use OpenRouter namespace z-ai/ for Z.AI models (#7514) 2026-09-10 22:30:44 -03:00
rh-idandnesquena-hermes 40a6a7696a feat: add GLM-5.3 Flash to Z.AI model list (#7323)
* feat: add GLM-5.3 Flash to Z.AI model list

Add glm-5.3-flash next to glm-5.3 in _PROVIDER_MODELS["zai"] and mirror
it in _FALLBACK_MODELS, following the existing glm-4.5 / glm-4.5-flash
sibling pattern. Reasoning-effort gating needs no change: the >= 5.2
version gate already classifies the id at the full effort tier.

Adds a regression suite asserting catalog presence, label, ordering,
fallback entry, and end-to-end propagation through
get_available_models().

* test: pin glm-5.3-flash sibling adjacency in zai provider catalog

Per PR review feedback (#7323): the provider-catalog ordering test now
asserts glm-5.3-flash immediately after glm-5.3 and glm-5.2 immediately
after the flash sibling, matching the adjacency the fallback test
already pins. The base<flash relative check is subsumed; flash<glm-5.1
is kept to pin 5.1 after the whole 5.3 generation.

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-09-10 13:08:49 -07:00
zicochaosandnesquena-hermes a6b187fc84 refactor(routes): extract the 585-line GET /api/session handler to _handle_session_get (#7340)
* refactor(routes): extract the 585-line GET /api/session handler to _handle_session_get

Pure move out of handle_get's if/elif chain into a named module-level handler, matching the pattern the other ~200 routes already use. Body is verbatim (one-level dedent only); AST identity of the moved statements verified; no behavior change — the GET /api/session test slice passes identically before and after.

* test: repoint four source-window tests to _handle_session_get

The moved block's source-window extractors (chat_start synthesiser/read-only
pins, regressions runtime-streaming-state window, issue1436 load-path
markers, security-redaction inspect) now anchor on the extracted handler;
assertions unchanged.

* test: repoint messaging-session loader pin to _handle_session_get

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-09-10 12:49:14 -07:00
656575a400 Fix geometric duplication of recovered reasoning-only rows at turn settle (refs #7388, independent of #7416) (#7443)
* Fix geometric duplication of recovered reasoning-only rows at turn settle

_restore_display_reasoning_metadata() re-inserted every display-only
reasoning row at its API-safe position in the new result. After context
compaction that position is past the end of result_messages, so the row
was appended after the new reply; _message_identity() is None for an
empty assistant row, so the display merge could not dedup it. Each
settled turn therefore re-added every existing empty
_recovered_from_run_journal anchor once more (1,1,2,4,8,... blocks).

Only restore a reasoning-only row in front of its own API-safe
successor; a compacted result with no aligned slot is skipped.

Refs #7388, #7416 (independent: that PR fixes linear journal replay).

* tests: keep docstrings within the 5-line block budget

* Prefer stable ids when aligning a reasoning row's successor at restore

The successor-alignment guard compared the historical successor with the
aligned current row via _message_identity(), which ignores stable ids and
returns equal keys for repeated prompts ("continue"). A distinct current
turn with the same text false-matched the historical successor, every
recovered anchor before it was re-inserted, and 64 anchors became 128
after one restore->merge settle (nesquena-hermes gate on ed2076eda).

Alignment is now stable-id-first: when either row carries an id, both
must carry the same id; _message_identity() is the fallback only when
neither row has one. Adds a production-order regression through
_settle_result_messages() with 64 anchors, stable ids and repeated
"continue" prompts (RED on ed2076eda and origin/master: 128 != 64), plus
a unit test pinning all four id-presence branches.

* Fail closed on stable-id identity: positive-int only, unique, no content fallback

_restore_display_reasoning_metadata() compared ids with raw equality, so a
persisted historical successor with id=True (or 1.0) matched a distinct
current row minted as id=1 and restored the reasoning card before the
wrong turn. Ids are now accepted only when type(value) is int and > 0,
both rows must carry a valid equal id whenever either has an id field,
and the id must be the sole owner in both API-safe projections.
_assign_stable_message_ids() re-mints invalid ids so they cannot survive
into a collision with minted integers.

* tests: explicit zip strict= for the ruff B905 gate

* Resolve the active-turn boundary before restoration; never carry historical ids/rows into it

A historical assistant successor with the same content as the distinct
current assistant ("Done." / "Done.") let _restore_reasoning_metadata copy
the historical stable id onto the current row; the uniqueness guard then
accepted that forged identity and 64 recovered cards settled to 128.

_active_turn_boundary() is decided once in _prepare_marker_clean_writeback
before any restore (token > strict context prefix > last matching user row
> 0) and threaded through _settle_result_messages into both restores.
Rows at/after it receive no historical id/metadata and no historical
reasoning-only row. Assistant-only results are boundary zero.

* Keep the pinned _restore_reasoning_metadata signature; boundary goes through _restore_reasoning_metadata_before_boundary

tests/test_sprint49.py pins the two-arg signature as source text, so the
boundary-aware core lives in _restore_reasoning_metadata_before_boundary
and the original name stays a thin no-boundary wrapper.

* Content-only prefix is not turn ownership: fail closed to boundary 0; sync route passes result/Agent turn authority

- _active_turn_boundary: a previous-context prefix matched by _message_identity
  (ids/timestamps ignored) only narrows where the current user may be; the
  boundary is the prompt-matching user AT OR AFTER that prefix, else 0.
- _handle_chat_sync: resolve authority via _resolve_active_turn_authority
  (result, agent) instead of hard-coding None.
- Regressions: three-row current-only collision (boundary 0, one 64 block),
  exact-authority + append-only controls, ID-less-successor settle/save/load
  pinned at the persisted-display boundary, sync-route authority pass-through.

---------

Co-authored-by: carlotestor <carlotestor@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-09-10 12:29:33 -07:00
Arush Khasru b39bf1a113 Fix key_cmd credentials in agent cache signatures 2026-09-10 04:40:26 +00:00
394ab31eea release: #7351 bind turn workspace as agent cwd (#7504)
* rebase #7351 onto master (bind turn workspace as agent session cwd)

Co-authored-by: samfoy <samfoy@users.noreply.github.com>

* fix test: use deployed agent.runtime_cwd API (_SESSION_CWD.get) not nonexistent _session_cwd_override

The two failing assertions referenced agent.runtime_cwd._session_cwd_override(), a
helper not present in the installed Agent (which exposes _SESSION_CWD, resolve_agent_cwd,
set/clear_session_cwd). The production binder is correct and verified: binding workspace=/tmp
sets _SESSION_CWD.get()=='/tmp' and resolve_agent_cwd()=='/tmp', reset restores _UNSET.
Line 343 was redundant with the surrounding assertions; line 386 now reads _SESSION_CWD.get().

Co-authored-by: samfoy <samfoy@users.noreply.github.com>

* docs(changelog): stamp #7351 turn workspace cwd binding

---------

Co-authored-by: n <a@n>
Co-authored-by: samfoy <samfoy@users.noreply.github.com>
2026-09-10 03:40:45 +00:00