Commit Graph

631 Commits

Author SHA1 Message Date
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 fb14bc4e11 review-fix(whitespace): strip trailing whitespace and EOF blank lines introduced by this PR
Trailing-whitespace-only edits so 'git diff --check BASE HEAD' is clean
(16 diagnostics across 11 files). No code changes.
2026-09-03 09:31:54 -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 23b9ffc4fa fix(integration): restore subprocess stdin=DEVNULL / utf-8 encoding guards and windows-footgun gates dropped by round-3 compaction
Repo scanners (check_subprocess_stdin, check-windows-footguns --all) flagged 21 sites where
the r3 single-line collapses lost stdin=DEVNULL, encoding='utf-8'/errors='replace', the
'# windows-footgun: ok' same-line marker, or the getattr(os, 'geteuid') gate. Each guard is
restored at the call site (real portability/hang fixes, not suppressions).
2026-09-03 02:46:19 -07:00
Teknium e1487be830 fix(integration): cron preflight reads primary config.yaml via read_user_config_raw()
Compaction put the 'config.yaml' literal within the config-read guard's
proximity window of a raw yaml.safe_load. Use the canonical raw reader with an
explicit path (same semantics: raw primary file, no defaults/overlay).
2026-09-03 02:08:03 -07:00
kshitijk4poor c3e9b28a42 fix(cron): key worker deliveries by the job's own attempt; deferred send is not a failure
Review fold-in on the salvage of #101877:

- `_deliver_result` routed to the durable queue whenever the worker's
  `_HERMES_CRON_EXTERNAL_WORKER` marker was set, regardless of WHICH job was
  delivering. A worker whose script dispatches another job in-process
  (`hermes cron run <other>`) inherits that env and would have queued the
  nested job's message under the outer execution id — `INSERT OR IGNORE`
  then drops it silently. Match the marker against the delivering job's own
  `execution_id`, as `run_one_job` already does. Regression test added
  (mutation-checked: fails with the guard removed).
- A `pending` row left queued at the worker's wait timeout was still
  reported as a delivery error, so `mark_job_run` recorded
  `last_status=delivery_failed` for a message the next gateway's drain goes
  on to send, and nothing ever corrects the job record. Log and return
  success instead; the deliveries row is the authority for the send.
- Reuse `cron.executions._TERMINAL_STATES` in the parent wait loop instead
  of a second hardcoded terminal set.
2026-09-03 12:40:26 +05:30
kshitijk4poor b440a492b3 fix(cron): keep unsent worker deliveries queued, cheapen handoff polling
Follow-up to the salvaged restart-safe worker (#101877):

- delivery_queue: a row still `pending` at the worker's wait timeout was
  marked `failed` and never drained, so any gateway outage longer than the
  300s budget (e.g. a restart that runs `hermes update`) silently lost the
  delivery. Unclaimed rows are certainly unsent, not uncertain — leave them
  queued for the next gateway; only mid-send rows are fenced `unknown`.
- delivery_queue: stop running the full-table prune UPDATE+COUNT inside
  every transaction (each `get_status` poll paid for it; terminalizing
  paths already prune explicitly); poll at 1s instead of 250ms.
- delivery_queue/executions: use `hermes_state.apply_wal_with_fallback`
  (bare `journal_mode=WAL` raises on NFS/SMB homes) and the race-safe
  `hermes_cli.sqlite_util.add_column_if_missing`; drop the copied
  owner-liveness helpers in favour of the ones in cron.executions.
- scheduler: the parent waited on the worker by re-opening the executions
  ledger every 50ms for the whole run (~20 opens/s, hours). Wait on the
  process with a 1s timeout instead — the worker commits its terminal row
  before exiting — and reap stranded payload/ack files once terminal.
- scheduler: skip the housekeeping drain until a worker has actually
  created deliveries.db, so non-systemd gateways never open it.
- scheduler: set up hermes logging in the detached worker entrypoint; it
  runs with stdout/stderr on DEVNULL and previously logged nowhere.
- tests: test_lost_fire_claim_stops_stale_delivery still mocked
  `mark_execution_running -> None`, which now means "ownership lost, return
  before run_job" — the test passed without ever reaching the path it
  names. Mocking `{}` restores it (mutation-checked).
2026-09-03 12:40:26 +05:30
Brooklyn Nicholson 83efdf5e5e [verified] fix(cron): close restart handoff races 2026-09-03 12:40:26 +05:30
Brooklyn Nicholson 29e5172487 [verified] fix(cron): harden gateway restart handoff 2026-09-03 12:40:26 +05:30
Brooklyn Nicholson 3373e97693 [verified] fix(cron): preserve active runs across gateway restart 2026-09-03 12:40:26 +05:30
Teknium caf7fc0686 refactor(cron): compact scheduler.py docstrings by hand (rules/invariants kept) 2026-09-02 20:40:14 -07:00
Teknium ce64929054 refactor(cron): join split string literals in scheduler.py (AST-identical) 2026-09-02 20:31:54 -07:00
Teknium a432b10ad9 Merge branch 'simp/r3-09-w1' into simp/r3-09 2026-09-02 20:28:44 -07:00
Teknium 3dde5e86a7 refactor(cron): jobs.py — collapse repeated record-mutation shapes into dict.update 2026-09-02 20:22:19 -07:00
Teknium 686d8d926e Merge branch 'simp/r3-09-w2' into simp/r3-09 2026-09-02 20:21:17 -07:00
Teknium 6774930ab1 refactor(cron): jobs.py — compact docstrings/comments by hand, wrap every line to ≤100 cols, tighten normalizer/resolve helpers 2026-09-02 20:14:00 -07:00
Teknium 2c0961b334 refactor(cron): table-dispatch media send, unify tree-kill fallback + home env lookup, squeeze local-import blanks 2026-09-02 20:12:25 -07:00
Teknium 8bc43aa524 refactor(cron): jobs.py — _DueJob bundle for due-scan helpers, unify parse_schedule natural-phrase branches, simplify weekday tokenizer/clock parse, collapse stat/TTL fallbacks 2026-09-02 20:07:37 -07:00
Teknium 8642c6c65d Merge branch 'simp/r3-09-w3' into simp/r3-09 2026-09-02 19:59:26 -07:00
Teknium 829d7331c7 refactor(cron): pack brackets, fold closers, compact docstrings in scheduler_* modules (AST-neutral) 2026-09-02 19:51:39 -07:00
Teknium cf08ccc31b refactor(cron): incidents override lookup via executions module ref; split-fire predicate via overrides() helper 2026-09-02 19:46:37 -07:00
Teknium b385c0198f refactor(cron): rewrap long comment/docstring lines to 100 cols 2026-09-02 19:37:58 -07:00
Teknium 52415641ce refactor(cron): jobs.py — fold docstring closers and join split string literals (AST-identical) 2026-09-02 19:35:11 -07:00
Teknium baf31733e9 refactor(cron): AST-neutral bracket packing 2026-09-02 19:32:42 -07:00
Teknium dfa2ecd961 refactor(cron): jobs.py — decompose mark_job_run/update_job, unify fire-fence wrappers and one-shot grace rejection, collapse defensive layers 2026-09-02 19:32:32 -07:00
Teknium 76a1ebdb4c refactor(cron): extract per-target delivery prologue, unify timeout/env/skill-name helpers in scheduler_* modules 2026-09-02 19:31:14 -07:00
Teknium b74da42457 refactor(cron): pack scheduler re-export blocks 2026-09-02 19:27:27 -07:00
Teknium f6460e681e refactor(cron): dedupe blueprint renderer slot lookups, compact classify_items/monitor/suggestion_catalog, drop dead _CLASSIFY_INSTRUCTIONS 2026-09-02 19:26:38 -07:00
Teknium 2f5a879e11 refactor(cron): extract inactivity-timeout diagnostics and async MCP-orphan sweep from watchdog/tick 2026-09-02 19:23:35 -07:00
Teknium b3d9b20a3b refactor(cron): share ledger connect/prepare/transaction across executions, incidents, notepad; compact suggestions 2026-09-02 19:18:21 -07:00
Teknium 83cad303f7 refactor(cron): lift sweep_stale_inflight ledger lookup and release bookkeeping into helpers 2026-09-02 19:16:16 -07:00
Teknium 383972f73b refactor(cron): pack hanging bracket spans in scheduler.py (AST-identical) 2026-09-02 19:12:04 -07:00
Teknium 281cc00db6 refactor(cron): scheduler_provider module logger, adapter-selection helper, drop dead provider_supports_fire_cancel 2026-09-02 19:10:25 -07:00
Teknium b09ddf7746 refactor(cron): decompose run_job and _run_one_job_body into phase helpers
run_job 307 -> ~75: _prepare_job_prompt (pre-agent gates, wake gate, injection block),
_CronRunScope (ContextVar/tool-cwd scope with paired enter/exit), _reload_dotenv_and_publish_
delivery_target, _resolve_cron_agent_setup -> _CronAgentSetup (same gate order), _construct_cron_
agent, _FireAudit. _run_one_job_body 338 -> 170: _FireOwnership (fence + ownership check),
_RunDelivery + _save_compose_deliver, _finish_interrupted_run, _finish_completed_run,
_deliver_crash_failure. Source-inspection test repointed to _finish_interrupted_run.
2026-09-02 19:08:28 -07:00
Teknium fe160f1847 refactor(cron): compact lifecycle_guard structure; patterns and scan order unchanged 2026-09-02 19:04:50 -07:00
Teknium 5a8fbbf16c refactor(cron): split scheduler.py into delivery/script/prompt/preflight modules
cron/scheduler.py 6386 -> 3423. Four sibling modules hold the free-function clusters,
re-exported from cron.scheduler so every existing import/monkeypatch target keeps resolving;
origin-resident helpers are reached late-bound via `_sched` (same-object visibility, no
import cycle). Every moved node is AST-identical to the base. Source-inspection test for the
bash resolver repointed to cron/scheduler_script.py.
2026-09-02 18:34:26 -07:00
kshitijk4poor e4c1e56d5d perf(cron): 1 MiB walk budget; document the independent pre-scan budget
Final-diff review measured the worst text the 4 MiB walk budget still
admitted at ~20 s of shlex (cost is linear in admitted bytes, ~7 lex passes
per line, so the line cap was not the binding knob). 1 MiB brings it to
~4 s while a 200-script wrapper graph (~5 KB) and a ~1 MB build script are
still allowed.

lifecycle_scan_root_within_budget: state that it uses a fresh budget,
independent of the full guard's walk (review point on #83821).
2026-09-03 03:33:24 +05:30
kshitijk4poor e1a791b9f6 refactor(cron): fold review findings into the lifecycle scan budget
- one _capped_read_limit() for local and remote reads (the file's history
  is local/remote divergence bugs: #76762, #77703)
- line cap 256 KiB -> 64 KiB: shlex is quadratic on a single token, so the
  worst text the budget still admits drops from ~60 s to ~17 s of lexing;
  real shell lines are far below either
- fix a comment that described an encode the code does not do; trim the
  header comment to the non-obvious WHY
2026-09-03 03:33:24 +05:30
kshitijk4poor d99eed7d83 perf(cron): bound the lifecycle guard's whole-walk scan work
The referenced-script walk in cron/lifecycle_guard.py capped each file
(1 MiB) and the recursion depth (8) but not the walk: a command referencing
hundreds of scripts, or one enormous shlex token, held the GIL for minutes
on every gateway terminal call (#78398).

Add a per-walk _LifecycleScanBudget (bytes, lines, longest line, unique
paths, remote reads) charged BEFORE any text reaches shlex, and cap each
referenced read at the remaining byte budget so an oversized file is never
read whole. Exhaustion fails closed (the existing contract for one oversized
file) and is logged at WARNING so operators can tell it from a genuine
lifecycle block. Limits are sized so real wrapper graphs never hit them:
a 200-script benign graph is allowed and a restart hidden behind it is
still caught.

tools/terminal_tool.py gates its optional launchctl pre-scan (which also
tokenizes) on the same budget; the full guard still runs afterwards.

Redesigned from #83821 by @Riccardo-Vecchi, which introduced the budget
idea but blocked benign wide graphs (64-path cap) and bundled a suffix
classification change that is left out here.

Refs #78398
2026-09-03 03:33:24 +05:30
Teknium 13fb87af92 refactor(cron): decompose scheduler run_job/_deliver_result and dedupe delivery lanes
cron/scheduler.py (8535 -> 6353):
- _deliver_result split into per-target helpers: _resolve_target_transport (live/relay/standalone
  transport + enablement), _deliver_via_live_adapter (_live_route_metadata for Telegram DM-topic
  vs forum routing, _live_send_text with the cancel()-based timeout disambiguation,
  _live_send_media, _seed_live_delivery_sessions), _standalone_send/_deliver_standalone. The
  three interpreter-shutdown skip branches and the repeated log+append+continue pattern collapse
  into one _note_target_error / one shutdown message; _TargetDelivery carries per-target state.
- Thread/channel session seeding unified into _seed_cron_session (was two near-identical
  functions).
- run_job decomposed into _run_no_agent_job, _apply_monitor_gate, _load_cron_job_config
  (_CronJobConfig), _resolve_job_runtime, _check_model_drift, _open_cron_session_db,
  _run_agent_with_watchdog, _finalize_cron_session, plus one _run_doc_header and one _audit
  closure for the success/failure paths.
- _run_one_job_body: ownership-lost bookkeeping, delivery composition and outcome classification
  extracted; tick: _acquire_tick_lock/_release_tick_lock, _maybe_reap_dead_owners,
  _sweep_stale_inflight_for_tick, _process_due_job, _submit_with_guard.
- _build_job_prompt: context_from injection and skill loading extracted; one
  _prepend_context_block for the four fenced-data blocks.
- One _start_heartbeat_thread for the script- and fire-claim heartbeat threads.
- Dropped unreachable return in SharedRouteAdapters.get; boolean-return and nested-if shapes
  collapsed.
- Comments/docstrings compacted by hand, rationale kept (fd-leak reason for the late SessionDB
  close callback, title-persistence rules, no_agent classification gate, inactivity-vs-provider
  timeout ordering, stale-claim force-release, interruption token keying).
2026-09-02 13:31:43 -07:00
Teknium e4711620a1 refactor(cron): simplify jobs.py store and lifecycle_guard
jobs.py (4654 -> 3332):
- _acquire_flock/_release_flock: one bounded LOCK_NB acquisition shared by _jobs_lock and
  _fire_job_lock (was two copies).
- _get_due_jobs_locked split into per-concern helpers over a _DueScan state object
  (record repair, persisted-error re-arm, timezone-migration catch-up, missed-run fast-forward,
  per-job due evaluation); raw-record repair loops unified.
- atomic_write_text used for heartbeat/counter/error/output writers (drops
  _atomic_write_counter/_atomic_write_epoch duplicates); save_jobs stage+verify loop deduped.
- create_job/update_job field normalization table-driven (_normalize_job_updates,
  _normalize_str_list, _normalize_context_from); parse_schedule croniter validation via
  _cron_schedule; single-record mutators routed through _with_job; completion/activation record
  writers and claim refresh shared; _parse_aware used for record repair and stale-error re-arm.
- Dead: clear_drift_alerted, _set_preflight_alerted shim (zero references).
- Comments/docstrings compacted by hand; rules, invariants and WHY (profile-home anchoring,
  bounded lock rationale, timezone-naive schedule rule, missed-run visibility) kept.

lifecycle_guard.py (1271 -> 906): shared cloud-path check, executed-command index,
resolve-or-skip yield, control-preserving segment split; regexes and matching semantics
byte-preserved; comments compacted.
2026-09-02 13:30:19 -07:00
Teknium 064bcaf3c1 refactor(cron): compact scheduler_provider and small cron modules
- scheduler_provider.py: _profile_entry/_profile_cron_scope replace four hand-rolled
  home-override+store blocks in the multiplex ticker; comments compacted to the WHY.
- incidents/executions/notepad/monitor/suggestions/blueprint_catalog/__init__: docstrings
  and comments compacted; no code change (AST-identical).
2026-09-02 13:29:30 -07:00
kshitijk4poor 95f62ca3bf fix(cron): validate failure_deliver at preflight and dashboard update lanes
Follow-up to the failure_deliver salvage (#100375):

- _preflight_check_delivery also checks the failure lane, so a typo'd
  failure_deliver platform blocks at config-validation time instead of
  surfacing only when a failure occurs — exactly when the notice must
  not be lost. Duplicate lanes are checked once.
- The dashboard cron-update normalizer treats failure_deliver like
  deliver (text normalization; empty clears the optional override
  instead of coalescing), closing the one update path that could write
  an unnormalized value into jobs.json.

4 guard tests; both fixes mutation-checked (neutralize -> red, restore -> green).
2026-09-02 20:16:14 +05:30
Victor Kyriazakos fd35e1ec5a fix(cron): delivery bookkeeping reads the failure lane it actually routed through
Review findings (Salt, NS-788):

B1: delivery_outcome classification, unresolved_origin, and incident
'alerted' marking all read the deliver lane while the notice itself was
routed through failure_deliver — a silenced failure recorded
delivery_outcome='delivered' and marked its incident alerted (corrupting
the 'failure seen' vs 'operator was pinged' distinction the incident
store documents), and a failure delivered via failure_deliver over an
unresolvable deliver=origin recorded 'not_configured'. New
_delivery_lane_value() helper feeds the SAME lane to routing and
bookkeeping at all five sites (both classifiers, both unresolved_origin
computations, both zero-target checks). Three regression tests assert
outcome + alerted-marking; verified to bite on the pre-fix classifier.

S1: failure_deliver now goes through _resolve_cron_context_deliver on
tool create/update, matching deliver — a job created from inside a cron
run can no longer store literal 'origin' in its failure lane.

S2/T1: corrected the false 'same helper' comment in create_job; the
str/list flatten mirrors the tool layer for direct callers.

Full cron suite + interrupt tests: 87 files, 1112 passed, 0 failed.
2026-09-02 20:16:14 +05:30
Victor Kyriazakos c9491e6a7d feat(cron): per-job failure_deliver — route or suppress failure notices (NS-788)
Coatue FR (Frank Long): jobs delivering into shared channels publish
engine failure notices ('⚠️ Cron X failed…') to those channels with no
opt-out. Adds an optional per-job failure_deliver field sharing
deliver's grammar: on failure, targets resolve from failure_deliver
when set (local = structural silence; state still recorded in
last_status/last_error/run history). Success delivery is unchanged;
absent field = today's behavior byte-for-byte.

Honored by every failure-category engine notice: the run_job failure
summary (+streak nudge), the escaped-failure retry path, drift-skip and
blocked-config alerts (composed into the same delivery), and the
gateway-shutdown interrupted-run notice (_notify_interrupted_cron_jobs).

Surfaces: cronjob tool create/update (same bot-chat validation as
deliver; '' clears on update), hermes cron create/edit
--failure-deliver, docs tip in automate-with-cron.

Existing fake_deliver test doubles gained **kwargs for the new
for_failure keyword — signature-compat only, no behavior change.
2026-09-02 20:16:14 +05:30
Teknium a6351a71e5 fix(cron): route a credentialless satellite's cron delivery through the primary adapter for exact profile_routes targets (#101113)
Under gateway.multiplex_profiles a shared-token satellite profile (routed
via gateway.profile_routes, no bot credential of its own) got an empty
adapter map from the multiplex ticker, so _deliver_result fell through to
the standalone sender under the satellite's secret scope and failed with
"DISCORD_BOT_TOKEN is not set" — even though the primary adapter owns the
exact routed channel and had delivered the same target before. Preflight
already rescued this topology (#97476); the delivery half did not.

- cron/scheduler.py: factor the preflight's primary-config route loader into
  `_primary_profile_routes_for_current_home()` (one owner for both halves,
  so route semantics cannot drift) and add `SharedRouteAdapters`, a
  read-only view over the primary adapter map that resolves an adapter for
  a (platform, target) ONLY when an enabled primary route with a
  chat_id/thread_id maps that exact target to the current profile —
  using the same `ProfileRoute.matches` predicate as inbound routing.
  `_deliver_result` resolves the transport per target from it; everything
  else (unmatched chat, disabled route, route for another profile, no
  primary adapter, guild-only route) is a miss and never uses the primary
  bot. Execution stays scoped to the satellite; no credential is copied.
- cron/scheduler_provider.py: a secondary with no adapter map of its own
  gets the SharedRouteAdapters view instead of `{}`. This is NOT a default
  fallback: with no matching route the view is falsy and delivers nothing.

Fixes #101113
2026-09-02 06:27:24 -07:00
Teknium 62a4599f89 fix(cron): keep profile scope on the standalone fallback pool; desktop ticker stands down for profiles with their own gateway (#100489)
Two mechanisms let the desktop multiplex ticker deliver a secondary
profile's cron output through the default profile's identity:

1. _deliver_result's `asyncio.run` ThreadPoolExecutor fallback (taken when
   the caller already has a running loop — the desktop dashboard shape) ran
   the standalone sender on a fresh thread with NO profile ContextVars: the
   home override and secret scope were gone, so the sender resolved the
   process default's home/token (or, fail-closed under multiplex, raised
   UnscopedSecretError). Wrap the submit in copy_context().run like the
   session-db (:6562), heartbeat (:4650) and parallel-pool (:8314) workers.

2. _start_desktop_cron_ticker ticked EVERY local profile, including ones
   whose own gateway (with live adapters) is running; winning the tick-lock
   race meant the adapter-less desktop ticker delivered standalone. The
   multiplex loop gains an optional per-cycle `profile_gate(name, home)`;
   the desktop wires it to `_check_gateway_running(home)` so such profiles
   are neither ticked nor heartbeated by the dashboard while their gateway
   is alive (re-evaluated every cycle, no restart needed).

Fixes #100489
2026-09-02 06:27:24 -07:00
Cyber-Yichen a2fea79de6 fix(cron): isolate multiplex profile failures per profile (#74878)
One profile's broken cron store no longer takes the whole multiplex ticker
down with it:

- startup recovery loop: a per-profile exception (e.g. an unreadable
  executions.db raising sqlite3.DatabaseError) was uncaught and killed the
  ticker thread before its first tick — no profile ever fired.
- tick loop: only CronTickYielded was caught per profile; any other
  exception escaped to the cycle-wide handler, skipping every remaining
  profile that cycle and marking all of them failed.

Both loops now catch per profile, record the failure into THAT profile's
ticker_last_error (`hermes cron status`), and keep ticking the siblings.
The existing CronTickYielded/_profile_errors semantics and the #87644
EMFILE reclaim/backoff are preserved (backoff is applied once per cycle
from the worst per-profile failure).

Salvaged from PR #70747 (@Cyber-Yichen); the recovery test's real
sqlite3.OperationalError shape is from PR #74888 (@OYLFLMH). Same class
also reported in PR #74952 (@webtecnica).

Co-authored-by: OYLFLMH <95945448+OYLFLMH@users.noreply.github.com>
Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-09-02 06:27:24 -07:00
이민재 e48bb828d4 test(cron): preserve explicit suggestions path override 2026-09-02 06:27:24 -07:00
wanliqin 9da8842585 fix(cron): resolve profile store paths per call
Resolve notepad and suggestion paths at transaction time so multiplexed profile ticks cannot write into the import-time home. Preserve explicit test overrides and cover writes after a profile context switch.

Co-authored-by: 이민재 <19909783+honor2030@users.noreply.github.com>
2026-09-02 06:27:24 -07:00