Commit Graph

5506 Commits

Author SHA1 Message Date
Maciej Lisiewski
c57fbf4584 ci: fix remaining ASYNCXXX linter errors 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
bda3aea496 ci: remove unused variables (linter) 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
7ef490ed3b ci: fix PLC0206, PLC0414 linter errors 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
0006a5e229 ci: fix PLW1509, PLW1510, PLW2101 linter errors 2026-10-10 15:20:13 +02:00
mnajafian-nv
4c7ab8b2a0 fix(compression): name the automatic trigger and effect in attempt telemetry
The content-free attempt record is the one place that says why a
compaction ran and what it did, but it could not answer either question:

- Every automatic caller passed no trigger, so idle, turn-start
  threshold, engine preflight, pre-API, post-tool and gateway hygiene
  attempts all logged trigger_source=auto. Each call site now passes its
  own label; the shared metric still buckets them as auto.
- tokens after, message counts and how the summary was made were
  computed and logged as prose but never reached the record. A finished
  rewrite now records messages_before/after, tokens_before/after,
  tokens_reclaimed (signed; a summary can grow a short transcript),
  token_count_method=estimate_rough, method (llm_summary,
  aux_fallback_main, deterministic_fallback), items_dropped, and the
  tool_results_pruned / reasoning_items_pruned pass counts.
- Attempts that never reached the local compressor returned silently:
  the automatic breaker gate (before and under the lease), every Codex
  app-server exit, a lease that found the session already rotated, and
  an unreadable durable cooldown. Each now logs one record with a
  commit_status (blocked, skipped, failed, aborted, committed), a route,
  and a class such as blocked:cooldown. These stay out of shared metrics,
  which never counted them.

Test doubles for _compress_context gain the trigger keyword the call
sites now pass.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 1795993d1a01d91dd76e6ec824cb606f8a03b897)
2026-10-10 04:27:27 -07:00
Teknium
a274d3388f fix: core modules import again on Python 3.11-3.13 (re-quote eager forward references)
#135348 ran ruff's UP037 autofix with ruff.strict.toml's target-version = "py314". On 3.14
annotations are lazy (PEP 649), so stripping quotes is safe there; on 3.11-3.13 a function or
variable annotation is evaluated when the def or statement runs. Every unquoted annotation that
names the enclosing class (`def clone_for_agent(self) -> ContextEngine`), a class bound later in
the module, or a TYPE_CHECKING-only import now raises NameError on import: 300 modules on main
(gateway.config, run_agent, hermes_state, cron.jobs, the Telegram adapter, ...) fail to import
under 3.11, the update path crashes mid-`hermes update` on any install whose interpreter is
older than 3.14, and the e2e-upgrade suite's retry leg fails with
`NameError: name 'ContextEngine' is not defined`.

- Re-quote exactly the 79 annotations (36 files) that name something unbound when they are
  evaluated (including tui_gateway/server.py's `_TurnScopes`, which only reaches that module's
  globals through register() after import); no other text changes.
- agent/turn_scripted_prelude.py: `Generator[...]` gets its third argument back. d76cce4f11
  applied UP043 (default type args, valid only from 3.13) to a module-level alias, so
  `Generator[Y, S]` raises TypeError on 3.11/3.12 and takes gateway.run, agent.conversation_loop
  and tui_gateway.{server,ws,entry} down with it.
- ruff.strict.toml target-version = "py311", the requires-python floor. With it UP037 leaves
  these quotes alone and only strips the two that are genuinely redundant, which this commit
  strips; any future version-gated fix stays valid on every supported Python.
- tests/ci/test_strict_ruff_target.py pins the target to the requires-python floor (red on main).
2026-10-09 14:25:07 -07:00
Maciej Lisiewski
2ff58849f4 ci: ruff UP037 fixes 2026-10-09 17:34:38 +02:00
Maciej Lisiewski
9eab688b42 ci: ruff UP017 fixes 2026-10-09 17:34:36 +02:00
Maciej Lisiewski
aff64c3eeb ci: UP007, UP022, UP028, UP031, UP041 linter fixes 2026-10-09 17:31:52 +02:00
Maciej Lisiewski
32b70e9733 ci: FURB188, FURB192 linter fixes 2026-10-09 17:31:52 +02:00
Maciej Lisiewski
4278b6aa24 ci: RUF013, RUF015 2026-10-08 21:29:48 +02:00
Maciej Lisiewski
74f1f17a7e ci: fix RUF017, RUF018, RUF022 2026-10-08 19:54:07 +02:00
Maciej Lisiewski
b4e436c115 ci: don't cast int to int 2026-10-08 19:28:23 +02:00
Maciej Lisiewski
aa2d120cba ci: prefix unused variables with underscore (RUF059) 2026-10-08 19:22:03 +02:00
Maciej Lisiewski
d76cce4f11 ci: apply all automatic linter fixes available 2026-10-08 17:22:54 +02:00
teknium1
dacb06ea93 feat(plugins): show on_session_finalize return messages to the user
A plugin's on_session_finalize hook can now return a str or {"message": str}
and Hermes shows it at the session boundary without starting a turn or
rewriting the model reply:

- CLI /new: printed before the new session; CLI quit: printed above the
  exit summary; one-shot -q/-z: printed to stderr (stdout stays the answer)
- TUI: session.close returns the messages; /new shows them as system lines
- Gateway /new and shutdown: sent to the owning chat via the adapter, while
  adapters are still connected

on_session_end stays observer-only: it fires after every turn, not at a
session boundary. Gateway finalize dispatch moves to
gateway/run_shutdown_session_end.py.
2026-10-08 04:11:24 -07:00
Teknium
b1a39a1b1b refactor(approval): one session /yolo contract for CLI, TUI/Desktop and the messaging gateway
The persist / restore / carry logic for session YOLO had grown three copies
(the CLI mixin, tui_gateway, the gateway runner), each with its own key choice,
ordering and error handling. tools/approval_yolo.py now owns it:

- toggle_session_yolo: flip or set, persist BEFORE the live flip (a racing
  restore never revives an OFF), stored-copy-aware so a toggle after a restart
  turns it OFF; a failed write logs a warning and never blocks the flip.
- restore_session_yolo: re-arm a persisted flag in a fresh process; skipped
  under the frozen process --yolo.
- transfer_session_yolo: move the live flag to a continuation id.
- with_session_yolo: a row being created records a live flag (lazy first row,
  compression child, /branch).

Each surface keeps only its storage: the session row for CLI/TUI/Desktop, the
routing entry for the gateway. Removed: HermesCLI._transfer_session_yolo /
_persist_session_yolo, GatewayRunner._restore_session_yolo, the TUI-only
restore helper, and the TUI's post-publish set_session_yolo carry (the
compression publish now writes the child row with the flag).

Closes a gap the unification exposed: a CLI manual /compress that rotated the
session dropped YOLO (the next dangerous command prompted, and a --resume of
the continuation came back with it off).
2026-10-08 00:49:32 -07:00
Teknium
61b7f957ba fix(plugins): uninstall on Windows succeeds while the gateway has the plugin loaded
With the live-gateway refusal reverted (#134824), `hermes plugins uninstall`
on Windows still failed with [WinError 5] whenever the running gateway had
the plugin loaded with a file open or a native .pyd mapped: Windows will
neither delete such a file nor rename its directory. The command exited 1
and left a half-deleted plugin.

Removal now deletes what it can. When a held file stops the delete, the rest
of the tree moves out of plugins/ into <HERMES_HOME>/cache/plugin-pending-delete,
which works for mapped DLLs. When the directory itself is held and cannot move,
removal deletes its manifests so the plugin can no longer load. The install
record is dropped in both cases, and the gateway's next start sweeps what was
parked. POSIX is unchanged: a PermissionError still raises.

Live proof: tests/hermes_cli/test_plugin_uninstall_windows_live.py (a real
gateway with plain, open-handle and native-.pyd plugins) fails 2 of 3 on the
revert branch and passes 3 of 3 with this change on windows-latest.
2026-10-07 17:13:12 -07:00
ethernet
bc2e4d3773 Merge pull request #134209 from NousResearch/sid/onboarding
feat(onboarding): first-run setup chat
2026-10-07 19:17:15 -04:00
Teknium
889eddcd86 fix(gateway): /yolo survives a gateway restart
/yolo on a messaging platform only flipped tools.approval's in-memory
set, so any gateway restart (hermes update, crash, systemd restart,
Desktop reopen) silently put the chat back on manual approvals while
the user still believed YOLO was on.

The flag now lives on the chat's routing entry (SessionEntry.yolo,
persisted in gateway_routing like the /model override). /yolo writes it
before flipping the live flag, materializing the entry for a
first-message toggle; every turn re-arms the live flag from the entry it
already resolved; switch_session carries it across compression-tip and
binding repoints; the conversation-boundary security funnel (/new,
/resume, /branch, auto-reset) clears the persisted copy with the
in-memory one so a restart can never revive a bypass the user dropped.

Also corrects the security docs, which still claimed /yolo sets
HERMES_YOLO_MODE.
2026-10-07 16:15:35 -07:00
liuhao1024
6dc79b2ad8 fix(gateway): stop _ensure_windows_gateway_venv_imports leaking PYTHONPATH into global environment
Remove the global os.environ["PYTHONPATH"] assignment from
_ensure_windows_gateway_venv_imports(). The sys.path manipulation
is sufficient for the current process, and all Hermes child-process
spawners (code_execution_tool, gateway_windows, watcher) already
build their own PYTHONPATH explicitly in subprocess-specific env dicts.

Setting PYTHONPATH globally poisons every non-Hermes Python process
launched from a Desktop session (Unsloth, ComfyUI, etc.), causing
version conflicts and GPU initialization failures.
2026-10-07 17:19:01 -04:00
kshitijk4poor
f2cd946a19 fix(onboarding): /initiate-setup names the user's profile on every surface
The TUI builder passed primary_profile(launch_home), which skips the setup
profile; the gateway and CLI builders passed profile_name_for_home(HERMES_HOME),
so /initiate-setup run inside the setup profile told the model to hand off to
the setup profile itself. Use primary_profile on all three surfaces.

Probe (real setup_profile, sandboxed root): HERMES_HOME=default -> default,
work -> work (unchanged); setup profile: old 'hermes-setup', new 'default'.
2026-10-08 00:41:21 +05:30
alt-glitch
2c8df45ee5 Merge remote-tracking branch 'origin/main' into sid/onboarding
# Conflicts:
#	agent/conversation_loop.py
#	tui_gateway/prompt_turn.py
2026-10-07 19:29:35 +05:30
kshitijk4poor
844e514e67 fix(cron): drop a departed profile's store state by its resolved store path
forget_homes rebuilt each store path from a departed home KEY
(Path(key) / "cron"). cron/AGENTS.md forbids rebuilding a home from a key
(hermes_home_key normcases), and the realpath only follows a symlinked cron/
while the home still exists: a profile whose home was deleted kept its degraded
record, holding the host-wide hermes.cron.store.writable gauge at 0.

register_ticked_homes now records each home's resolved store (new public
store_health.store_key) while the home exists and hands the departed stores to
forget_homes. gateway/cron_store_notices.py uses the same store_key against
record.store (already resolved), so one normaliser decides "is this the served
home's store" everywhere.

Gate r1 (PM12, 2c + quality + 2ab toothless) Low finding.
2026-10-07 18:20:07 +05:30
kshitijk4poor
0a8aee587d fix(gateway): send cron store notices for a symlinked cron/ directory
Follow-up to #133283 (greptile review). Store records are keyed by the
resolved store path, but the notice sender rebuilt the owning profile from
that path's parent. A profile whose cron/ is a symlink to another disk has
a foreign parent, so neither its outage nor recovery notice reached any
home channel; forget_homes had the same mismatch and never dropped such a
departed profile's record.

Both now match by resolving each served home's own cron/ path.
2026-10-07 18:20:07 +05:30
Teknium
f55f8d4e4c fix(gateway): decode the re-entered .cmd launcher directly; keep the base64 form untouched in the test
The exec row rebuilt an `import base64; exec(base64.b64decode(...))` string only to feed it back
through the base64 row; decoding the payload directly is shorter and leaves no line that reads like
the base64+exec payload signature the supply-chain scan (rightly) refuses. The test derives the two
re-entry forms from the existing launcher forms instead of re-spelling the cmd-launcher source.
2026-10-07 04:39:00 -07:00
Teknium
33764c64d3 fix(gateway): a gateway re-entered through a dependency-syncing launcher is a gateway
When a launch through the published -c launcher (POSIX shell or Windows .cmd) syncs dependencies
first, venv_sync.relaunch_command re-execs it as `python -I -c "...; sys.argv = [...]; exec(<launcher
source literal>)"`. _BOOTSTRAPS covered the run_module/run_path re-entries but not this exec branch, so
the gateway it started was invisible to every canonical reader: live_gateway_pid_for_home, the fleet
restart, the stale-survivor sweep and update inventory. The new row matches the launcher in place by its
own anchors (readers normalize the literal escapes, so decoding it cannot work) and recurses into the
base64 row for the .cmd form. Extends the #124318 identity table and its test.
2026-10-07 04:39:00 -07:00
Teknium
6cc1efd8f9 feat(voice): auxiliary.voice_chat routes spoken voice turns to their own model
Voice replies had to come from the session's main model, so a heavy model made every spoken
answer slow (#83894). A new auxiliary slot, voice_chat, picks the model for voice turns:

- CLI/TUI voice mode, the Desktop voice conversation (prompt.submit voice_turn) and messaging
  voice notes mark the turn; agent/voice_turn_route.py binds the route after the prompt, row and
  preflight compaction were settled against the main model and undoes it before memory sync and
  background review, so the next typed turn and the review run on the main model.
- A turn larger than the voice model's compression threshold stays on the main model with a
  one-time notice: switching to voice never compacts the conversation.
- Usage lands in session_model_usage under task voice_chat; it never rewrites the session's
  recorded model/route, so resume keeps the model the user picked.
- Slot appears in Desktop Settings -> Models, the dashboard, and `hermes model`; GPT-Live
  delegations keep using the session model.
2026-10-07 01:35:11 -07:00
kshitijk4poor
77bf0d7f73 refactor(gateway): one drain predicate for the fire gate and the adapter
Lead fold from the independent review (quality + thermo + 2c + efficiency
lenses, all Low): the fire startup gate and
APIServerAdapter._gateway_is_draining now share runner_is_draining, so a
future drain flag cannot be added to one and missed by the other; the gate
returns early on no runner instead of looping on an unchanging guard. The
cold-adapter test closes its loop and joins its thread, the slow-verify
test keeps the same ordering at a fifth of the wall-clock, and the Chronos
contract doc lists the startup 503.
2026-10-07 13:43:19 +05:30
kshitijk4poor
932d7d40d1 fix(gateway): a fire that waited on startup is refused when the gateway finishes starting into a drain
The startup wait exited as soon as _running turned True. A restart drain keeps _running True
(run_shutdown sets _draining and deliberately leaves _running), and the handler's own drain check
ran before the wait, so a fire whose wait ended in a drain was claimed and run anyway. The gate
now checks the drain flags on every poll before it accepts readiness.
2026-10-07 13:43:19 +05:30
kshitijk4poor
f0d22a8c07 refactor(gateway): the fire's runner lookup, startup gate and live adapters resolve in the fire-startup sibling
The code-health ratchet holds gateway/platforms/api_server.py (over the 2k target) at its size on
main, and the startup gate's follow-ups grew it by 7 lines. The handler's runner lookup, startup
gate and live-adapter snapshot now resolve in one call, live_adapters_once_started() in
api_server_fire_startup, which returns (refusal, adapters). The facade ends below main (4619 vs
4621). No behaviour change.
2026-10-07 13:43:19 +05:30
kshitijk4poor
05d0a910d4 fix(gateway): a fire waiting on startup is refused at once when the gateway starts draining
A gateway that is stopped during boot never reaches _running, so a fire parked in the startup
wait held its drain reservation for the full budget before refusing, and stalled the shutdown
drain with it. The wait now exits on _draining / _external_drain_active with the same retryable
503 it returns on timeout.
2026-10-07 13:43:19 +05:30
kshitijk4poor
500ad4decc fix(gateway): the fire startup budget runs from handler entry, so a slow token verify cannot outlast the forwarder
The 7 s startup wait was measured from the gate, which runs after the JWT verify. A slow JWKS
fetch (sync verifiers run off-loop and may hit the network) plus a full 7 s wait could exceed the
dashboard forwarder's 10 s timeout: NAS then sees a 503 and retries while this handler still
claims and runs the job, so it runs twice. The handler now stamps its entry time and the gate's
deadline is measured from it.
2026-10-07 13:43:19 +05:30
kshitijk4poor
ae2ed3583e style(gateway): split the fire startup refusal onto house-style lines and keep the live-adapters WHY
The startup gate's refusal was a one-line walrus `if ...: return refusal`, unlike every other
early return in the handler, and the comment explaining why the fire needs the live adapters
(E2EE / relay-fronted platforms have no native credential) was dropped with it. No behaviour
change.
2026-10-07 13:43:19 +05:30
IAvecilla
733ee1e96a fix(gateway): the fire startup wait stays under the dashboard forwarder timeout and retries after 60s
(cherry picked from commit e5d54c20ec719dafaac88c7469f34f02c5eec462)
2026-10-07 13:43:19 +05:30
IAvecilla
660427e6be refactor(cron): fold the fire startup gate into one function and drop the timing assertion
(cherry picked from commit 3c52910d5726901e41da4ea6e71a3567308c4b5a)
2026-10-07 13:43:19 +05:30
IAvecilla
e574b02570 fix(gateway): a Chronos fire that lands during startup waits for the gateway's adapters instead of running with none
(cherry picked from commit cd1314e0ed830a8b5af689297dde4a434276168e)
2026-10-07 13:43:19 +05:30
alt-glitch
17646902e1 Merge remote-tracking branch 'origin/main' into sid/onboarding 2026-10-07 13:28:50 +05:30
kshitijk4poor
e8b53518ea fix(webhook): an all-NUL whsec_ key fails closed like the empty key
HMAC zero-pads its key, so a whsec_ secret that decodes to NUL bytes
(whsec_AA==) signs exactly like the empty key b"" the previous fold
refuses; the whitespace-only guard let it through. Strip NULs with the
ASCII whitespace before the emptiness check. (Lead fold, 2c lens Low.)
2026-10-07 13:19:35 +05:30
kshitijk4poor
b0ea19a9cd fix(webhook): a whsec_ Svix secret that decodes to an empty or blank key fails closed
_is_usable_secret only sees the encoded string, so "whsec_" (decodes to b"")
and "whsec_ICAg" (decodes to three spaces) passed it and keyed the Svix HMAC
with a public value: any caller could sign a request. Apply the same
blank-key rule to the decoded key before computing the HMAC.
2026-10-07 13:19:35 +05:30
kshitijk4poor
53f84cc716 fix(whatsapp_cloud): keep a null app secret unset instead of keying HMAC with "None"
An empty YAML value (`app_secret:` / `verify_token:`) arrives as None, and
str(None) made the inbound signing key the literal "None". The truthiness
guard in _handle_webhook then accepted it, so anyone could sign a POST with
HMAC("None", body). Same class as the LINE null-secret fix in this stack;
`or ""` lets a null read as unset and reach the existing 503 refusal.
2026-10-07 13:19:35 +05:30
kshitijk4poor
caa63dfc44 test(webhook): pin the dynamic-route secret guard
Gate r2 findings 3 and 4. Mutating _dynamic_route_allowed to
`if not effective_secret:` stayed green, because no test reached the
hot-reload guard with a blank or non-string secret. The existing
parametrized connect case now asserts the dynamic-route guard rejects
the same secrets, and it goes red under that mutation (4/4). Also
rename the test and its param to match what they cover (unusable, not
only whitespace-only), and refresh the stale docstring to name
_is_usable_secret.
2026-10-07 13:19:35 +05:30
kshitijk4poor
5a527de775 fix(webhook): say a non-string secret is rejected in the secret messages
Fold finding 2 (gate): _is_usable_secret now also rejects non-string
secrets (a YAML int like `secret: 123` fails closed), but the startup
ValueError, the dynamic-route warning, the per-request log and the
comment in _read_authenticated_body still said "no secret" / "missing
or empty". An operator with an int secret would be told it is absent.
All three now say "missing, blank, or not a string"; the connect()
test gains an int row and matches the new wording.
2026-10-07 13:19:35 +05:30
kshitijk4poor
6e6657e1ab refactor(webhook): keep the secret classifier private to the webhook adapter
Fold finding 1 (gate, 3 reviewers): is_usable_secret lived in the
experimental gateway/relay/auth.py, so every webhook import pulled in
the relay package (~2.5ms) for a one-line predicate only the webhook
adapter uses. Move it next to _hex_hmac as module-private
_is_usable_secret, revert both relay/auth.py hunks, and move the
parametrized classifier test into tests/gateway/test_webhook_adapter.py.
No behaviour change.
2026-10-07 13:19:35 +05:30
Alex Honecker
8ec668b3cc fix(security): treat whitespace-only HMAC secrets as unset
A blank or whitespace-only secret now fails closed at webhook startup,
on the request path, and when a dynamic route is admitted.

(cherry picked from commit 05e918d52c41e905d88e6c0a0f55e7612f4d899d)
2026-10-07 13:19:35 +05:30
kshitijk4poor
152aa695bb fix(cron): key store state by resolved path, never swallow a flapped outage notice
Final-stack review (hermes-pr-review 2c) on #133283:
- The tick lock reported its outage under the unresolved profile home (_get_lock_paths), while
  every save, probe and grace check uses the resolved store path. With a symlinked HERMES_HOME
  (or macOS /var -> /private/var) the lock outage never cleared: writable stuck at 0, no
  "recovered" notice, and a skipped one-shot retired as missed. All store_health records are now
  keyed through one _key() (realpath). Pinned with a symlinked home; red without it.
- unwritable -> recovered (before delivery) -> unwritable posted nothing: the dropped first
  notice left the "recovered" timer pending, so the re-entry cancelled it as if an outage notice
  had gone out. A dropped "unwritable" now cancels that timer, so the re-entry announces. The
  notice test runs on a manual loop clock (no wall-clock timing; it had flaked once under load)
  and pins the sequence.
- cron status printed "0 due run(s) not fired" and an empty summary when the jobs could not be
  read from an unwritable store; it now says the jobs could not be read.
2026-10-07 10:20:18 +05:30
kshitijk4poor
6f88751a7e refactor(cron): share is_recurring, the CLI probe-then-read and the report fields
/simplify-code quality pass on the final #133283 stack, no behaviour change:
- One null-safe is_recurring in cron/constants.py replaces scheduler_tick's new copy and
  unreachable_retry's (whose .get("schedule", {}) raised on a null schedule).
- cron status / cron list share _probe_then_list_jobs (probe, print, guarded read) instead of
  two near-identical blocks.
- StoreDegraded.notice_fields and probe_report build their fields through one _report_fields.
- The notice listener's pre-checks no longer repeat deliver()'s store re-check; deliver() is the
  single staleness gate (still red when removed).
2026-10-07 10:20:18 +05:30
kshitijk4poor
3192c53003 fix(gateway): deliver cron store notices in order, in the owner's language
Greptile r1 on #133283:
- #7: the "recovered" timer started an un-awaited send; if the store failed again while that
  send waited on profile loading or its transport, the outage notice could land first and the
  stale "recovered" last. Notices for a store now go through one asyncio.Lock in transition
  order and re-check the store at delivery, so a notice that no longer matches is dropped and
  the outage notice stays the last word.
- #6: t() ran before entering the owning profile's runtime scope, so a secondary profile's home
  channel got the launch profile's display.language. The message is now rendered inside that
  scope.
- #8: the notice test shortened the window to 0.05 s and slept 0.2 s, against tests/AGENTS.md
  (no quiet-runner timing). It now uses a zero window and drains the loop until no timer or send
  remains, holds a delivery open with an asyncio.Event to drive the flip-during-send case, and
  asserts the full notice sequence.
2026-10-07 10:20:18 +05:30
kshitijk4poor
4453800b82 refactor(cron): drop the warn_store_unwritable wrapper and two getattr hacks
Review cleanup, no behaviour change:
- warn_store_unwritable only resolved the active store before calling
  store_health.note_unwritable; note_unwritable now defaults cron_dir to the active store and
  every caller uses it directly (scheduler_tick no longer imports both).
- The advance-skip split no longer zips a parallel bool list twice, and reads
  (schedule or {}) so a null schedule cannot raise inside the OSError handler.
- skipped_runs metric: a walrus instead of getattr around an Optional record.
- Notices: runner._served_profile_homes is declared on the reconcile mixin, so no getattr.
- cron status loads the active jobs once (skipped count + summary) instead of twice.
2026-10-07 10:20:18 +05:30
kshitijk4poor
9989e152b0 refactor(gateway): drop the unreachable concurrent.futures cancel arm from the notice callback
Notices are asyncio Tasks since the deferred-recovered change; a cancelled Task raises asyncio.CancelledError only.
2026-10-07 10:20:18 +05:30