Commit Graph

27 Commits

Author SHA1 Message Date
teknium1
dbcbd9d9db feat(gateway): /branch opens a sibling thread by default; --here keeps this chat (#66023)
On Discord, Telegram, Slack and Matrix a plain `/branch` used to rebind the
CURRENT chat/thread's session key to the clone, ending the original session
on that surface. The user could not keep the original path live while
exploring an alternate one — the opposite of what a branch is for.

Now the handler opens a sibling thread through the adapter's existing
`create_handoff_thread` BEFORE cloning (a failed create never orphans a
branch row), binds the thread's own session key to the clone with the
thread's routing columns written at create time, and leaves the origin key
untouched. `/branch --here` keeps the legacy in-place switch; platforms
without threads, DMs, unknown Discord parents and adapters that cannot open
a thread fall back to in-place with a one-line note. The CLI strips the
flag through the same parser so `--here` never becomes a session title.

Destination source shapes mirror each adapter's inbound key (Discord keys
threads on their own id; Telegram/Slack/Matrix on the parent chat), the
same rules the CLI->platform handoff uses.

Live repro (real gateway + real Slack adapter against a stand-in Slack
Socket Mode/Web API): base ends the origin session and rebinds its key;
fixed posts the thread seed, replies "this chat stays on it", the origin
thread keeps its session and the follow-up typed in the new thread lands on
the branch (parent_session_id = origin).

Design and first implementation by Angello Picasso (#66014, #66024);
this is a slim port onto the split slash_commands_* layout.

Co-authored-by: Angello Picasso <angello.picasso@devsu.com>
2026-09-20 13:37:10 -07:00
teknium1
52b649991e fix: route the curator review fork and gateway /compress agent through resolve_reasoning_config
Two more production AIAgent() sites (same class as #85153) never passed
reasoning_config, so agent.reasoning_effort was ignored there. The /compress agent
uses the gateway's session-aware resolver (session /reasoning > per-model > global),
the curator the shared chokepoint against its review model. Also pins the Feishu
comment-agent sibling shipped earlier on this branch with a test.
2026-09-19 09:39:50 -07:00
teknium1
42d142ba41 refactor(gateway): slash_commands_session resolves adapters through the intake/delivery seams
Mechanical migration of the `_adapter_for_source` call sites in this sibling. Intake policy sites take `_intake_adapter_for`; every send/edit/typing/pending-slot site takes `_delivery_adapter_for`. Part of #88715 (phase 4).
2026-09-19 01:28:23 -07:00
kshitijk4poor
62661023f3 fix(gateway): an empty session id must not reset every task's read dedup
reset_file_dedup treats a falsy task_id as "all tasks"; mirror the existing
`or "default"` guard from the hermes-mode /compress site at the three forwarded
sites, and shrink the run_turn comment to the one non-obvious fact.
2026-09-19 00:31:26 +05:30
kshitijk4poor
1b4821c967 fix(gateway): codex manual /compress resets the live session's read dedup too
Sibling site of the previous commit: _compress_codex_app_server_session passed
no task_id either, so a manual /compress on a codex_app_server session reset the
"default" bucket and left the live session's read_file dedup armed. Forward the
session row id; one parametrised test covers both codex entry points through
the real compress_context against the real read tracker.
2026-09-19 00:31:26 +05:30
teknium1
09cf1b926f fix(state): reset forks leave the Python lineage walk too; /resume ranks lineages by activity
The SQL chain step (#114287) stopped a `_reset_from` child of a compression-ended parent
from winning tip projection. The Python twin had the same blind spot:
`_is_compression_child_row` / `_compression_lineage_root` treated the reset fork as a
continuation, so `get_compression_lineage(tip)` collapsed to `[tip]` (ancestors lost for
prompt-cache scope and export) and the fork shared the lineage's turn-lease key. Both now
ask `_is_explicit_fork_child_row(include_reset=True)`; `get_compression_lineage`'s own
early return keeps excluding only branch/delegate/tool so a reset child that later
compresses still walks forward to its children.

Gateway bare `/resume` lists with `order_by_last_active=True`: a lineage compressed for
days is projected onto its live tip and belongs where the user last touched it, not at
its root's `started_at` (the reporter's tip, active yesterday, was buried under a
September-12 start). Desktop already requests `order=recent`.

Docs: `/resume` row in slash-commands reference. Tests: one lineage-walk invariant, one
/resume ranking invariant, both red on origin/main.

Part of #114271
2026-09-18 10:39:13 -07:00
teknium1
b002dfc04d fix: pruned skill_view/read_file results reload after a proactive prune
A committed proactive tool-result prune (`prune_tool_results_only`, driven from
`agent/turn_preflight.py::compress_after_tool_results`) demotes old skill_view and
read_file bodies to one-line markers, but only the full-compaction path called
`_reset_read_dedup_caches`. The repeat-view dedup therefore kept answering
"unchanged / content_returned: false" for content that no longer existed in the
transcript, so the `[SKILL_PRUNED: ... reload with skill_view(...)]` marker asked
for a reload the tool then refused (#112763). Treat the committed prune as the same
content-loss boundary compaction already is: reset the task's read/skill dedup right
where the pruned list is committed.

Sibling surface: manual `/compress` on CLI, TUI and the messaging gateway called
`compress_now()` without `task_id`, so the boundary reset hit the "default" bucket
instead of the session's. Pass the session-scoped task id on all three.

Test double in tests/hermes_cli/test_cli_manual_compress.py gains the `task_id`
kwarg the real `_compress_context` facade already accepts.

Supersedes #103268 (@jo0wz), which reached the same path with a process-global
ghost registry (not task-keyed, skill_view only).
2026-09-16 17:23:44 -07:00
teknium1
4003840378 fix(gateway): /save delivers the export document instead of crashing on get_adapter
`GatewayRunner` never had a `get_adapter` method, so every gateway `/save`
(Telegram, Discord, ...) rendered the file and then failed with
"'GatewayRunner' object has no attribute 'get_adapter'". Resolve the adapter
through `_adapter_for_source`, the profile-aware lookup the rest of the runner
uses, so multiplex secondaries deliver through their own bot rather than a
missing key on the default map.

Co-authored-by: pierrenode <298902573+pierrenode@users.noreply.github.com>
Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
Co-authored-by: Baophan00 <109447498+Baophan00@users.noreply.github.com>
2026-09-13 14:45:10 -07:00
teknium1
b5f072e2ce fix(agent): manual /compress "nothing to compress" gate stays gateway-only
The shared core applied `has_content_to_compress(head) is False -> nothing_to_do`
on every surface, where origin/main only had it in the gateway handler. That
predicate only knows the local summarizer's window: on CLI/TUI/ACP,
`_compress_context(force=True)` still routes codex_app_server sessions to native
compaction before any local-compressor check, and `ContextCompressor.compress`
commits the phase-1 tool-result prune / blank-echo drop even when no summary
window exists -- so the gate wrongly skipped real work there. It is now an opt-in
`skip_without_window` that only the gateway passes, restoring each surface's
prior behavior.

Review follow-up on #109610.
2026-09-13 05:20:26 -07:00
teknium1
7114da3de6 refactor(agent): manual /compress runs through one core with --preview/--aggressive on every surface
CLI, gateway, TUI and ACP each re-sequenced the same chain (partial split -> estimate ->
_compress_context(force=True) -> lock-skip detection -> rejoin tail -> summary), and the
flag set differed per surface: TUI treated `--preview` as a focus topic, ACP ignored
arguments entirely. For the one command that legitimately breaks the prompt cache that
divergence is a correctness problem, not a style one.

`agent/conversation_compression_manual.py::compress_now` owns the sequence; surfaces parse
their own argv, install `after_messages`, re-anchor session ids and render. TUI and ACP gain
`--preview`, `--aggressive` refusal and `here [N]` parity.
2026-09-13 05:20:26 -07:00
Teknium
77180acc2c fix(gateway): mid-turn authorization reads the admitting bot's allowlist; shared-bot satellites keep a transport
Inside a routed satellite's turn the ambient scope is the satellite's, whose
.env has no token or allowlist. Five sites still called `_is_user_authorized()`
directly there — `/topic`, the sibling-thread `/stop` grant, plugin message
injection, Discord voice transcripts and startup auto-resume — so the shared
bot's owner was refused ("not authorized to use /topic") and a satellite that
DID copy an allowlist widened who may drive those commands. They now go through
`_is_user_authorized_for_source`, and `_under_authorization_profile` derives the
transport home from the delivering adapter's owner when ingress did not stamp
one (restored/cached sources).

`_adapters_for_profile` (the resolver behind `_authorization_adapter`,
`_adapter_for_source` and now `_resolve_injection_adapter`) returns the primary
map for a shared-bot satellite: a served profile with the `{}` startup
placeholder, no reconnect pending, targeted by a default-bot route. Kanban and
cron already applied that rule; the gateway's own resolvers returned None, so
heartbeats, process completions, goal notices and delegation results for such
profiles were undeliverable after a restart. A secondary that owns a credential
(connected on any platform, or queued for reconnect) still fails closed.
2026-09-11 15:28:00 -07:00
Teknium
75ae2859b9 fix(gateway): default-profile rows and /undo eviction stay on the profile that owns them
Two multiplex key/store mismatches in the gateway session layer:

- SessionStore._db_for_key resolved legacy agent:main keys to the AMBIENT
  store. Named-profile keys were already pinned to profiles/<x>/state.db
  (#97309), but a default-profile session touched inside a secondary's
  runtime scope (a coalesced async-delegation drain, a cron mirror into a
  default chat, a scoped watcher) was written into the secondary's state.db
  — the profile_name='<A>' row inside B's store reported in #102157, after
  which the routing index and row disagree and the #54878 self-heal drops a
  live conversation. Default keys now resolve to _routing_home/state.db, the
  launch home already pinned for the routing index. Single-profile gateways
  (multiplex off) keep the ambient store byte-for-byte.

- /undo evicted the cached agent under build_session_key(source) —
  agent:main:… — while the cache is keyed by the profile namespace, so on a
  secondary profile the eviction missed and the next turn reused an agent
  whose in-memory history still held the undone turns. Use
  _session_key_for_source like /reset and /retry.

Addresses #102157.
2026-09-11 15:26:09 -07:00
kshitijk4poor
ab2f4602de refactor: MessageEvent to gateway/platforms/event.py; ElicitationHandler takes a call_context thunk
Breaks the two import cycles that forced Protocol stand-ins in the F821 sweep, so the two
sites now name the real types.

gateway/platforms/event.py (new leaf): MessageType, ProcessingOutcome, MessageEvent moved
out of base.py verbatim. Their only dependency is gateway.session.SessionSource; base.py
imported helpers.py at module level, so helpers could not name MessageEvent. Now
TextBatchAggregator is typed by the real MessageEvent. 249 importers repointed
(`from gateway.platforms.base import` -> `.event`, preserving each import's layout);
gateway.platforms.__init__ re-exports from .event. The three revert-scheduled PLUGIN-COMPAT
pointers that named these symbols (gateway.slash_commands → MessageType, dingtalk → MessageType,
photon → ProcessingOutcome) and their COMPAT_MANIFEST rows now target gateway.platforms.event.
Docs updated: ADDING_A_PLATFORM.md, adding-platform-adapters.md (en + zh-Hans).

tools/mcp_tool_sampling.py: ElicitationHandler no longer holds a back-reference to its
MCPServerTask (mcp_tool imports sampling, so the task type cannot be named there). It only
ever read owner._pending_call_context, so it takes `call_context: Callable[[], Context | None]`
and MCPServerTask passes `lambda: self._pending_call_context`. The consent call is one
`functools.partial`, run directly or inside the captured Context.

ty on the 11 touched production files vs origin/main: 0 new diagnostics, 14 resolved.
(The one `source: SessionSource = None` diagnostic moves with the class; typing it Optional
exposes ~60 unguarded call sites — separate follow-up.)

Tests: tests/gateway + tests/plugins + tests/tools + touched files, 18,235 passed; the 31
failures reproduce identically on origin/main (macOS /private/tmp, systemd socket,
long-path fixtures, live-service tests).
2026-09-07 22:47:33 +05:30
Teknium
2031c819fe simplify(compat): gateway — re-land 92d0bd0d73 (reverted by stale-index commit b818085298)
Re-applies the gateway compat removal byte-for-byte; see 92d0bd0d73 for the
full inventory (30 re-exports/aliases + 2 shim modules dropped, 3 shim-only
names re-removed, 24 callers + 34 test files repointed). No new changes.
2026-09-03 13:12:50 -07:00
Teknium
b818085298 simplify(compat): doctor/status — drop 13 re-exports + the doctor_* globals() facade (97 names), repoint 6 callers / 13 tests 2026-09-03 13:10:52 -07:00
Teknium
92d0bd0d73 simplify(compat): gateway — drop 30 re-exports/aliases + 2 shim modules, re-remove 3 shim-only names, repoint 24 callers + 34 test files
Per COMPAT_REMOVAL.md (internal import paths are not a stable API):

Re-exports removed
- gateway/run.py: atomic_json_write, load_dotenv, resolve_delivery_transport,
  TurnRunner, merge_pending_message_event, _arm_loop_floor_timer,
  start_loop_liveness_watchdog, DEFAULT_GATEWAY_POST_INTERRUPT_GRACE_TIMEOUT,
  _UNSET (9) — run_* mixins and tests now import from the defining module
  (gateway.delivery / gateway.run_turn_runner / gateway.platforms.base /
  gateway.shutdown_watchdog / gateway.restart / utils).
- gateway/session.py: SessionResetPolicy, normalize_whatsapp_identifier,
  TranscriptReadError, auto_continue_freshness_window (+ "_now & co." noqa
  facade) — gateway/__init__ takes SessionResetPolicy from .config; callers
  take TranscriptReadError from gateway.session_transcript.
- gateway/kanban_watchers.py: _wake_scope_id + "tests import via origin" noqa
  facade; tests import from kanban_watchers_common / _notifier.
- gateway/slash_commands.py: _model_switch_skew_guard, HISTORY_UNREADABLE.
- gateway/stream_consumer.py: escape_code_fences_for_display.
- gateway/platforms/api_server.py: "re-exported" RunIdempotencyStore comment;
  tui_gateway + tests import gateway.platforms.api_server_run_idempotency.
- gateway/platforms/__init__.py: PEP 562 __getattr__/__dir__ lazy QQAdapter /
  YuanbaoAdapter facade (no in-tree importer).
- gateway/startup_watchdog.py: whole re-export shim module deleted; the three
  in-tree callers import hermes_startup_watchdog directly.

Aliases removed
- gateway/platforms/signal.py: SignalAdapter._markdown_to_signal.
- gateway/shutdown_forensics.py: _parse_systemd_duration_to_us.
- gateway/platforms/yuanbao.py: OutboundManager.start_slow_notifier /
  cancel_slow_notifier / get_chat_lock / _chat_locks / CHAT_DICT_MAX_SIZE
  delegates; module-level get_active_adapter / send_yuanbao_direct;
  MarkdownProcessor has_unclosed_fence / ends_with_table_row /
  split_at_paragraph_boundary static pass-throughs (chunk_markdown_text stays —
  it carries yuanbao's chunking policy). tools/send_message_senders +
  tools/yuanbao_tools call YuanbaoAdapter.get_active() / sender.send_direct().

Shim-only names re-removed (earlier review-fix round 96c104c903)
- CapabilityDescriptor.from_platform_entry (+ tests/gateway/relay/test_descriptor_from_entry.py)
- SessionTurnLeaseRegistry.__len__ (+ its test)
- is_relay_media_url KEPT: download() uses it, real internal helper.

Tests that pinned a shim (startup_watchdog re-export identity, api_server
RunIdempotencyStore identity, signal wrapper parity) are dropped; tests that
pinned live behavior are repointed at the implementation.

Note: the gateway/run.py hunk of this change was swept into 00a3cfe5c9 by a
concurrent commit on the shared worktree; this commit carries the rest.

Verified in an isolated worktree (HEAD + this change only): ruff clean,
import-smoke of all 33 touched modules under a fresh HERMES_HOME,
605 passed / 1 skipped across the 39 covering test files.
2026-09-03 13:10:49 -07:00
Teknium
9e3df66e95 review-fix(locks): keep the lock-free cache read only for manual codex /compress (BASE exception)
BASE's _compress_codex_app_server_session was the single cache-peek site that read
_agent_cache without a lock when _agent_cache_lock was None (tests/gateway/
test_codex_hygiene_compaction.py::_slash_host relies on it). Every other BASE caller returned
None without a lock. Model that as an explicit lockless_fallback=True at that one call site.
2026-09-03 12:42:38 -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
0071ba9965 Merge origin/main (561b053f79) into simp/forwardport: forward-port 220 main commits into the simplified tree 2026-09-03 03:31:03 -07:00
Teknium
4987f27fde refactor(gateway): slash_commands_session — inline single-use lifecycle-hook helper, compact docstrings, fold calls 2026-09-03 00:07:29 -07:00
Teknium
e530f5936f refactor(gateway): stream_consumer*/slash_commands* — hug closing brackets (AST-identical) 2026-09-02 23:42:01 -07:00
Teknium
901796093b refactor(gateway): fold long string literals and dict/call layouts in slash_commands_session/model
Implicit-concatenation string literals re-wrapped to 100 cols (byte-identical values, verified
by AST constant diff vs base); remaining fitted calls folded. Group now -25.2% LOC.
2026-09-02 19:55:39 -07:00
Teknium
559714fb18 refactor(gateway): compact slash_commands_session/model — fold calls, inline single-use helpers, tighten docs
- Fold parenthesised multi-line calls that fit in 100 cols; inline _switch_and_commit,
  _reasoning_picker_choices, _configured_display_context, listing_kwargs (single-use).
- Collapse getattr-guarded calls on always-present mixin methods (_is_user_authorized,
  _schedule_telegram_topic_title_rename); next()-based lookups; contextlib.suppress.
- Docstrings/comments compacted by hand — every WHY (IDOR guards, cache/rotation
  invariants, off-loop rationale) kept; narrative and issue-number history dropped.
- Differential parity vs base: 9216 traced cases for /new,/title,/undo,/branch;
  12000 for the resume ownership guards; /fast + /reasoning tables; listing goldens.
2026-09-02 19:40:07 -07:00
Teknium
f7c5b16fcc refactor(gateway): break /model, /new, /compress, /resume god methods into phase helpers
- slash_commands_model: _ModelSwitchContext dataclass replaces 12-kwarg plumbing;
  _handle_model_command (193 LOC) -> listing / switch / guard / commit phases;
  _commit_model_switch (175) -> cached-agent swap / record / confirmation;
  /fast and /reasoning display toggles routed through dict tables.
- slash_commands_session: /new hooks + cleanup lifted; _handle_compress_command_inner
  -> _run_manual_compression; /resume -> resolve target / access-denied helpers;
  persisted-row ownership proof extracted (_persisted_row_proves_owner, _sattr).
- Zero user-visible change (golden + differential fuzz vs base for guards, listing,
  /fast, /reasoning); test net 137 files / 1842 passed.
2026-09-02 19:04:09 -07:00
Teknium
2e9a5e9f32 refactor(gateway/slash): collapse /whoami, /platform, /stop, /footer, /verbose duplication; extract /compress preview; module docstrings 2026-09-02 16:41:41 -07:00
Teknium
59acca1850 refactor(gateway/slash): unify context-breakdown transcript load; tidy module headers and import order 2026-09-02 16:30:01 -07:00
Teknium
04fc5ea9ee refactor(gateway/slash): extract session-transcript commands into gateway/slash_commands_session.py (GatewaySessionCommandsMixin) 2026-09-02 16:03:07 -07:00