Commit Graph

31 Commits

Author SHA1 Message Date
kshitijk4poor
7ac231fcdb docs(agent): keep the overload-escalation rationale on the constant only
The why (retry can win vs. deferred compression_exhausted wipe, and why the
streak is durable) was spelled out in full at the constant, in
_on_summary_failure, in record_completed_compaction and in the durable
getter docstring. Keep it once on _CONSECUTIVE_OVERLOAD_ABORT_ESCALATION
and cut the other copies to one line, matching the sibling fallback-streak
getter. Comment-only.
2026-09-26 22:42:38 +05:30
kshitijk4poor
50066dda6e fix(state): bump the summary-overload streak atomically
The durable overload budget was a read-modify-write (memory += 1, then
set the column), so two agents compressing the same session concurrently
could each write N+1 and lose a strike, delaying the fallback escalation.
Bump the column with one UPDATE ... RETURNING inside _execute_write and
take the returned row value as authoritative; memory-only counting is
kept for unbound compressors. No try/except or legacy-setter shim: the
existing _durable_read plumbing already degrades on unsupported DBs.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-09-26 22:42:38 +05:30
Yuan Li
2305385042 fix(agent): persist the summary-overload budget in session state so fresh bindings inherit it
Review P1 on #123186 (ehz0ah): the N=3 sustained-overload budget was
object-local. The gateway binds a fresh compressor to the same session on
every turn / cache eviction, and restart / API-server requests construct
one too, so each fresh instance restarted the budget at zero and a
sustained summary-provider outage walked the session back into
compression_exhausted and auto-reset — the exact wipe the PR exists to
prevent. Reproduced by the reviewer with three fresh compressors bound to
one session: every attempt ended at counter=1.

Persist the streak as sessions.compression_overload_streak through the
same durable channel the fallback streak and recovery deadline already
use (#100185): SessionCompressionMixin get/set pair over the sessions
row, loaded in the compressor's durable-load block, written on every
change (overload abort, successful summary, committed boundary, runtime
model switch).

- Compression rotation carries the streak to the child row at the
  boundary, same as the fallback streak: the parent's value is read
  before the bind, re-applied, and persisted onto the fresh child row.
- A completed compaction boundary settles the budget to 0 — including
  the committed degraded fallback, so a recovered provider regains a
  full budget instead of the session staying degraded forever.
- Schema event appended to SCHEMA_HISTORY["sessions"] (seq 28, after
  compression_recovery_deadline) so salvage replay maps the new column.
- Fresh-instance regression: two aborts on one compressor, then a fresh
  compressor bound to the same session inherits streak=2 and the third
  session-wide attempt commits the fallback; plus a rotation carry-over
  test. Both fail red against the memory-only implementation.

(cherry picked from commit 3742fee8d03d7077d23b0dac3a3c79fe48e102ac)
2026-09-26 22:42:38 +05:30
teknium1
d956f0ae57 fix: key the tools[] pin by code version; never re-add config-excluded tools
Review follow-up on the byte-identical tools[] pin.

- The pin records the code identity that built it (checkout/build sha, else
  the release version). Written by the same code, every pinned tool that is
  still available keeps its pinned bytes, including tools whose parameters
  are derived per surface (delegate_task, text_to_speech, memory, patch).
  The per-tool "parameters differ -> take current" rule replaced those bytes
  on every surface hop and rewrote the ~44KB pin each time. A pin from other
  code (`hermes update`, legacy name lists) takes the current definitions
  once and is re-pinned.
- A pinned tool this process did not build is carried forward only while
  this agent's toolset selection allows it (enabled minus disabled toolsets
  and role reservations, before check_fn). It must also pass the session
  schema gates on the merged array, so browser_exec never comes back once
  terminal is gone. Client-surface toolsets (desktop_ui, project) still
  carry across hops: no config choice removed them there.
- The rotation compaction child inherits the parent's pin in the publish
  transaction.
- `hermes sessions recover` keeps pin rows in its system_prompts sweep and
  clears dangling pin hashes, as lost-and-found now does too. Profile moves
  carry the pin like the prompt. A continuing session whose pin is missing
  or unreadable (a row swept by an older build) pins the tools it sends on
  that turn, so later hops stay stable.
2026-09-23 15:43:51 -07:00
teknium1
52a293fb07 fix(state): stale-agent recovery ignores reset forks of a compression parent
_NON_CONTINUATION_CHILD_FILTER_SQL now excludes a child whose
_reset_from marker is bound to the queried parent, like the branch and
delegate markers, so find_live_compression_child never recovers to (or
fails closed because of) a reset fork and a lone reset child no longer
blocks reopen_orphaned_compression_session (#114271).
2026-09-18 10:39:13 -07:00
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
liuhao1024
714c2be565 fix(state): keep reset forks from stealing the compression lineage tip
A reset fork child (model_config._reset_from, or the legacy same-key
heuristic) is a separate user-visible conversation that already lists
as its own row, but the compression chain step only excluded
branch/delegate/tool children. When a reset sibling ended later than
the real continuation it won the last_active tiebreak, so the lineage
tip projection landed on it: the newest session became invisible in
every list and the reset sibling showed twice (#114271).

Exclude _RESET_CHILD_SQL children in both forward-chain walkers
(get_compression_chain's _CHAIN_STEP_SQL and the order_by_last_active
CTE in list_sessions_rich), mirroring the resume path's exclusion.
2026-09-18 10:39:13 -07:00
Efe Büken
a239c4f811 fix(state): tolerate malformed session marker JSON 2026-09-11 06:24:54 -07:00
Teknium
a0749d583a refactor(sessions): trim #106543 to the host-side heal and two invariants
The publish-side taxonomy (_end_stamp_class, _compression_parent_obstacle,
compression_parent_deliberately_ended) and the agent-guard delegation produced
exactly main's verdict -- every non-automatic stamp still fails closed -- so
they only changed an error string. Inline the "explicit close with no
continuation" test into reopen_if_explicitly_closed(), restore main's publish
branch and agent guard untouched, and keep two tests: the field shape rotates
after the host clears the stamp; boundary/compression/automatic stamps and a
session already claimed for teardown are never cleared.
2026-09-10 03:04:51 -07:00
Totoro-qaq
9cd1453f26 fix(sessions): a stale explicit-close stamp no longer makes a session permanently uncompressible
Consolidated from PR #106543 (5 commits, final tree d2c4d908) by @Totoro-qaq.
publish_compression_child() fails closed on any non-automatic end stamp and
end_session() is first-stamp-wins, so a stale tui_close on a session the TUI
still routes turned every rotation into "compute the summary, then discard it"
(#106459). The host that still routes the session clears the stale explicit
close via SessionDB.reopen_if_explicitly_closed() before the turn starts;
publication never heals explicit closes. Review probes by @ehz0ah.
2026-09-10 03:04:51 -07:00
Teknium
8a50413ba2 refactor(state): drop inline comments duplicating the cooldown-rollback docstring
The docstring already states WHY a vanished session row is tolerated (#106271); the two inline paragraphs restated it per branch.
2026-09-09 09:21:39 -07:00
liuhao1024
c0e14092e5 fix(state): also tolerate session deletion between cooldown rollback and read-back
Review follow-up for #106276: the compensating UPDATE can succeed and the
session row can still be retired before the verification read-back runs.
That window raised the same RuntimeError as a genuine restore mismatch.
Accept the absent session (warn + return) exactly like the pre-update
deletion window, keep strict verification for a surviving row, and add a
regression test that deletes the session after the UPDATE commits.

[salvage: test hunk dropped from this pick, see previous commit]
2026-09-09 09:21:39 -07:00
liuhao1024
ccc5cba3d2 fix(state): tolerate a vanished session row in compression cooldown rollback
The exact-row rollback in restore_compression_failure_cooldown_row raised
RuntimeError when its UPDATE hit rowcount == 0, so a session row retired or
expired mid-attempt (e.g. by the maintenance sweep) crashed the turn
dispatcher during the compression summary failure path (#106271). A missing
session row means the cooldown died with it: nothing is left to restore, so
treat rowcount == 0 as a tolerated no-op (warn + return, skipping read-back
verification), mirroring the existing early-return for snapshots taken while
the session already did not exist. Write and verification failures still
propagate.

[salvage: contributor test file dropped from this pick; regression tests are carried from #106277 and trimmed to two invariants]
2026-09-09 09:21:39 -07:00
Teknium
53db597201 simplify(compat): hermes_state — drop 81 re-exports + 3 registry aliases + 3 shims, repoint 45 callers + 60 test files
hermes_state.py: delete every '# noqa: F401 (re-exported...)' import block (hermes_state_common/errors/guard/
readpool/sessions/fts/dbfile/wal/repair/registry + agent.context_compressor _DB_PERSISTED_MARKER_KEY); keep
only the names hermes_state.py itself uses, without noqa.
hermes_state_registry.py: drop get_shared_session_db/release_shared_session_db/close_shared_session_dbs
aliases; every caller (gateway/, tools/, tui_gateway/, cron/, mcp_serve, run_agent, tests) now imports
acquire/release/close_all/release_or_close from hermes_state_registry.
hermes_state_titles.py: drop set_auto_title_if_empty shim (title_generator keeps its getattr fallback).
Re-remove shim-only names restored by 34abf954bd: latest_user_message_row_id (tests call
latest_message_row_id(key, role='user'); role-targeting assertions kept) and get_session_activity (tests
build the snapshot via agent.session_activity.build_activity_snapshot over db.get_session(sid)).
hermes_state_wal._log_once resolves its dedupe sets as module globals instead of via hermes_state;
hermes_state_repair helpers call module globals directly (tests patch hermes_state_repair.<name>).
Frozen updater surface untouched (update_cmd_maint imports only SessionDB from hermes_state).
2026-09-03 13:46:50 -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
36b7fe14ee refactor(state): hand-compact remaining long docstrings (all invariants kept) 2026-09-02 20:12:31 -07:00
Teknium
28eb24eb3a refactor(state): reflow full-line comment paragraphs to the 108-col budget (word-preserving) 2026-09-02 19:59:11 -07:00
Teknium
2865973f9e refactor(state): hoist chain-step SQL, fold cooldown/number readers in compression mixin 2026-09-02 19:53:54 -07:00
Teknium
f731c63e89 refactor(state): drop blank separators around nested _do txn closures 2026-09-02 19:47:36 -07:00
Teknium
3c615c48e9 refactor(state): AST-neutral bracket/string-literal layout pass on the six mixin modules 2026-09-02 19:45:18 -07:00
Teknium
4de8710b74 refactor(state): unify _placeholders/_ended_by_compression/row-probe SQL into hermes_state_common 2026-09-02 19:43:49 -07:00
Teknium
7d48a84acf refactor(state): reflow prose docstrings to the 108-col budget (word-preserving) 2026-09-02 19:39:50 -07:00
Teknium
af85e7c644 refactor(state): compact SessionGatewayMixin/SessionCompressionMixin — compose fail_handoff/lineage SQL, unify set_* writers, trim docstrings 2026-09-02 19:21:56 -07:00
Teknium
831f2e542c refactor(state): fold small get_messages/reaction/publish shapes; hug trailing closers (AST-identical) 2026-09-02 17:20:16 -07:00
Teknium
1991695511 refactor(state): _cooldown_row shape helper; per-model usage kwargs via field set; small folds 2026-09-02 17:17:16 -07:00
Teknium
8656cc31f2 refactor(state): table-driven _rows_to_conversation column copy; unify tool-call counting; collapse defensive locals 2026-09-02 16:59:58 -07:00
Teknium
f54075e0bb refactor(state): unify lock/lease claim (_claim_lease_row); finalize_orphaned via _write_rowcount 2026-09-02 16:52:21 -07:00
Teknium
52d982281c refactor(state): _stale_holder guard helper; join implicit-concat SQL fragments (AST-identical) 2026-09-02 16:44:38 -07:00
Teknium
0d7acf67b5 refactor(state): pack hanging signatures/tuples in messages/compression/usage (AST-identical) 2026-09-02 16:38:21 -07:00
Teknium
5c3acca66b refactor(state): resume — verified messages/compression/titles/usage simplification 2026-09-02 16:37:21 -07:00
Teknium
d15c61b5dc refactor(state): split SessionDB into domain mixins and free-function modules; unify SQL boilerplate
hermes_state.py 17,220 -> 6,442 LOC. Behavior-neutral: every moved body is
AST-identical to the original, verified per extraction.

SessionDB core
- _write_sql / _write_rowcount / _read_one / _read_all replace ~120 copies of
  the `def _do(conn): conn.execute(...)` + `_execute_write(_do)` and
  `with self._read_ctx() as conn: row = conn.execute(...).fetchone()` shapes.
- _set_lineage_column replaces four copies of the recursive compression-lineage
  UPDATE (archived / pinned / hidden / last_read_at).
- _read_session_number unifies the three compression counter readers.
- Dead (zero refs repo-wide): restore_rewound, delete_gateway_routing_entries,
  _is_duplicate_replayed_user_message, SessionPortabilityMixin.get_first_assistant_text.

New mixins bound onto SessionDB via the MRO (logger name stays "hermes_state"):
  hermes_state_messages    SessionMessagesMixin       48 methods
  hermes_state_compression SessionCompressionMixin    30
  hermes_state_gateway     SessionGatewayMixin        26
  hermes_state_maintenance SessionMaintenanceMixin    13
  hermes_state_usage       SessionUsageMixin          12
  hermes_state_titles      SessionTitlesMixin         13
  hermes_state_telegram    SessionTelegramTopicsMixin 11
Origin-internal symbols resolve through a lazy `from hermes_state import ...`
inside the few methods that need them (no import cycle).

New free-function modules, every name re-imported into hermes_state so
`hermes_state.<name>` (and test monkeypatches on it) keep working; intra-module
calls to patched helpers go through the lazy origin import:
  hermes_state_repair   repair/backup/preflight (43 defs)
  hermes_state_wal      journal-mode / PRAGMA policy (33 defs)
  hermes_state_dbfile   header probes, zeroed-db quarantine, stats, holders (21 defs)

Existing mixins: search — shared FTS MATCH/LIKE builders, unified rebuild
status/step/finish engines, state_meta helpers; schema — one legacy/v23 FTS init
branch, shared _live_pk_columns, Row/tuple dual access dropped; portability —
shared _PREVIEW_RAW_SUBQUERY_SQL and _rich_row; common — single
stat_db_file_identity (was 3 copies), AUTO_VACUUM_MIN_FREELIST_RATIO.

Docstrings/comments hand-compacted (AST-identical) keeping every invariant,
ordering rule, failure mode and WHY. Schema SQL, migration order and PRAGMAs
untouched. test_repair_path_has_no_bare_connects repointed to hermes_state_repair.
2026-09-02 13:32:13 -07:00