Default kanban_create children now keep fresh scratch paths, while explicit dir sharing remains supported and project context resolves to a per-task worktree. Surface resolved workspace fields in create responses/events and cover scratch mutation, nesting, explicit sharing, and project inheritance.
Fixes#67567
Dispatcher-spawned Kanban workers are finite one-shot processes, so detached delegation completions can outlive their only consumer. Mark that runtime as unable to deliver async completions and reuse the synchronous delegation fallback, returning required child results before the worker exits.\n\nAlso make unsupported-session notes runtime-generic and cover the delayed-child lifecycle regression.\n\nRefs #63169
Treat cua-driver's Linux `is_on_screen: null` as unknown instead of
off-screen, and skip GNOME Shell desktop/backdrop helper windows
(ding "Desktop Icons", @!x,y;BDHF) when selecting the default capture
target — they are targetable X11 windows but capture as empty.
Reconciled with the _NET_ACTIVE_WINDOW fallback from #58030: helper
windows are filtered out of the candidate pool first, then the tied
z-order active-window probe runs on the remaining real app windows.
Also falls back to the requested app name for _last_app when Linux
windows carry no app_name.
Salvaged from #54173 by @dnth.
The three subprocess tests spawn 'sys.executable -c' children that import
hermes_cli. From a worktree, the child resolved the MAIN checkout's editable
install instead of the tree under test, so the new DB/CLI guards appeared
missing and the tests failed with rc=0. Route the spawns through a helper
that pins the repo root under test on PYTHONPATH.
cua_driver_update_check now short-circuits to None when no driver
resolves; CI has none installed, so pin resolve_cua_driver_cmd in the
update-check and env-sanitization tests.
- Add autouse fixture to TestMcpInvocationResolution to disable
--no-overlay flag so existing tests assert baseline args
- Make test_config_load_failure_fails_safe and test_missing_section_enables
platform-aware (Linux auto-detect returns True, macOS/Windows False)
Post-rebase consolidation over the merged C7/C16/C10 work:
- _ensure_dm_conversation now records workspace ownership via
_remember_channel_team and bounds _dm_conversation_cache (cap 5000)
- module-level _slack_dm_cache (C7 standalone path) bounded oldest-first
- _user_is_bot_cache (C10) bounded with _trim_oldest_dict_entries
- caption-mode contract tests updated for the C7+C8 merged media path
* test(clarify-gateway): cover signature, timeout fallback, and notify paths for 100% coverage
Fixes#36531
(cherry picked from commit 5265dfe2f5)
* fix(clarify): one canonical timeout across CLI, TUI/desktop, and gateway
The clarify wait timeout was resolved three different (wrong) ways:
- CLI (`cli.py`, `hermes_cli/callbacks.py`) read a non-existent top-level
`clarify.timeout`, so it always fell through to a hardcoded 120s instead of
the canonical `agent.clarify_timeout` (default 3600) the gateway uses (#42969).
- The TUI/desktop bridge called `_block("clarify.request", …)` with no timeout,
so it used the hardcoded 300s `_block` default and ignored config (#51960).
- There was no way to disable the auto-skip: a user who wanted the agent to wait
indefinitely while they think couldn't get it.
Collapse all of this onto a single resolver:
- `tools.clarify_gateway.resolve_clarify_timeout(config)` is the one source of
truth. Order: explicit legacy `clarify.timeout` (back-compat) → canonical
`agent.clarify_timeout` → 3600. `<= 0` is preserved verbatim as "unlimited".
- CLI, callbacks, and the TUI bridge (`_clarify_timeout_seconds`) all route
through it, so the three surfaces can't drift.
- `<= 0` means unlimited everywhere: `wait_for_response` and `_block` drop the
deadline (heartbeat still fires), and the CLI hides its countdown.
Tests: resolver order / default / non-numeric / unlimited-sentinel; an
unlimited `wait_for_response` blocks until resolved rather than auto-skipping;
the TUI clarify bridge passes the configured timeout to `_block`.
Supersedes #42974 (CLI key), #51993 (TUI honors config), and #68986 (unlimited
wait); folds in #52031 (clarify_gateway coverage).
Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: lkevincc0 <lkevincc0@users.noreply.github.com>
Co-authored-by: theone139344 <theone139344@users.noreply.github.com>
Co-authored-by: baauzi <baauzi@users.noreply.github.com>
---------
Co-authored-by: Christopher-Schulze <210261288+Christopher-Schulze@users.noreply.github.com>
Co-authored-by: liuhao1024 <liuhao1024@users.noreply.github.com>
Co-authored-by: lkevincc0 <lkevincc0@users.noreply.github.com>
Co-authored-by: theone139344 <theone139344@users.noreply.github.com>
Co-authored-by: baauzi <baauzi@users.noreply.github.com>
Integration test for the two compaction layers landing this sweep:
#63144's discovery-scope fix (archived rows surface from the current
session) and #69334's bookend bounding (summary exclusion + content
caps). A single compacted session exercises both: the archived FTS hit
must surface while the compaction handoff at the session tail stays out
of bookend_end and long messages stay capped.
Addresses @teknium1's second review round on #63144:
1. _is_compacted_message now checks both active=0 AND compacted=1.
Previously checked active=0 alone, which also matched rewind/undo rows
(active=0, compacted=0) that must stay hidden.
2. Added _is_compression_ended() — checks only the session's own
end_reason, not the lineage-wide has_compression_hop flag. This prevents
delegation children living under a compression continuation from leaking
through the lineage filter.
3. _discover lineage skip now uses is_ended_session (session-level check)
instead of has_compression (lineage-level flag).
New tests:
- TestRewindExclusion: rewind rows stay hidden alongside compacted rows
- TestCompressionEndedHelper: session-level end_reason checks
- TestLegacyContinuationPlusDelegation: delegate child excluded while
compression ancestors surface
78 passed (71 + 7 new).
After context compaction, pre-compaction content was invisible to
session_search — a memory black hole. The _discover() skip logic
filtered both same-session and same-lineage hits unconditionally,
without distinguishing compression-summarised content (gone from live
context) from delegation children (still visible to the parent agent).
Reworked _resolve_to_parent to return (root_id, has_compression_hop),
checking end_reason='compression' on every hop during the same
db.get_session() traversal — zero extra queries.
_discover() now has three compression-aware paths:
- In-place compaction: FTS hits on active=0 (compacted=1) rows pass
through even when raw_sid == current_session_id
- Legacy rotation: lineage hits pass through when has_compression_hop
is true on either side of the chain
- Delegation children: still excluded (no compression edge)
18 new tests covering all three scenarios + unit tests for the helpers.
Addresses Teknium's review feedback on #6256.
Closes#13840, #13841.
mark_speech_interrupted() / take_speech_interrupted(): a one-shot,
TTL'd (120s) latch plus SPEECH_INTERRUPTED_NOTE. Barge-in paths mark
it when they cut live speech; the next turn's submit path pops it and
prepends the note to the model-bound message — API-call local, never
persisted, so history and prompt caching are untouched.
The streaming gate broadens from ElevenLabs-only to any provider that
passes check_tts_requirements(). In continuous voice mode a mic monitor
runs during playback: talking over the agent cuts TTS at detection
while the monitor keeps recording, then the captured interruption is
transcribed and queued as the next turn (process_loop's auto-restart
stands down while the capture owns the mic). New voice.barge_in config
key, default true.
listen_for_speech(): sustained-RMS speech detection with the noise
floor calibrated against audible playback (speaker bleed doesn't
self-trigger). With capture=True it keeps a rolling pre-roll buffer and
records through to a silence endpoint, so the interruption is
transcribed from its first syllable — detection alone loses the opening
words to the detector's sustain window plus the mic re-open.
transcribe_recording() maps no_speech provider results to a successful
empty transcript (silence, not an error).
An STT provider that hears no words is reporting silence, not failing.
ElevenLabs/xAI empty-transcript errors now carry no_speech so live
voice loops can re-listen quietly instead of surfacing an error on
every pause.
StreamingTTSProvider ABC + registry + resolve_streaming_provider() —
ElevenLabs (pcm_24000) and OpenAI (response_format=pcm) stream chunked
PCM; every other provider keeps its configured voice and falls back to
per-sentence sync synthesis. SentenceChunker is the one incremental
cutter every surface shares: sentence-boundary cuts on the delta
stream, <think> blocks stripped even when split across deltas, short
fragments merged forward so they never stall as tiny clips.
Extract the open_preview emitter into a shared tools/desktop_ui bridge
(one gateway-injected sink, routed by HERMES_UI_SESSION_ID) and add a
second desktop-gated tool on top of it:
- focus_pane(chat|files|terminal|review|sessions) -> pane.reveal event.
The desktop runs each pane's own reveal path (revealDesktopPane table)
and only acts on the active window -- a background turn never moves the
user's focus (desktop AGENTS.md: offer, don't hijack).
open_preview now emits through the same bridge. Both tools are check_fn
on HERMES_DESKTOP (zero footprint elsewhere), sitting beside
read_terminal/close_terminal in _HERMES_CORE_TOOLS.
Deliberately not adding run_slash: letting the agent fire slash commands
mid-turn (/model, /new, /clear) fights prompt-cache + conversation
invariants.
Add a desktop-gated open_preview tool so 'open cnn.com in the preview
pane' works. The tool (check_fn on HERMES_DESKTOP, zero footprint
elsewhere) emits a preview.open event through a gateway-injected emitter,
mirroring the close_terminal -> terminal.close bridge. The desktop
handles it in usePreviewRouting, normalizing the target and opening the
pane for the active session only -- a background turn never hijacks it.
Bare domains and localhost are coaxed into fetchable URLs (www.cnn.com ->
https://, localhost:3000 -> http://); file paths and schemes pass through
to the renderer's normalizer.
In Slack threads, ordinary follow-up messages sent while a native
multi-choice clarify was pending were consumed as clarify answers —
_coerce_text_response accepted arbitrary text for any pending entry, so
the gateway text-intercept swallowed unrelated thread messages and the
user's messages appeared to be ignored.
Tighten resolve_text_response_for_session for native interactive
multi-choice prompts (buttons rendered, awaiting_text=False):
- numeric selections ("2") still resolve to the canonical choice
- exact choice-label matches (case-insensitive) still resolve
- arbitrary prose is now REJECTED (returns False) so the message
continues as a normal turn instead of vanishing into the clarify
Behavior is preserved everywhere free text is legitimately the answer:
open-ended clarifies, explicit 'Other' text-capture mode, and the base
adapter's numbered-text fallback (which flips awaiting_text at send
time).
Salvaged from PR #62042 by @liuhao1024.
Fixes#62034
Context-compaction handoff summaries (prefixed with [CONTEXT COMPACTION])
were being returned as normal bookend_start/bookend_end messages in
session_search discovery mode. A single compaction handoff could be 57K+
chars, immediately bloating a fresh session prompt to 73K+ chars from one
search hit.
Changes:
- Add _COMPACTION_PREFIXES and _is_compaction_summary() helper
- Filter compaction summaries from bookend_start and bookend_end in _discover()
- Cap bookend content to 1200 chars and window content to 4000 chars
- Add content_truncated/original_content_chars metadata when truncation occurs
- Add 6 regression tests covering prefix detection, bookend filtering,
content capping, and legacy [CONTEXT SUMMARY] prefix
Fixes#43175
Children inherit the parent's env, repo, and toolsets but were denied
execute_code ('children should reason step-by-step, not write scripts').
That forces subagents doing mechanical multi-step work (batch file
reads, fetch-N-pages loops, filter-before-context reductions) to burn
reasoning iterations one tool call at a time.
- Remove execute_code from DELEGATE_BLOCKED_TOOLS
- Stop stripping the code_execution toolset from child bundles
- No recursion risk: the sandbox bridges only the 7 SANDBOX_ALLOWED_TOOLS
(web/file/terminal) — delegate_task and execute_code itself are not
reachable from inside a sandbox script
- Update schema text, AGENTS.md, and delegation docs
- Tests: blocked-constant, strip, and child-assembly tests updated;
new test pins execute_code as intentionally unblocked
Surgical reapply of PR #37523 onto current main (the original branch
predates the SecretSource registry refactor). _build_safe_env() now
forwards env vars tagged in env_loader._SECRET_SOURCES — widened from
Bitwarden-only to any registered secret source (Bitwarden, 1Password,
plugin backends), since the provenance map is source-agnostic.
Explicit server env: config still wins; untagged secrets stay filtered.
Fixes#37499.
test_session_expired_retry_waits_for_new_session hardcoded
_server_breaker_opened_at["hindsight"] = 123.0 to simulate a circuit breaker
whose cooldown has already elapsed. But the breaker in tools/mcp_tool.py
compares that stamp against time.monotonic() (age = monotonic() - opened_at,
elapsed when age >= _CIRCUIT_BREAKER_COOLDOWN_SEC). time.monotonic()'s origin
is arbitrary and small on a freshly-booted CI container, so age worked out to
only a few seconds there (< the 60s cooldown) — the breaker stayed open, the
half-open probe never fired, and the retry returned the "unreachable" error
instead of "bank ok". It passed on long-uptime dev boxes (large monotonic)
and failed under CI, with the reported "Auto-retry available in ~Ns" drifting
run to run as the container's monotonic clock varied.
Stamp opened_at relative to the same clock the code reads
(time.monotonic() - _CIRCUIT_BREAKER_COOLDOWN_SEC - 1.0) so the cooldown is
provably elapsed regardless of the monotonic origin, exercising the intended
half-open transition deterministically.
Builds on diffen77's #66547 (cherry-picked as the previous commits).
Extend _is_session_expired_error's iterative traversal to follow
__cause__/__context__ in addition to ExceptionGroup .exceptions — SDK
wrappers often raise a generic RuntimeError *from* the message-less
ClosedResourceError, leaving the transport signal reachable only via
the chain. The identity-visited set guards chain cycles (handlers
re-raising previously seen exceptions), and a bounded node budget
(_EXC_TRAVERSAL_MAX_NODES) caps pathological acyclic graphs.
Adds regression tests: cause/context chain detection, interruption
precedence through chains, cyclic cause/context termination, and
budget-bounded termination.
Builds on trevorgordon981's #50589 (cherry-picked as the previous commit).
The #50394 cooldown reset only ran inside the async _shutdown coroutine,
which is skipped on the empty-_servers fast path — the most common state
when a server failed to connect (failed servers are never recorded in
_servers). It was also skipped when the MCP loop wasn't running.
Clear _server_connect_retry_after/_server_connect_failures on the fast
path and in a final unconditional sweep so a full shutdown/restart always
re-attempts every configured server immediately. Adds regression tests
for both paths.
Log-storm hygiene for the reconnect machinery (#65673, #66092):
- State transitions carry exactly one WARNING each:
connected → degraded (keepalive failure), degraded → parked
(budget exhausted / rapid-drop park / permanent error),
parked → connected (revival proven healthy, via
_mark_session_proven).
- Per-attempt retry logs (initial-connect attempts, connection-lost
reconnect attempts, per-cycle rebuild notices, park-wake probes)
demoted to DEBUG.
- Backoff sleeps get +/-20% uniform jitter (_jittered) so a herd of
servers that lost the same backend doesn't retry — and log — in
lockstep.
- Add module-level _unwrap_exception_group (ported from
hermes_cli/mcp_config.py, adapted): handles nested groups, prefers
non-cancellation leaves over the CancelledErrors anyio sprays across
sibling tasks, and re-raises KeyboardInterrupt/SystemExit leaves.
- Add _classify_mcp_failure(exc) -> 'permanent'|'transient'.
Permanent: auth 401/403, NonMcpEndpointError, InvalidMcpUrlError,
FileNotFoundError/ENOENT on the stdio command. Transient: everything
else (network/EOF/ClosedResource/TaskGroup drops). Permanent failures
park immediately without burning the retry ladder — every retry
against a missing binary or a revoked credential hits the same wall.
- Every log site in run() and the keepalive failure log now logs the
unwrapped root cause as 'TypeName: message', so a dead stdio pipe
says 'BrokenPipeError' instead of the opaque
'unhandled errors in a TaskGroup (1 sub-exception)' (and empty
str(exc) dead-pipe errors are no longer blank).
Harden the immediate-reconnect path from #66271:
- _reconnect_or_reraise_group re-raises when the transport TaskGroup
carries a KeyboardInterrupt / SystemExit leaf — fatal signals must
propagate to the interpreter, never be converted into a reconnect.
- Rapid-drop budget: a completed handshake alone no longer clears
_reconnect_retries/backoff on clean transport return. A session is
UNPROVEN until it demonstrates real health — survived >=1 full
keepalive interval (keepalive success path) or served >=1 successful
tool call (_mark_session_proven). Unproven clean returns are charged
against _MAX_RECONNECT_RETRIES, so a flapping transport that
handshakes fine and drops moments later still reaches the park
instead of respawning forever (#62212: 6212 spawns in 63h).
Proven sessions keep the #57604 behaviour: budget clears, transient
blips over a long-lived session never accumulate toward parking.
Streamable-HTTP / SSE MCP transports run their stream pump inside an anyio
TaskGroup. A transient stream drop (idle timeout, brief backend blip,
server-side TCP close) surfaces as a BaseExceptionGroup escaping the
transport context manager. It reached run()'s error path, which applied
exponential backoff (1s..16s) and eventually parked the server for 300s and
deregistered its tools — turning a sub-second glitch into a multi-minute
tool outage even though the POST path was still healthy.
_run_http now wraps all three transport branches (SSE, new + deprecated
Streamable-HTTP) and routes a BaseExceptionGroup through a new
_reconnect_or_reraise_group() helper that returns 'reconnect' (immediate
rebuild, no backoff/park/deregister). It re-raises instead of masking when
shutdown is in progress, when the group carries a real CancelledError
(cancellation must propagate, cf. #9930), or when no live session was
established this attempt (a connect/handshake failure that should fall
through to run()'s backoff rather than hot-loop).
Fixes#66092
Co-authored-by: 雨哥 <hanyu1212@users.noreply.github.com>
Update the Matrix reaction-seeding contract to the four-reaction default
(once/session/always/deny), add tirith-tier (session without always) and
no-session-tier cases, and assert allow_session=True in the tirith
gateway payload.
Three related messaging-approval fixes:
1. approvals.timeout default 60 -> 300. PR #63501 collapsed the gateway
wait onto the canonical approvals.timeout (previously
gateway_timeout=300), silently shrinking messaging approval windows
to 60s. Push-notification approvals routinely arrive later than a
minute; taps landed after the wait had already failed closed.
2. Stale-tap honesty: adapters resolved the approval AFTER rendering
'<checkmark> Approved by <user>' (Telegram/Discord/Slack), or ignored a zero
resolve count (WhatsApp Cloud/Feishu). A tap on an expired prompt
claimed approval while the command had already been denied. All
button paths now resolve first and render 'Approval expired -
command was not run' when nothing was waiting.
3. Mixed-warning prompts (dangerous pattern + tirith finding) now offer
Always: the persistence layer already permanently allowlists the
pattern key and downgrades the tirith key to session scope, but the
UI hid Always whenever ANY tirith warning was present. Pure-tirith
prompts still withhold Always (content findings are session-max by
design), and Smart-DENY overrides remain once-only.
_resolve_active_context_length() called get_model_context_length() with the
model id alone, so provider-enforced windows (e.g. Codex OAuth's 272K for
gpt-5.x vs the direct API's 1.05M) never reached the tool-search activation
gate — it sized against generic metadata for the same slug.
Resolve the runtime provider for the configured model and pass provider,
base_url, and api_key through. If credential resolution fails (offline, no
keys), degrade to a provider+base_url-only lookup so the static
provider-aware fallbacks still apply; explicit model.context_length keeps
short-circuiting as before (#46620). Gap flagged during review of #16735.