Commit Graph

23 Commits

Author SHA1 Message Date
teknium1 21bc1d3775 fix(api-server): trim RequestKey fallback comment, add import-isolation test
The salvaged comment restated the symptom at length; keep only the WHY.
Drop the `# pragma: no cover` markers (the repo does not gate on coverage).
One invariant test: with aiohttp.web_request lacking RequestKey, `web` stays
bound to the aiohttp module and RequestKey is None (red on origin/main).
2026-09-12 08:04:30 -07:00
hoozge 943e04ba2b fix(gateway): keep aiohttp web import alive when RequestKey is unavailable
On aiohttp < 3.14 the RequestKey import fails and the shared except clause
also resets the already-imported web module to None, so every admission
reply (non-streaming POST /v1/runs) raised AttributeError: 'NoneType'
object has no attribute 'json_response' and surfaced as HTTP 500.

Import the two names in separate try/except blocks; RequestKey already has
None-guards at its use sites.
2026-09-12 08:04:30 -07:00
nmediaie 65579d8a46 fix(gateway): bind HERMES_SESSION_PROFILE per /p/<profile>/ api request
_bind_api_server_session never passed profile= to set_session_vars, so
every API-server turn bound HERMES_SESSION_PROFILE="" and the terminal
tool collapsed ALL api_server sessions (default AND org profiles) onto
the shared 'default' container key. In a multiplex gateway the first
caller after restart therefore won the cache slot and org-profile turns
could reuse the default profile's sandbox container (SSH key and secrets
exposure). Pass the request profile through so org turns key their own
profile-scoped container.

(cherry picked from commit b8b01b3d8cdafab292dda4388592044393222886)
2026-09-11 15:28:00 -07:00
Erosika 0ac631b6c0 feat(api): accept a turn author on session chat and runs; peer dm forwards it
POST /api/sessions/{id}/chat, /chat/stream and /v1/runs take an optional
"author" object. absent or null keeps today's behavior; a non-object is a 400
invalid_author. the value reaches run_conversation as turn_author and nothing
else: it is a claim by an API-key holder and grants no capability.

hermes peer dm and peer run add the author to the request body when the
message_agent runner launched them with HERMES_TURN_AUTHOR set, so a
cross-machine dm is attributed the same way a local one is.
2026-09-10 10:27:07 -07:00
Teknium d5926b2494 fix: persist API delegation units once without waking the model 2026-09-09 10:55:32 -07:00
liuhao1024 71818aaf52 fix(gateway): deny wake authority for caller-supplied conversation_history
#98619 review follow-up: an explicit session_id plus non-empty
caller-supplied conversation_history skipped the SessionDB load yet was
still marked wake-capable ('not previous_response_id' alone). Such a
turn is authoritative on the caller snapshot and never consumes the
SessionDB delivery row a detached completion persists, so wake is now
fixed before the history load can overwrite the variable and denied on
the same default-deny contract as previous_response_id continuations.

Adds an HTTP regression proving the binder receives wake_capable=""
and _conversation_history_for_session stays uncalled on that branch.
2026-09-09 10:55:32 -07:00
liuhao1024 0efb525420 fix(gateway): adopt live compression continuation for /v1/runs delivery
Address the compression-rotation blocker from the follow-up review of #98619:

The parent run can compress/rotate before a detached delegate completes.
The captured origin session id is then a closed parent: the delivery
append is rejected with CompressionSessionClosedError forever (the
watcher retries the same stale id), and no read surface exposes the row.

- persist_delegation_delivery now resolves the live continuation tip via
  the canonical resolve_resume_session_id (the same resolution
  /api/sessions/{id}/messages reads use) before appending, failing open
  to the original id.
- /v1/runs adopts the resolved tip for a client-addressed session id
  (body id or declared key): history loads from it, the turn and wake
  binding target it, so the run no longer writes into the closed parent.

End-to-end regression: parent compresses, detached delivery persists to
the stale id, and the next same-id run loads the delivery row from the
live tip and binds it.
2026-09-09 10:55:32 -07:00
liuhao1024 3db45bab6e fix(gateway): load declared-key history and deny wake for response-chain runs
Address the two /v1/runs blockers from re-review of #98619:

- P1: resolve the session identity (body id > response chain > declared
  X-Hermes-Session-Key > run_id fallback) BEFORE loading history, so a
  header-only declared-key run now loads its selected session's SessionDB
  history. A persisted async-delegation delivery row therefore reaches the
  next same-key run's context, which is what the wake opt-in promises.
- P1: a previous_response_id continuation consumes its ResponseStore
  snapshot and can never see a SessionDB delivery row (async completion
  persists to SessionDB only), so it is no longer granted wake_capable;
  delegate_task keeps its synchronous fallback on that branch until a
  merge contract exists for the chain.

Regressions: declared-key history load (delivery row consumed), no-row
declared key stays fresh, response-chain continuation denied wake with
snapshot history preserved, plain run still granted wake.
2026-09-09 10:55:32 -07:00
liuhao1024 dffc0fa2d5 fix(gateway): require wake-capable session provenance for background delegate_task (#98619)
Rebuilt against the post-refactor owners: the chat-completions route now
lives in gateway/platforms/api_server_openai_routes.py, the wake gate in
tools/delegate_tool_dispatch.py, and session_context.py was reshaped —
the original patch aimed at code main no longer has.

Header-less OpenAI-compatible clients get a fingerprint-derived session
id bound as the api_server chat_id. delegate_task's background gate
treated ANY bound session id as wake-capable and dispatched detached
subagents, but for derived ids the wake self-post lands in a session
whose history the client never reloads — the result is undeliverable by
construction.

Bind a wake_capable provenance flag at session-bind time, DEFAULT-DENY
at the central boundary (set_session_vars and _bind_api_server_session
both treat an omitted declaration as denied; a binder that says nothing
grants no wake authority): "1" only from audited producers whose client
can address the id again (explicit X-Hermes-Session-Id — 403-gated on
API_SERVER_KEY — native /api/sessions/{id} routes, /v1/runs); "" for
fingerprint-derived ids. The delegate gate requires the flag (fail-closed,
captured pre-child-construction alongside origin_wake_sid) and keeps the
forced-sync fallback with its honest note.

Reported-and-investigated-by: shojikumaru (Sho + Alpha) via #98619
2026-09-09 10:55:32 -07:00
Teknium 14791b4d4e simplify(compat): approval — drop 43 facade re-exports + _command_detection_variants late-bind seam, repoint 30 callers + 46 test files
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/
human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined
names directly (module-attribute reads on tools.approval_context so patching the defining module
still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied,
_denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval.
approval_detection calls its own _command_detection_variants instead of late-binding through the facade.
2026-09-03 13:49:57 -07:00
Teknium e83816a4d1 review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE 63279301bc non-test .py and absent on HEAD, the BASE comment/docstring block was re-attached at the HEAD location of the code it explained (matched by the distinctive code line / enclosing def). Sentences already covered by an existing HEAD comment were deduped; the issue number always survives. Insert-only: no code lines changed.
2026-09-03 09:44:26 -07:00
Teknium 7b6f60b4f9 refactor(gateway/platforms): runs/room — fold _emit_to_stream into _mark_run_event, unify room-grant error builder, inline sweep filters, tighten layouts 2026-09-02 19:46:51 -07:00
Teknium e42ced1314 refactor(gateway/platforms): runs — usage/owner-alive/idempotency-key collapse, inline approval envelope via _run_event, body blank-line squeeze 2026-09-02 19:33:07 -07:00
Teknium 0da4cbc25b refactor(gateway/platforms): runs/room modules — _RunLaunch carries agent kwargs, room-identity field table, _record reuse, comment/docstring compaction 2026-09-02 19:25:16 -07:00
Teknium a12a72314d refactor(gateway/platforms): runs — unify 202 admission/replay responses, scope hashing, AST-neutral layout packing across the four /v1/runs + room modules 2026-09-02 19:12:53 -07:00
Teknium 4acbbd1f2b refactor(gateway/platforms): api_server_runs — event dispatch table, approval check table, auth folded into _load_owned_run, contextvar reset list, transport closures moved onto _RunLaunch 2026-09-02 19:03:43 -07:00
Teknium a166aa73bb refactor(gateway/platforms): split _handle_runs, unify run teardown/terminal-event helpers, dedupe room-grant target checks and idempotency txn 2026-09-02 18:43:40 -07:00
Teknium 581d97e545 refactor(adapters/api_server): 10589->8871; extract OpenAI-compatible routes into api_server_openai_routes.py, dedupe SSE/response builders, drop dead idempotency acknowledge path 2026-09-02 14:06:32 -07:00
Teknium 7a86397a46 fix(api_server): fail closed on unstamped runs; claim session-chat-stream run owner (#93689)
Port the run-ownership invariants from PR #93747 onto main's `_run_owners`
model in gateway/platforms/api_server_runs.py:

- `_request_owns_run` no longer admits run state that exists without an
  owner stamp. Under gateway.multiplex_profiles every served profile holds
  a valid key, so the "backward compatibility" branch made the boundary
  allow-all whenever provenance was missing. Unstamped state now fails
  closed; only an in-memory owner match or a durable idempotency record
  under the caller's own scope admits a run.
- POST /api/sessions/{id}/chat/stream claims `_run_owners` at the run mint,
  inside the request's profile scope, so its run is confined to the
  creating profile like /v1/runs.
- Owner release is tied to "no run-keyed state survives"
  (`_release_run_owner_if_forgotten`) and runs at every retirement point
  (task finally, SSE stream close, both sweep loops, chat-stream finally),
  not only the terminal-status sweep — no stranded entries, no stateful id
  ever left unowned.

Docs: note that runs are per-profile scoped (replaces the now-false
visibility admonition proposed in PR #92822).

Fixes #93689
Fixes #90415
Supersedes #93747, #93704, #92822

Co-authored-by: RickyYii <237135932+RickyYii@users.noreply.github.com>
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
2026-09-02 06:17:47 -07:00
Teknium a2600740e8 feat(delegate): tag every subagent progress line with its batch id
Concurrent or nested delegation batches (a parent's 9-way fan-out plus a
child's own 3-way fan-out) printed interleaved `✓ [3/3]` / `✓ [3/9]` lines
with nothing identifying which batch each belongs to.

- CLI: batch header `🔀 [6a66] delegating 9 tasks`; completion lines and
  child tree-view lines become `[6a66 3/9]`; spinner remaining-count tagged.
- Relay: `delegation_id` rides on every `subagent.*` event (TUI gateway
  payload, api_server SSE subagent.start/complete).
- TUI: `[6a66 3/9]` prefix on /agents rows; Desktop Agents pane groups
  workers by exact delegation_id (heuristic shape/time grouping kept for
  older backends) and shows the tag on the group header.
- Tag = last 4 hex of the deleg_xxxxxxxx id (format_batch_tag), same id
  returned by the dispatch and used for cache/delegation/live/<id>/.
2026-09-02 01:06:24 -07:00
joaomarcos d63e5d8a10 fix(cache): source-qualify the peer identity and gate the declared bind
Both blockers from @andrexibiza's review of 28a2d7f0ee.

1. The generation lookup was not in the same identity domain as recovery.
   latest_conversation_boundary() selected on session_key alone, while
   _declared_conversation_session() is qualified by (source, session_key).
   X-Hermes-Session-Key accepts any authenticated caller-supplied string, so an
   API conversation may legally carry the same key as a Telegram row in one
   database -- a /new over there rotated this conversation's gwk_ generation
   while recovery correctly refused to cross the same line, moving the affinity
   identity out from under a physical identity that had not moved.

   The boundary read now takes (session_key, source), and the carrier is
   'source|key|generation' rather than 'key|generation' -- keying on the string
   alone would also collapse two same-key conversations from different sources
   onto one routing key, since this value leaves the process verbatim as
   OpenRouter's sticky session_id and xAI's x-grok-conv-id. The source comes
   from the agent's own session row, falling back to the platform the row will
   be created with before it lands.

2. The declared key's stated lower precedence did not survive settlement. Both
   handlers let stored_session_id / an explicit body session_id win, then called
   _bind_declared_conversation() unconditionally. record_gateway_session_peer()
   does SET session_key = ? across compression ancestors, so a request carrying
   conversation A's chain plus header key B silently rebound A to B: A could no
   longer be recovered by its own key, and B recovered A's session.

   Recording is now gated on the declared key having actually selected or
   minted the session, on both paths. Behind that gate the bind itself refuses
   to overwrite a row already bound to a different key, so a future caller
   cannot reintroduce the same defect by opting in wrongly.

test_declaration_outranks_the_lineage_root asserted the pre-qualification
contract by comparing a DB-backed agent against a DB-less one; it now makes the
stronger statement it was written for -- one declared conversation reached
through two different physical ids on the same peer.

Refs #96811

Found in review by @andrexibiza, whose analysis located each of these
defects and specified what a correct fix had to prove.

Co-Authored-By: Andrex Ibiza, MBA <andrexibiza@gmail.com>
2026-09-01 02:14:35 -07:00
joaomarcos 3739cf3b86 fix(api): resolve the declared conversation instead of minting a session per request
POST /v1/responses and POST /v1/runs parse and authenticate the client's
X-Hermes-Session-Key, pass it downstream for memory scoping, and then mint a
throwaway physical session id anyway whenever the client manages its own
history (no previous_response_id chain to carry one forward).

Every conversation-affinity hint Hermes sends is derived from that physical
id, so all four re-keyed on every single reply: prompt_cache_key on both
OpenAI-wire transports, the OpenRouter and Nous sticky session_id, and xAI's
x-grok-conv-id. The conversation never landed back on a warm prefix.

Fix the identity rather than the four consumers. The declared key resolves to
its live session through find_latest_gateway_session_for_peer -- the same
reset-fenced recovery every native gateway platform already uses -- and the
turn records the row it ended on through record_gateway_session_peer, which
AIAgent._ensure_db_session never did (it knows the key and writes the row
unkeyed, so the mapping the next reply needs did not exist).

Because the lookup is fenced on sessions.end_reason, the generation that must
rotate is already durable: session_reset (/new), session_switch, idle, daily,
suspended and resume_pending_expired all return None, so a new conversation
gets a new id and a cold affinity scope, and a retired generation can never be
resolved again. No counter, no new persisted field, and no new precedence rule
in the cache-scope resolver -- /branch, delegate and tool children keep the
isolation of #79161/#79017 byte for byte.

Precedence is unchanged where it already worked: an explicit body session_id
and the previous_response_id chain both still outrank the declared key, and a
request that declares nothing keeps its per-request id. Recording is opt-in
(bind_declared_conversation), so no other _run_agent caller's rows change.

Refs #96811

(cherry picked from commit e7c83dddf36784d1012bf483240ebc7f6b2ef9aa)
2026-09-01 02:14:35 -07:00
David Dudok de Wit e7433910e9 feat(bot-mode): add scoped cross-gateway Group Chat transport 2026-08-31 01:04:11 -07:00