Stage 387 / v0.51.94 — Release BR — 10-PR full sweep batch#2606
Merged
Conversation
Adds a "Download Folder" item to the workspace file-tree right-click menu and a GET /api/folder/download endpoint that streams the directory as a zip with Content-Disposition: attachment. Configurable caps: HERMES_WEBUI_FOLDER_ZIP_MAX_MB (default 1024) HERMES_WEBUI_FOLDER_ZIP_MAX_FILES (default 50000) Pre-flights the walk so cap-exceeded returns 413 + JSON BEFORE any zip bytes are sent. Symlinks resolving outside the workspace are skipped. Mirrors the existing _handle_file_raw shape (session_id resolution, safe_resolve, RFC 5987 filename via _content_disposition_value). Stdlib zipfile only; no new dependencies. Tests: 11 static-inspection tests matching the style of tests/test_issue1867_upload_size_preflight.py. All passing on Python 3.11/3.12/3.13.
CI parity tests enforce that every key in the English locale block exists in zh, ja, ko, ru, and es. The PR introducing download_folder added it to en only, which broke the 5 hard-parity test files. Adds the English fallback to all 10 non-en blocks (it/ja/ru/es/de/zh/zh-Hant/pt/ko/fr) with the project's // TODO: translate marker so translators can refine later. Tests: tests/test_chinese_locale.py, test_japanese_locale.py, test_korean_locale.py, test_russian_locale.py, test_spanish_locale.py — 26/26 passing locally.
Per reviewer note: because the zip streams straight into handler.wfile (no io.BytesIO buffering), peak memory is bounded by zipfile's per-file read buffer, not the HERMES_WEBUI_FOLDER_ZIP_MAX_MB cap. Adds a comment so the next reader doesn't have to trace it to learn the cap's actual shape.
…pending is empty During active streaming, dangerous-command approvals go through the gateway path and are stored in _gateway_queues as _ApprovalEntry objects, not in _pending. The _resolve_approval_legacy helper only looked at _pending, so 'Allow for this session' never called approve_session() — the user clicked Allow, the card vanished, but the next dangerous command asked again. Now when _pending has no matching entry, the helper peeks into _gateway_queues to extract pattern_keys, calls approve_session(), and marks found_target=True so resolve_gateway_approval also fires. This commit is re-scoped to peek-only (no agent_session_key round-trip, no state_db metadata changes). Includes: - Import + fallback for _gateway_queues - Null-safe key filtering in all_keys - Source-contract test (static) + functional test with @requires_agent_modules skip marker for CI - All comments and docstrings in English
Drop the redundant 'if gw_data else []' guard — gw_data is already
guaranteed to be a dict by the 'or {}' fallback above.
Add a one-line comment explaining the peek-without-pop race window:
a concurrent resolver may pop a different gateway entry, but
approve_session is idempotent over the session key set so the
outcome is the same regardless.
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
# Conflicts: # api/routes.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage 387 / v0.51.94 — Release BR — 10-PR full sweep batch
Ten contributor PRs across streaming/state.db reconciliation followups, runtime-adapter Slice 4b, folder-zip download, partial-marker dedup, browser timeouts, composer-draft and auto-compression polish, approval-queue persistence, and indexed-context metadata.
PRs in this batch
Fixed
GET /api/folder/downloadstreaming-zip endpoint with pre-flight 413,os.walk(followlinks=False)+ per-symlink workspace-root containment check,allowZip64=True, configurable caps (HERMES_WEBUI_FOLDER_ZIP_MAX_MB=1024,HERMES_WEBUI_FOLDER_ZIP_MAX_FILES=50000). "Download Folder" item in workspace file context menu.download_folderi18n key added across all 11 locales._partialmessages (3x session bloat) #2592) — Deduplicate cancelled/recovered partial assistant markers using full(content, reasoning, partial tool calls)payload instead of just text content. Tool-only failed turns no longer append identical empty-content_partialmessages repeatedly. New_partial_message_signature()/_partial_marker_already_present()helpers scope dedup search to the current user turn only.api()helper gets a 30s default client-side timeout withAbortControllercancellation, per-calltimeoutMsoverrides, body-read race against the same timeout, and explicit 60s/120s ceilings for legitimately longer update flows. Closes the agent followup issue I filed last week against PR fix: guard new conversation cold-start clicks #2528.opts.preserveActiveInputto_restoreComposerDraft, gates overwrite oncurrent && current !== text. Backends.save(touch_updated_at=False)for/api/session/draft. Supersedes parallel-discovery PR fix(webui): preserve composer during external refresh #2602.compressedSSE listener's strict active-session guard now allows the post-rotation case (wherestateevent already flippedS.session.session_idto continuation id) while still rejecting events for completely unrelated sessions./api/session?messages=0count usinglen(merge_session_messages_append_only(...))instead ofmax(sidecar_count, state_count). Closes the agent followup issue I filed last sweep against PR Recovery slice 1 from the closed reconciliation pr #2581. Sidebar refresh no longer loops when state.db retains old rows that the merge filters._metadata_message_countfrom session JSON when no session-index entry exists and the merged count is zero, so external-refresh signal still trips on legacy sessions. Composes cleanly with fix: reconcile session metadata counts #2604 — the legacy fallback only fires when the merged count is 0._gateway_queues[sid]when_pendingis empty so "Allow for this session" persists during active streaming. Scope reduced from prior version to peek-only per maintainer feedback; the dead-on-streaming-pathagent_session_keyround-trip plumbing was dropped.Added
RunnerRuntimeAdapterfacade — a protocol-translator client over a future runner/sidecar backend. Facade delegatesstart_run/observe_run/get_run/control calls to an injected runner client, normalizes results into existing dataclasses, carries explicitprofile/workspace/modelpayload fields, returns boundedunsupportedcontrol results. No route wiring, no default-on runner mode, no public response-shape change.Verification
Pre-Opus 7-point gate — green
ast.parseover allapi/*.py+tests/*.py— cleanTests
6051 passed, 6 skipped, 3 xpassed, 8 subtests passed in 139.11s— +33 tests vs v0.51.93Opus advisor — GO (no inline followups)
Verified each of five self-verify concerns with file:line citations:
_summary_message_count = len(_all_msgs)falls back to_metadata_message_countonly when merged count is 0 (legacy session). Both tests pass against the merged logic. ✅_cancel_marker_idxto most recentrole=='user', scans onlymessages[start:end]. Multi-turn duplicate retry pattern stays safe (turn boundaries anchor the search). ✅eventMatchesCurrentrequirescurrentSidto match one ofeventSid/new_session_id/continuation_session_id. A→B switch scenario: B doesn't match any of A's identifiers → event correctly rejected. ✅grep -rn RunnerRuntimeAdapter api/returns only the definition). Facade-only as advertised. ✅check_authbeforehandle_get. CSRF intentionally GET-exempt (safe-method semantics, same as/api/file/raw). NoAccess-Control-Allow-Origin, so cross-origin JS can't read response.Content-Disposition: attachmentblocks inline.safe_resolve+ per-file symlink resolution check. Exposure shape byte-for-byte identical to existing/api/file/rawbaseline. ✅Single follow-up to track (NOT a blocker for v0.51.94): bjb2's promised PR to add defense-in-depth (cookie-bound token or non-cookie auth) for both
/api/file/rawand/api/folder/download.Triage actions taken this sweep (not in the batch)
tool_progress_callbackANDtool_start_callbackwired (the default in modern agent builds),STREAM_LIVE_TOOL_CALLSwill hold two entries per tool call and the live-card DOM will render duplicate cards. Asked contributor to gate the legacy path or unify the event source before this can ship._restoreComposerDraftAPI shape is cleaner). fix(webui): preserve composer during external refresh #2602's mobile-typing diagnosis referenced in the close note for future-revival.// TODO: translatefallback pattern.Triage carryover unchanged
Notes
api/routes.pyconflict between fix: reconcile session metadata counts #2604 (fixed metadata count via merge) and fix(webui): refresh active session on external sidecar updates #2605 (legacy-session sidecar fallback). Resolved by composing them: use the merged count first, only fall back to_metadata_message_countwhen that's 0. Both PRs' regression tests still pass.