_apply_pins let any exception from _pin_artifacts propagate: one broken
pin - a package's resolution bug (gh's 3-part target split) or one
target's upstream pool skew (Termux rolling pool 404 while nodejs.org
is ahead) - aborted the entire run, blocked every unrelated package,
and left lockfile.save() unreached, so any failure meant zero progress.
Catch per package, pin everything that succeeds, install the pinned
set, print a 'pinned X, failed Y' summary, and exit non-zero when
anything failed. A run where every pin fails still leaves the lockfile
untouched.
Fixes#125386
mcp_field(None, ...) already yields None, so the extra None guard is dead.
Drop the #121042 KNOWN entry from the stdio conformance suite: on a
trust: untrusted server the readOnlyHint tool now runs without approval
against the real mcp 2.x fixture server.
Co-authored-by: pierrenode <298902573+pierrenode@users.noreply.github.com>
mcp 2.0 renamed ToolAnnotations.readOnlyHint to read_only_hint; camelCase
survives only as a pydantic serialization alias, which is not applied to
attribute access. _annotation_read_only_hint() read the camelCase attribute
off the SDK object, so on mcp 2.x the hint was always None and every tool --
including honestly annotated read-only ones -- fell into the
"unknown = write-capable" branch.
With trust: untrusted that means the approval gate fires before every read
call (get_*, search_*, list_*), which is exactly the failure mode that
trains a user to click through the prompt without reading it, weakening the
gate on the writes it exists for.
Use mcp_field() for the object path (the helper already used across the MCP
client for this 1.x -> 2.x rename) and accept snake_case in the cache dict
too, for caches written by another client. Unknown still means
write-capable; only the field lookup changes.
Regression test covers the SDK 2.x model, both snake_case shapes, the 1.x
camelCase attribute and a snake_case cache dict.
flatten_single_dir renames a tree extracted seconds earlier, while
Defender or another scanner still holds it, and ffmpeg/agent-browser
installs died on the package/bin rename with [WinError 5] (#131884).
The entry displace/restore renames in install.py had the same bare call.
pm.filesystem.retry_held retries EACCES/EPERM/EBUSY for about 3s and
raises anything else immediately; the hoist, merge_tree and the entry
swaps use it.
Co-authored-by: bighamx <10262931+bighamx@users.noreply.github.com>
Co-authored-by: lzmuhioin <1814826+lzmuhioin@users.noreply.github.com>
publishDirectory moved a finished product into place with one renameSync.
On Windows an antivirus scanner opens every freshly written file, so the
rename onto build/native-deps (right after hud-modifier-monitor.exe is
compiled) or hermes_cli/web_dist failed EPERM and threw away a build that
took minutes.
retryHeld retries EPERM/EACCES/EBUSY for about 3s and raises every other
code at once. All three publication renames use it, as does the HUD
helper's staging rename. The previous product's backup now sits beside
the output instead of inside withProduct's scratch tree, so a rollback
that also fails no longer has its only copy deleted by scratch cleanup;
the error names where it is.
Co-authored-by: dskwe <6286305+dskwe@users.noreply.github.com>
Co-authored-by: juliorobertoalvarez <39749815+juliorobertoalvarez@users.noreply.github.com>
Co-authored-by: bighamx <10262931+bighamx@users.noreply.github.com>
Co-authored-by: lzmuhioin <1814826+lzmuhioin@users.noreply.github.com>
Review on #132008: os.path.normcase/normpath derive case folding and
separator handling from the host, so a POSIX backend serving Windows
paths never folds C:\Users\me\Project with C:/Users/me/project, and
accented folders regress on the #65014 class. tui_gateway.project_tree
already defines the shape-derived identity the per-profile trees use;
reuse it so the merge agrees by construction with its inputs. The
casing test no longer monkeypatches normcase — it pins the real
normalization on POSIX, plus forward-slash and NFC/NFD fold tests
(red on the normcase key, green with _path_key).
Address sweeper review feedback on #92637:
1. Chain os.path.normpath before os.path.normcase to also handle
trailing-slash and '..' duplicate shapes, not just casing.
2. Add unit tests for _merge_profile_tree covering case-insensitive
merge, trailing-slash dedup, id fallback, and declared-vs-auto
identity precedence.
The all-profiles project tree merge used raw path strings as keys.
On Windows (case-insensitive fs), the same folder with different
casings across profiles (e.g. C:\Users\me\Project vs
C:\Users\me\project) produced duplicate top-level entries in the
sidebar Projects section.
Backend: normalize the merge key with os.path.normcase() (no-op on
POSIX, lowercases on Windows).
Frontend: deduplicate projectModel by id before rendering — a safety
net that catches any merge edge cases the backend fix doesn't cover.
Closes#92588
Main split ar.ts/zh-hant.ts into per-section modules, so the three new
statusbar keys (backend, messagingStopped, messagingDegraded) now live
in ar_chrome.ts/zh-hant_chrome.ts next to the statusbar section that
owns them, and the duplicate gateway-health-pill import left by the
cherry-pick is collapsed. Regenerated locales/_keys.desktop.json.
Co-authored-by: Noa <astraltrekkin@users.noreply.github.com>
CI failed on eslint perfectionist/sort-imports because the new helper
was imported after @/lib/icons. Also restore the padding-line blanks
eslint --fix wants around the derived messaging-down locals.
Co-authored-by: Noa <astraltrekkin@users.noreply.github.com>
The status-bar pill labeled the serve-backend socket "Gateway ready",
which users read as Discord/Telegram being up. Relabel the pill to
Backend and fold StatusResponse messaging liveness into the detail
when the messaging process is stopped or a platform is unhealthy.
Co-authored-by: Noa <astraltrekkin@users.noreply.github.com>
Review follow-up:
- The fallback-parameter assertions all passed the hardcoded English
constant, so deleting the fallback param kept the suite green
(verified: mutant survived 10/10). Pin the real per-locale values
from the i18n files (zh, zh-hant, ja, ar) so a broken fallback path
or an emptied locale string now flips a test; also pin the
localized-fallback 140-char cap.
- Reword the 'old Studio builder' comments: that chain lived in the
released 0.20.0 dist/client bundle quoted by #88488, not in this
repo's source. This app's own chain was text.slice(0, 140) || i18n
fallback and never read the title.
When the assistant reply is missing, fall back to a generic phrase instead of an empty body or the session title. Extract a tested helper so that title cannot sneak back into the chain.
The Send button's disabled gate derives from AUI composer state, which
lags the contentEditable by the coalesced per-frame flush — the same
seam Enter guards against by reading the live DOM (#39630). A disabled
button swallows the whole press in Chromium (pointer-events-none drops
mousedown/click/form-submit with no feedback), so while the flush is
stalled the button reads dead even though Enter still sends (#52950).
Sync the live editor into composer state at the top of the press
(form pointerdown-capture, before the press's hit-test resolves), so
the gate is open when the click hit-tests. Skip mid-IME-composition:
compositionend owns that flush. Preserve the HUD drag wiring.
The vertical sash grab band sat 1px into the leading pane and stole
the Windows overlay-scrollbar hit target. Keep the 8px band entirely
on the trailing side of the seam.
Review finding on the resumeSession half of #97809: the staleness test
compared the remembered hint against activeGatewayConnectionId() — the
CURRENT foreground socket — but resumeSession is the function that moves
the foreground (ensureGatewayAgent below), and hints are minted from the
AMBIENT connection at create/open time (sdk openSession), which in the
all-profiles view is not the foreground. So 'names a connection that
isn't active' was true for exactly the correct hints, and a pathname-
driven resume with no capturedOwner deleted them.
Use the row as the authority instead — the same predicate as the click
path (openStoredSession) and the boot auto-restore
(repairOwnerHintsForRestore): a connection-tagged cached row pins the
hint's route; an untagged or absent row leaves no basis to trust an
explicit persisted hint, so it is dropped rather than dialed. Explicit
captures stay authoritative as given.
Tests: the two hint tests now pin the row-based contract (untagged row
drops a legacy local hint; a tagged row keeps an agreeing hint even when
the window is not looking at that connection; a disagreeing hint is
dropped), and the resets clear the persisted storage copy like the
integrations suite does.
Older desktop builds persisted a `local` owner hint for sessions whose
rows carry no connection tag (the legacy primary-SSH path). Sidebar
clicks repair it (openStoredSession drops the hint for untagged rows),
but every pathname-driven resume (boot auto-restore, reconnect
re-resume, stranded-view self-heal) trusted the hint verbatim — dialing
the Mac backend for a remote session and dying with "session not
found".
Two seams now apply the same hygiene:
- resumeSession (use-session-actions/index.ts): a remembered hint is
only honored when the connection it names is this window's live
primary; a stale one is repaired (deleted), not just ignored, so it
cannot re-poison the next resume or the session-scoped RPC ladder.
An explicitly captured owner remains authoritative as given.
- useDesktopIntegrations boot auto-restore: the row ladder that
resolves a remembered/route session now repairs the owner hint from
the row (tagged row pins its exact route, untagged row drops the
stale hint), matching what openStoredSession does for clicks.
Fixes#97809
Review follow-up on the surrogate-pair truncation fix:
- lastErrorSummary caps astral error text through the shared
truncateText helper, so a surrogate pair can no longer be split at
the 200-unit cap (slice()'s UTF-16 limit).
- truncateText now segments on grapheme clusters (Intl.Segmenter, the
same idiom as profile-short-label.ts) instead of code points, so
ZWJ sequences, flags, skin tones, and combining marks stay intact
in job names/prompts.
- Tests port the grapheme shapes from profile-glyph.test.tsx and pin
the astral error-text cap.
The WeakSet keyed optimistic-row tracking on object identity, but SessionInfo
rows are re-spread by value on ordinary user paths (touchSessionActivity on
send, applySessionTitle on rename). A copy carried the painted `pinned` while
losing WeakSet membership, so the next reconcile read it as a page agreeing
with our write and released the guard while the PATCH was still in flight —
re-opening the revert #128198 closes. Stamp the row with an enumerable symbol
instead: identity no longer matters, and only a row a real list page delivered
(a fresh object without the stamp) can confirm the guard.
Also covers the unpin half in the failed-write comment.
Found by @Enough1122 review on #128198.
A local pin toggle left cached rows carrying the pre-toggle pinned flag; once the 10s write guard lapsed, the next reconcile (e.g. another shift+click) re-adopted the stale row and PATCHed it back. Paint the written value onto loaded rows at write time and never let those optimistic rows confirm the guard.
Fixes#128196
The child_env fixture still reset the allowlist via the removed
_allowed_env_vars_var ContextVar token, so every test using the fixture
(AttributeError at setup) failed across all Python/OS lanes once #90004's
process-wide set landed. Save/restore the set contents instead.
skill_view registers a skill's declared env vars (required_environment_
variables and the legacy prerequisites.env_vars) into the sandbox
passthrough allowlist. The allowlist was backed by a ContextVar, and
tool dispatch fans each tool call onto a worker whose context is a
copy_context() snapshot taken at submit time
(tools.thread_context.propagate_context_to_thread). The registration
therefore lived and died inside the skill_view worker's snapshot: the
submitting thread's context never saw it, and the next tool's snapshot
(execute_code, terminal) always started from an empty allowlist — so a
skill's env vars were never passed through, on any turn, in any session
(#90004). The config-based allowlist (terminal.env_passthrough) is a
module-level global and worked, which is why manually adding the var
there was the only workaround.
Store the skill-registered names in a process-wide set, matching the
config path's visibility. Cross-session exposure is limited to the
names — values still resolve per profile through
resolve_passthrough_value's secret_scope, so a name registered by one
session cannot read another profile's secret. clear_env_passthrough
keeps its reset semantics; a later skill_view re-registers on demand.
Review on #132013 (Enough1122): $ProgressPreference and the CLIXML strip
only covered probeWindowsRemote, but the same SSH connection runs four
more PowerShell builders that parse stdout the same way.
- windowsUpdateMarkerProbeCommand: fail-closed gate in
assertWindowsRemoteInstallUpdateClear reads .pop() of the last line —
a progress record serialized after the final Write-Output used to win
the pop and refuse SSH startup as kind='update-in-progress' (misleading
safety verdict). Add-Type compiles C# in this script, well outside the
probe's Get-Item/Get-Command progress suppression.
- helperCommand / atomicWindowsSpawnCommand: import the remote
hermes_cli module (auto-load progress) and JSON-parse the last line.
Extract stripPowerShellNoise (BOM strip + CLIXML block removal + the
last-meaningful-line convention) and share it across all four parsers;
prepend $ProgressPreference to the three parsed builders. The fourth
builder, buildWindowsInteractiveCommand, drives a -tt pty the user reads
directly — nothing parses its output, so it keeps no strip.
On the stderr premise: the issue's error string is the _fail() default
('SSH error connecting to …'), i.e. CLIXML in stderr on a non-zero exit —
exec() throws before any parse in that case, so the stdout strip is
defense-in-depth for the stdout-channel race the issue's grep loop
shows; $ProgressPreference suppresses both channels at the source.
Tests: the marker gate stays CLEAR with CLIXML before/after the verdict
line (red on base via .pop()), LIVE still pauses startup; all three
parsed builders pin $ProgressPreference before $ErrorActionPreference;
helper tolerates CLIXML around its JSON.
The CLIXML-tolerant probe now requires parsed.arch; the script-shape test's
stub only returned os. Same assertion either way — the fixture feeds
probeWindowsRemote only to capture the emitted script.
Answers the #132009 review (Enough1122): listEveryArchivedSession used
sessions.length as the next offset, but pages carry back-filled pinned rows
past the window (the pin back-fill ignores the archived filter), so the
cursor over-advanced and skipped archived rows, and comparing that inflated
count against the archived-only total ended pagination early. The offset
now advances by the page size only, rows dedupe by id, and the loop ends
once the cursor reaches page.total (which pins can't inflate) or a page
comes back empty. Regression tests pin the page-size offset, the pin dedupe
and the 1000-archived/300-pin case; the existing pagination tests now use
full 200-row pages so they exercise the real cursor contract.
The settings page now loads its archived list through
listEveryArchivedSession() (store module), so the '@/hermes' barrel
mock of listAllProfileSessions never intercepted the call and the
unarchive button never rendered in CI. Mock the store seam instead.
Main gained its own sidebar-archive.test.ts (refresh-failure coverage,
dbda53b8be) while #88504 was open; the cherry-picked pagination tests
landed on top of it. Merge both describes in one file and mock
'@/api/sessions' directly — the store now imports from the API module,
so the '@/hermes' barrel mock never intercepted the call.
GLM-5.3 rejects thinking.type=disabled with HTTP 400 code 1210
(issue #85890, #96373). An explicit off preference now sends
enabled plus reasoning_effort=low. 4.5/4.6/5.2 still send disabled.
A targeted or failed CLI run must not clear another session's stale
flag or wipe its last_edit_at. Gate fan-out and the workspace fallback
on status=passed and scope=full.
Standalone hermes verify records under session_id=default. The stop
guard keyed on the editing session never saw that pass. Share verify
events across sessions for the same root.
The status snapshot hook skips its poll while the window is unfocused, so a credential-readiness failure could remain on screen after an OAuth sign-in completed if the user stayed in the browser. Subscribe to the onboarding store and force one focus-independent readiness refresh on the auth-success transition, restoring the normal focus-gated cadence afterwards. Adds a regression test plus a guard that non-success transitions do not force a background poll.
The group-leadership guard nulls a non-leader pgid so the shared group is
never killpg'd, but that also skipped the helpers-outlive-leader sweep
(#76759): a SIGHUP-ignoring helper of a shared-group child kept the PTY
slave open past close(). Snapshot the child's descendants with psutil
BEFORE the parent is signalled (they reparent and become undiscoverable
once it exits), then send SIGHUP with the helper grace and SIGKILL the
survivors individually — mirroring browser_tool_lifecycle's non-leader
branch and the group sweep's leader-dead cadence.
The grandchild in test_real_shared_group_child_descendants_are_killed
reparents to init the moment proc.kill() lands, and psutil's kill routes
through os.kill, which the guard only authorizes for the test process's
subtree — a walk that races with the reparenting. The blocked kill was
suppressed by the sweep's contextlib.suppress, so the grandchild survived
and the test flaked ~1.5% on the macOS CI lane (and 3/200 locally).
Signalling a reparented descendant is the exact contract under test, the
established pattern for live-signal tests; the flake is gone over 400
local full-file runs.
- process_registry._reap_untracked: delegate to _terminate_host_pid with the
session's recorded host_start_time instead of a raw killpg -- pgid == pid
only proves group leadership, and a reaped child recycled onto an unrelated
group leader would still have been signalled. The helper revalidates the
kernel start time and refuses on mismatch, and settles descendants from a
psutil snapshot so no group signal is needed at all.
- browser_tool_lifecycle._legacy_kill_process_tree: in the non-leader path,
snapshot descendants via psutil before proc.kill() and kill each
individually -- a bare proc.kill() left grandchildren holding the capture
pipe's write end, reintroducing the communicate() hang the helper exists
to prevent.
_kill_process_tree fallback and three latent killpg callers resolved
os.getpgid(child) and signalled the result unconditionally. A child that
shares the caller's group resolves to that group, so TERM+KILL took down
the whole Hermes tree; a reaped-and-recycled pid resolves to a foreign
leader's group. Signal the group only when pgid == pid, the rule
deadline.kill_process_tree and _subprocess_compat already use, and fall
back to signalling the direct child otherwise.
The sk-or- shape guard on unbound OPENAI_API_KEY (openrouter.ai context)
is deliberate: a real OpenAI key must never reach a third-party host.
The new fallback test used a neutral string, so the guard correctly
refused it. Make the fixture key sk-or- shaped so the test exercises the
fallback it means to.
Lift the credential pool's op:// carve-out into
hermes_cli.config.get_env_value_prefer_dotenv so the OpenRouter/OPENAI
fallback rungs added here cannot send an unresolved op:// reference as
the API key when the 1Password source already resolved the key into the
active scope. Mirrors the pool's precedence test on the resolver path
and covers the OPENAI_API_KEY .env rung.
The per-call provider argument on text_to_speech (added via #73513,
closing #47459) let the model reroute speech to any configured backend,
contradicting the tool's own schema header ("Voice and provider are
user-configured ... not model-selected") and leaving compact channels
unable to keep a house voice without prompt-layer appendices.
- drop provider from the model-facing TTS_SCHEMA
- stop forwarding args["provider"] in the registry handler
- keep the function argument for internal/test callers, but a value
disagreeing with the configured tts.provider is now ignored with a
warning (both text_to_speech_tool and _text_to_speech_single)
Fixes#90109
VoiceReceiver.start() captured conn.secret_key (by value) and
conn.dave_session (by reference) exactly once. Both go stale in normal
operation:
- op 4 SESSION_DESCRIPTION rotates the transport secret_key on every
voice (re)connect — the snapshot then fails NaCl decrypt forever.
- voice_state.reinit_dave_session() REPLACES conn.dave_session with a
NEW DaveSession when none existed yet, so a receiver that starts
before DAVE finishes negotiating keeps None forever and treats E2EE
as off — every frame reaches opus as ciphertext for the whole
session.
The failure is silent by construction: decrypt failures warn for the
first 10 packets then go dark, and shredded opus output raises nothing.
The operator-visible symptom is "the bot ignores me."
Fix — resolve, don't copy:
- Credentials live in ONE tuple (secret_key, dave_session,
dave_protocol_version, dave_downgraded), re-resolved from the live
connection and swapped in a single reference store; the receive
thread reads one consistent generation per packet, so a refresh can
never pair a new key with an old session.
- Refresh triggers: the voice-ws hook on op 4 and op 24 (the hook runs
AFTER received_message dispatches the op, so it reads the new
values); membership changes in the bot's channel (DAVE epoch bump)
via the existing on_voice_state_update handler; and a
25-consecutive-NaCl-failure streak as the catch-all, retried every
streak window while the failure persists.
- Unmapped-SSRC hole closed without recreating it elsewhere: with E2EE
actively on (protocol_version > 0 and outside a passthrough window),
frames for an SSRC with no user mapping are dropped (after the
existing sole-member inference) instead of being fed to opus as
ciphertext. A non-null session alone is NOT the predicate — the
session object survives protocol downgrades to 0 and passthrough
transitions, where plaintext is legitimate. Passthrough windows
mirror discord.py's own set_passthrough_mode calls: op 21
pending-downgrade opens 120s; op 22 opens a 10s grace ONLY on the
dave_downgraded True -> False edge (upgrade-after-downgrade), with
replace-semantics so the short grace supersedes a residual long
window; a receiver starting mid-pending-downgrade seeds the window
from conn.dave_pending_transitions.
- Silence made visible: decrypt failures keep emitting one warning per
250 after the first 10, and stop() logs decode-health counters
(ok / decrypt_failed / dave_unmapped_dropped).
Tests: tests/gateway/test_discord_voice_rekey.py (24) — credential
resolution/rotation, the late-DAVE-session upgrade, failure-streak
refresh + reset, op 4/24/5 hook behavior, original-hook passthrough,
the DAVE-state drop predicate (downgraded-plaintext decodes, op-21
window decodes, expired window drops, upgrade edge replaces the long
window, same-version and duplicate op 22 open nothing,
mid-pending-downgrade startup seeds), unmapped-drop vs mapped flow,
sole-member rescue, tuple-generation consistency, teardown health
line. Adjacent voice_command / voice_mixer / send / opus suites green
under the diff (111 passed).
Fixes#77968
Port of #72710 by @foma-agent onto current main.
With provider: copilot-acp the model sometimes announces its next action
and ends the turn without emitting a tool call, in terse log style with
no first-person lead-in: "Brief written. Creating the session now."
The intent-ack detector only matched first-person future acks
(_ACK_FUTURE_RE), so the task stalled until the user sent another
message.
looks_like_codex_intermediate_ack now also accepts a narrowly bounded
pronounless action clause: a present-participle action verb that begins
the response or follows a sentence/status/newline boundary, plus an
explicit transition cue ("now", "on Copilot via acpx", "the actual log",
"with corrected arguments", "whether ... healthy", ...). Questions,
completed reports ("Testing complete."), bare status counters, retry
counters and numbered/option lists stay final.
The 16-commit PR branch predates main's agent_runtime_helpers refactor
(dead-predicate prune eb67765c58, module-level _ACK_* constants), so the
final state is re-applied by hand: main's constants are kept, the
pronounless detector folds into the guardrail chain, and the PR's test
corpus (21 announcements that continue, 70 finals/questions/status
lists that do not) is restored minus the test for the pruned
intent_ack_continuation_enabled helper.
Co-authored-by: foma <foma@bokonon.ai>
Fixes#72692