The async-delegation watcher drained the completion queue as a batch but
then delivered each event as its own synthetic turn, flooding the session
when a fan-out of background subagents finished together. Builds on the
per-process completion batching salvaged from PR #71898 (thanks
@yuzilongleif-collab) which coalesces concurrent _run_process_watcher
completions behind a short per-route fan-in window.
This commit adds the async-delegation half: group the drained batch by
full routing key (session_key + parent_session_id + platform/chat/thread/
user) and inject ONE consolidated turn per group. Durable-ack handling
stays honest: sibling rows are claimed up front via claim_event_delivery;
rows another consumer owns are excluded from the consolidated text (no
double-delivery); sibling claims are acknowledged only after adapter
acceptance and released (still pending) on failure. Events for different
sessions never coalesce, and a single-event group rides the existing
per-event path unchanged (latency and text identical).
Tests: 3 same-tick events -> exactly one adapter.handle_message carrying
all 3 results with all 3 durable rows delivered; 2 sessions -> 2 turns;
single-event path unchanged; failed batch releases claims and retries;
foreign-claimed sibling excluded and left pending.
Before delivery_mode existed the notifier woke unconditionally when the task
carried a session_id — pre-existing gateway subscriptions had de facto active
wake. The column's 'notify' default alone would silently disable that on
upgrade. Backfill gateway rows to notify+wake on first-add only (tui stays
notify); explicit user downgrades are never overwritten by re-migration.
Sabotage-verified regression tests included.
Builds on Paolo Antinori's #68431 salvage for #64176. Move plugin dispatch behind the profile-scoped runner authorization boundary, fail closed on malformed reaction identities, preserve observer registration across Telegram app rebuilds, and document the deliberately observer-only contract.
Co-authored-by: Paolo Antinori <pantinor@redhat.com>
Route plugin target parsing, validation, and host-driven delivery through PlatformEntry across CLI and cron while preserving the host-only send_message policy.
* fix(models_dev): map meta-ai provider to models.dev 'meta' id
Muse Spark models (muse-spark-1.1/1.2/-contributor) are served via the Meta
Model API and reverse-map from api.meta.ai to the Hermes provider id 'meta-ai'.
models.dev keys the same models under the provider id 'meta'.
lookup_models_dev_context() / _get_provider_models() resolve the models.dev id
strictly via PROVIDER_TO_MODELS_DEV.get(provider) (no raw-id fallback, unlike
get_model_info()), so an unmapped 'meta-ai' missed entirely and context fell
back to the generic 256K default instead of the true ~1M window. Add the
meta-ai->meta mapping (plus a defensive meta->meta) so context and pricing
resolve from models.dev: 1.1=1,000,000; 1.2 & -contributor=1,048,576.
* add contributor email
---------
Co-authored-by: Beto de Paola <betodepaola@meta.com>
Salvaged PR #83678's commit is authored under a generic local agent
identity with no linked GitHub account; map it to the PR opener for
release attribution (same pattern as hermes-agent@users.noreply.local).
Two-part fix for the dashboard fd exhaustion reported in #81547:
1. Raise RLIMIT_NOFILE soft limit on startup (before uvicorn binds).
macOS defaults to 256 for LaunchAgent processes — too tight for the
dashboard which opens 3 fds (db+wal+shm) per SessionDB per request
across all profiles. After days of polling the soft limit exhausts
and every os.listdir/open raises OSError [Errno 24]. The helper raises
to the hard limit (or minimum 4096), matching the reporter's ulimit
workaround. No-op on Windows (no resource module).
2. Replace bare Path.iterdir() with context-managed os.scandir() in four
dashboard hot paths: _fallback_profile_dicts, file manager list,
checkpoint listing, and plugin discovery. iterdir() returns a
generator that holds an open directory fd until fully consumed; if
an exception interrupts iteration the fd leaks. os.scandir() is an
explicit context manager that guarantees close on exit, following
the same idiom already used in /api/fs/list.
Tests: 6 passed, 3 skipped (resource-module tests skip on Windows).
Follow-ups on the salvaged Browser Use CLI integration (PR #66476):
- browser_exec runs model-written Python on the host. Strip it at
tool-definition time for sessions whose resolved toolsets exclude
'terminal' so terminal-less surfaces (locked-down messaging configs)
don't silently regain host code execution through the browser toolset.
Session-level gate in model_tools, not a check_fn (check_fn results are
TTL-cached process-wide across sessions).
- Replace the live 'browser-use skill' schema fetch with a pinned helpers
digest: no third-party version-drifting text in the prompt, byte-stable
schema across machines. A/B benchmarked (108 runs, opus-4.8 + kimi-k3,
6 multi-step web tasks x 3 arms x 3 reps): pinned digest matches the
full skill dump 36/36 vs 36/36 at ~equal tokens; both cut total task
tokens ~60% vs the legacy browser_* toolset.
- Docs note for the terminal gate; contributor mapping for salvage.
Two hardening guards extracted from #82766 by @StanleyStetson:
- bool is an int subclass, so a JSON `true` in truncate_before_user_ordinal
coerced via int() to ordinal 1 and aimed a CONFIRMED rewind at the second
user turn — the same silent-loss class as #82756. Reject with 4004.
- confirm_truncate with no truncation target is leaked client rewind state
on an ordinary submit; fail fast with 4004 instead of silently ignoring
the flag, so the corrupted client state is surfaced.
Part of the composite fix for #82756.