Commit Graph

12 Commits

Author SHA1 Message Date
kshitijk4poor dc50f02090 fix(adoption): re-check donor growth at retire time, not just at export
Closes the TOCTOU window flagged in review on #93369 (merged via
#93430): the divergence guard compared EXPORT-TIME message counts, but
another backend can append donor messages between the export snapshot
and the retire loop — that growth would be stamped behind the
non-recoverable adopted_by_profile archive, the exact H2 class the
guard exists to prevent, just via a narrower race.

The retire loop now re-reads live donor vs local counts immediately
before end_session and leaves the donor unretired (donor_retired=False,
warn-logged) on any donor-ahead signal; the next resume's export-time
guard then handles the divergence normally. Equal-count CONTENT
divergence (donor rewind+rewrite) remains invisible to count comparison
— documented as accepted: bytes stay in the donor store either way.

New red-first-verified regression simulates the exact race by appending
to the donor from inside an export_session_lineage wrapper.
adoption+ownership suites: 25 passed; ruff clean.
2026-08-24 15:07:18 +05:30
kshitijk4poor 80b202f53a harden(adoption): review findings — exact-id donors only, divergence guard, honest donor_retired
Review batch (3 reviewers) on the final diff surfaced:
- H1: title-based donor matching could adopt AND non-recoverably retire
  an UNRELATED default-store conversation (bot titles collide by design;
  get_session_by_title has no archived filter/ordering). Donor probe is
  now exact-id only — the stranded repro always has the id.
- H2: re-adoption after a partial run could retire a donor that had
  accumulated NEWER messages than the profile copy (skip-based
  idempotency never merges). New divergence guard compares message
  counts and refuses retirement when the donor is ahead (still adopts).
- M1: donor_retired reported True even when every retirement step
  failed under suppress. Now per-segment tracked + warn-logged;
  True only when all applied.
- M3: adopted=False (e.g. import validation limits) was silent — now
  warn-logged with import errors.
- M4: archived donors are never re-adopted (no cross-profile cloning).
- Dead 'from pathlib import Path' dropped; contextlib no longer needed.

5 new red-first-verified regressions (title-collision immunity,
archived-donor immunity, non-vacuous owns_db gating with a real donor
seeded, divergent-donor retirement refusal, donor_retired truthfulness).
tests/tui_gateway: 578 passed. ruff clean.
2026-08-23 18:58:40 -07:00
kshitijk4poor 26a4f89ada fix(gateway): adopt stranded bot sessions from the default store on profile resume
Pre-#93296, the desktop routed session RPCs by the focused tile, so a
profile bot's turns executed on the default backend and its canonical
session accumulated in the DEFAULT profile's state.db. Post-fix, the
profile backend correctly receives the resume — but its store has never
seen the session, so the same chat 4001s forever (unreachable instead
of misrouted). Live repro: Teknium's Developer bot, session c93770.

- hermes_state_portability: SessionDB.adopt_session_lineage_from() —
  composes the existing export_session_lineage()/import_sessions()
  primitives; donor rows are archived (never deleted) with
  end_reason=adopted_by_profile, which is deliberately NOT in
  RECOVERABLE_END_REASONS so canonical-lookup resurrection cannot undo
  an adoption. Idempotent (already-present ids skip).
- tui_gateway/methods_session: profile-scoped session.resume falls back
  to adoption from the default store right before the 4007; ids unknown
  to BOTH stores still 4007 exactly as before, and launch-profile
  resumes never consult the fallback.
- tests: 10 new (7 unit on the primitive incl. compression-lineage
  unit adoption + non-resurrectable archive; 3 handler-level through
  server.handle_request incl. the live repro shape); db-ownership
  leak test taught that the shared launch handle probe is by design.

Follow-up to #93296/#93311; part of #93091.
2026-08-23 18:58:40 -07:00
abundantbeing a2a23a8f7e fix(clients): hide compaction carriers across surfaces 2026-08-21 21:53:11 +05:30
Teknium 26aa12337a feat: /save exports the current session as json, md, or html on all platforms
Rework of salvaged PR #6372 (@ag9920) onto current main:

- /save promoted from CLI-only JSON snapshot to a cross-platform session
  export: `/save [json|md|html] [filename] [redact]` on CLI and every
  gateway platform (sent as a document via adapter.send_document).
- Rendering routes through the existing shared renderers
  (hermes_cli/session_export.py + session_export_html.py) instead of the
  PR's new hermes_state formatter — new helpers normalize_save_format /
  render_session_for_save / default_save_filename are shared by both
  surfaces.
- `redact` arg runs the export through the force-mode secret redaction
  pass (session_export_md.redact_session_data) before writing.
- Gateway handler awaits AsyncSessionDB correctly, sanitizes user-supplied
  filenames with basename, and lands in gateway/slash_commands.py (the
  handlers moved out of gateway/run.py since the PR was authored).
- /export stays profile export (name collision resolved: session export
  lives on /save).
- Slack 50-slash cap curation: /platform moves to the /hermes-only set to
  free a native slot for /save (parity test updated rationale comment).
- Folds in PR #62268 (@briandevans): None title/model coalescing in the
  single-session HTML export.

Closes #4249. Closes #51200.
2026-08-15 02:04:49 -07:00
ag9920 23ad35b7fa feat: Add /export command for session export (Markdown/JSON) to CLI and Gateway 2026-08-15 02:04:49 -07:00
embwl0x 7d066c3c56 fix(state): deduplicate session system prompts 2026-08-03 20:37:17 +05:30
kshitij 7db2827520 refactor(state): chunk the batched tip-row IN clause at 900 ids
Simplify-pass fold: SQLITE_MAX_VARIABLE_NUMBER is 999 on pre-3.32\nbuilds (which the repo still supports — the trigram-availability\nmachinery exists for exactly that class), and limit=10000\nlist_sessions_rich callers exist in web_server. Chunk inside the\nbatch helper — the single choke point — so no call site can overflow.
2026-08-03 17:32:17 +05:30
jasoisjaso adcdf9dc63 perf(state): batch compression-tip row fetch in list_sessions_rich
list_sessions_rich()'s compression-root projection called
_get_session_rich_row() once per root — a separate single-row query per
compression root on every session-list render. Resolve every tip id
first, then fetch all tip rows in one WHERE id IN (...) query via the
new _get_session_rich_rows_batch().

_get_session_rich_row() is now a thin wrapper over the batch method, so
the enriched SELECT (preview + last_active) lives in exactly one place —
future column changes (e.g. #42196's include_system_prompt) only touch
one query.

get_compression_tip()'s chain walk is untouched; it's a genuine
per-session graph walk with branch/delegate-exclusion and race handling,
and batching it safely is out of scope here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 17:32:17 +05:30
Teknium 038c1ad872 docs(gateway): precise watchdog scope, explicit import-resets-activity contract (review S4)
- Config docs now describe session_stall_timeout precisely: a RECOVERY
  notifier for an in-process AIAgent with an adapter-queued follow-up —
  not a general gateway/session stall detector — with a per-AIAgent scan
  cadence (not globally coordinated per durable session).
- import_sessions documents the deliberate export-includes /
  import-resets asymmetry for the activity fields (no resurrected
  'working' labels on machines where no agent runs), with a regression
  pinning both halves.
- Strip trailing whitespace in contributors/emails/fangliquan@qq.com
  (git diff --check housekeeping).

PR #76354 review, scope/contract items + housekeeping.
2026-08-02 16:16:36 -07:00
fangliquanflq c2088efe9e feat(gateway): session activity watchdog, stall notify, compress timeout (#72424)
Three mechanisms to detect and notify when gateway sessions stall silently:

1. Mid-turn activity heartbeats stamped to SessionDB so hermes sessions list
   and hermes status show progress during long turns without new message rows.

2. Stall watchdog: when a busy session has pending inbound and the shared
   activity clock is idle past agent.session_stall_timeout (default 300),
   log a WARNING and notify the user once to try /new. Notify-only; does
   not kill the turn.

3. Compaction timeout: fenceless compress_context callers get a progress-aware
   host budget (compression.context_timeout_seconds default 120 idle,
   compression.context_total_ceiling_seconds default 600 ceiling). On timeout,
   cancel via commit fence, skip compaction without dropping messages, and
   continue the turn.

Closes #72016 (slices 1-3; slice 4 cumulative SSE stream-retry deadline
remains a follow-up).

Cherry-picked from PR #72424 by @fangliquanflq.
2026-08-02 16:16:36 -07:00
teknium1 21c7ae8563 refactor: split SessionDB into Search/Schema/Portability mixins (mechanical move, ~2.9K LOC out of hermes_state.py) 2026-07-29 10:14:19 -07:00