diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ea20cbf6e..3173510bd2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -272,7 +272,7 @@ hermes-agent/ │ ├── run.py # GatewayRunner facade (~5.5k LOC); phases in run_*.py (startup, inbound, turn, busy, ...) │ ├── slash_commands_*.py # Gateway slash command handler mixins │ ├── config.py # Platform configuration resolution -│ ├── session.py # Session store, context prompts, reset policies (+ session_*.py siblings) +│ ├── session.py # Session store, context prompts, explicit resets (+ session_*.py siblings) │ └── platforms/ # Platform adapters │ ├── telegram.py, discord_adapter.py, slack.py, whatsapp.py │ diff --git a/docs/session-lifecycle.md b/docs/session-lifecycle.md index 7bedee9232..7d52866613 100644 --- a/docs/session-lifecycle.md +++ b/docs/session-lifecycle.md @@ -15,7 +15,7 @@ The session system lives primarily in two modules: - `gateway/session.py` — Data model (`SessionSource`, `SessionEntry`, `SessionContext`), key generation (`build_session_key`), and the main store (`SessionStore`). - `gateway/run.py` — Gateway runner (`GatewayRunner`) facade that wires sessions into the message - processing pipeline; the phases live in `run_*.py` siblings: session expiry watching + processing pipeline; the phases live in `run_*.py` siblings: session housekeeping (`run_watchers.py`), agent caching (`run_agent_cache.py`), restart recovery (`session_recovery.py`), and message queuing (`run_busy.py`). @@ -66,10 +66,10 @@ incoming `MessageEvent` and used for routing, isolation, and context injection. | `session_key` | `str` | *(required)* | Deterministic key identifying the conversation lane (see §4). | | `session_id` | `str` | *(required)* | Unique identifier for this specific conversation incarnation. Format: `YYYYMMDD_HHMMSS_<8hex>`. | | `created_at` | `datetime` | *(required)* | When this session incarnation was created. | -| `updated_at` | `datetime` | *(required)* | Last activity timestamp. Used for idle timeout and expiry checks. | +| `updated_at` | `datetime` | *(required)* | Last activity timestamp used for resource housekeeping. | | `origin` | `Optional[SessionSource]` | `None` | The source that created this session, used for delivery routing. | | `display_name` | `Optional[str]` | `None` | Chat display name (sourced from `SessionSource.chat_name`). | -| `platform` | `Optional[Platform]` | `None` | Platform enum, persisted for expiry policy lookup across restarts. | +| `platform` | `Optional[Platform]` | `None` | Platform enum persisted for routing across restarts. | | `chat_type` | `str` | `"dm"` | Chat type, also persisted for policy lookup. | | `input_tokens` | `int` | `0` | Cumulative LLM input (prompt) tokens consumed. | | `output_tokens` | `int` | `0` | Cumulative LLM output (completion) tokens consumed. | @@ -87,11 +87,11 @@ behavior on the next access. | Flag | Type | Default | Description | |---|---|---|---| -| `was_auto_reset` | `bool` | `False` | Set when a session was auto-reset due to policy expiry (idle/daily). Consumed once to inject a context notice. | +| `was_auto_reset` | `bool` | `False` | Set when explicit suspension causes a replacement session. Also retained for historical records. | | `auto_reset_reason` | `Optional[str]` | `None` | `"idle"` or `"daily"` — why the previous session was auto-reset. | -| `reset_had_activity` | `bool` | `False` | Whether the expired session had any messages (`total_tokens > 0`). | +| `reset_had_activity` | `bool` | `False` | Whether the replaced session had prior activity. | | `is_fresh_reset` | `bool` | `False` | Set by explicit `/new` or `/reset`. Triggers topic/channel skill re-injection on first message. Distinguished from `was_auto_reset` to avoid misleading "session expired" notices. | -| `expiry_finalized` | `bool` | `False` | Set by background expiry watcher after invoking `on_session_finalize` hooks, cleaning tool resources, and evicting the cached agent. Prevents redundant finalization across restarts. | +| `expiry_finalized` | `bool` | `False` | Historical finalization fence retained for recovery; no timer writes it. | | `suspended` | `bool` | `False` | Hard force-wipe signal. Set by `/stop` or stuck-loop escalation (3+ consecutive restart failures). On next `get_or_create_session()`, forces a new `session_id` regardless of `resume_pending`. | | `resume_pending` | `bool` | `False` | Soft recovery marker. Set by `suspend_recently_active()` (crash recovery) or drain timeout. On next access, preserves the existing `session_id` — the user continues on the same transcript. Cleared after the next successful turn completes. | | `resume_reason` | `Optional[str]` | `None` | Why resume was marked: `"restart_timeout"`, `"shutdown_timeout"`, `"restart_interrupted"`. | @@ -137,8 +137,7 @@ behavior on the next access. **Priority order in `get_or_create_session()`:** 1. `suspended=True` → always force-reset (hard wipe) 2. `resume_pending=True` → preserve session_id (soft recovery) -3. Policy expiry (idle/daily) → auto-reset -4. No trigger → return existing entry (bump `updated_at`) +3. No trigger → return existing entry (bump `updated_at`) --- @@ -155,15 +154,15 @@ SessionStore(sessions_dir: Path, config: GatewayConfig, has_active_processes_fn= ``` - `sessions_dir` — Directory where `sessions.json` lives. -- `config` — `GatewayConfig` instance for reset policy lookups. +- `config` — `GatewayConfig` instance for routing and housekeeping settings. - `has_active_processes_fn` — Optional callback keyed by `session_key` to check for running - background processes. Sessions with active processes are never expired or pruned. + background processes. Sessions with active processes are protected from routing-entry pruning. ### Operations (Methods) | Method | Description | |---|---| -| `get_or_create_session(source, force_new=False)` | Core entry point. Returns existing or creates new `SessionEntry`. Evaluates `suspended`, `resume_pending`, and reset policy. Creates/ends SQLite records. | +| `get_or_create_session(source, force_new=False)` | Core entry point. Returns existing or creates new `SessionEntry`. Evaluates explicit suspension and restart recovery state. Creates/ends SQLite records. | | `update_session(session_key, last_prompt_tokens=None)` | Lightweight metadata update after an interaction. Bumps `updated_at`, optionally records `last_prompt_tokens`. | | `reset_session(session_key, display_name=None)` | Explicit reset (from `/new` or `/reset`). Creates new `session_id`, sets `is_fresh_reset=True`. Ends old SQLite session, creates new one. | | `switch_session(session_key, target_session_id)` | Switch to a different existing session ID (from `/resume`). Ends current SQLite session, reopens target. | @@ -185,9 +184,6 @@ SessionStore(sessions_dir: Path, config: GatewayConfig, has_active_processes_fn= - `_ensure_loaded()` / `_ensure_loaded_locked()` — Load `sessions.json` into `_entries` dict. - `_save()` — Atomic write to `sessions.json` via temp file + `atomic_replace`. - `_generate_session_key(source)` — Delegates to `build_session_key()` with config params. -- `_is_session_expired(entry)` — Policy check from entry alone (no source needed). Used by - background expiry watcher. -- `_should_reset(entry, source)` — Policy check returning `"idle"`, `"daily"`, or `None`. ### Storage Layout @@ -292,54 +288,16 @@ gateway at runtime, preserving prompt caching (the system prompt doesn't change --- -## 6. Reset Policy +## 6. Explicit Conversation Boundaries -Reset policies control when a session automatically loses context (gets a new `session_id`). +Inactivity and wall-clock time never rotate a conversation. `/new` and `/reset` +create an explicit boundary; context compression continues to manage long histories. +Legacy timer configuration is ignored. The existing `SessionResetPolicy` datatype +is inert compatibility data, not a runtime policy. -### Policy Modes (`SessionResetPolicy`) - -| Mode | Behavior | Default Config | -|---|---|---| -| `"none"` | Never auto-reset. Context managed only by compression. | — | -| `"idle"` | Reset after N minutes of inactivity from `updated_at`. | `idle_minutes: 1440` (24h) | -| `"daily"` | Reset at a specific hour each day (local time). | `at_hour: 4` (4 AM) | -| `"both"` | Whichever triggers first — daily boundary OR idle timeout. | **(default)** | - -### Policy Evaluation - -```python -# Idle check -idle_deadline = entry.updated_at + timedelta(minutes=policy.idle_minutes) -if now > idle_deadline: return "idle" - -# Daily check -today_reset = now.replace(hour=policy.at_hour, minute=0, second=0, microsecond=0) -if now.hour < policy.at_hour: - today_reset -= timedelta(days=1) # Reset hasn't happened yet today -if entry.updated_at < today_reset: return "daily" -``` - -### Per-Platform/Per-Type Policies - -Reset policies are configurable per platform and session type via `config.get_reset_policy()`. -This allows different platforms to have different expiry rules (e.g., Telegram DMs reset -after 24h idle, but Slack groups persist indefinitely). - -### Exclusions - -Sessions with active background processes are **never** expired or reset. The -`has_active_processes_fn` callback checks for running processes when evaluating policies. - -### Reset Effects - -When a reset triggers: - -1. Old session is ended in SQLite (with reason `"session_reset"`). -2. New `session_id` is generated (`YYYYMMDD_HHMMSS_<8hex>`). -3. New `SessionEntry` is created with `was_auto_reset=True` and the reset reason. -4. `reset_had_activity` is set if the old session had any turns (`total_tokens > 0`). -5. The old AIAgent cache entry is evicted on the next expiry watcher pass. -6. On the first message after reset, a context notice is injected: "Session expired due to inactivity / daily reset." +Explicit suspension still creates a boundary on the next inbound turn. Recovery +respects explicit and historical finalized boundaries rather than reopening them. +Resource-only eviction and WebSocket orphan reaping leave conversations resumable. --- @@ -537,42 +495,18 @@ operations always use the original values. --- -## 10. Background Expiry Watcher +## 10. Background Housekeeping -The `_session_expiry_watcher` task runs in the gateway event loop every 300 seconds (5 min). +The `_session_housekeeping_watcher` periodically sweeps idle cached agents, sheds +cache entries under memory pressure, and prunes old routing entries hourly. +It never ends a transcript because of inactivity or the time of day. -### Responsibilities - -1. **Finalize expired sessions** — For each entry where `_is_session_expired()` returns - True and `expiry_finalized` is False: - - Invoke `on_session_finalize` plugin hooks (cleanup, notifications). - - Clean up cached AIAgent resources (close tool resources, shut down memory provider). - - Evict the cached agent entry. - - Clear per-session overrides (`_session_model_overrides`, reasoning overrides, etc.). - - Mark `expiry_finalized=True` and persist (sessions.json + state.db). - - Promote the state.db session row to `end_reason='session_reset'` via - `promote_to_session_reset()` — conditional: only live rows or rows ended with a - recoverable accidental reason (`agent_close`, `ws_orphan_reap`) are promoted, so - explicit boundaries (`compression`, `session_switch`, …) are never overwritten. This - durably records the reset so stale-route recovery cannot resurrect the expired - session with its full history (#61220, #61993, #63539). - -2. **Sweep idle cached agents** — Calls `_sweep_idle_cached_agents()` to evict agents that - have been idle beyond the idle TTL (3600s / 1h by default), regardless of session - reset policy. This prevents unbounded memory growth in gateways with long-lived sessions. - -3. **Sweep under memory pressure** — Calls `_sweep_agent_cache_under_pressure()` to shed - least-recently-used transcripts once the process's anonymous RSS is over budget. See - §11. - -4. **Prune stale entries** — Calls `session_store.prune_old_entries()` hourly based on - `config.session_store_max_age_days`. Prevents `sessions.json` from growing unbounded. - -### Failure Handling - -- Per-session retry count: each failed finalize is retried up to 3 consecutive times. -- After 3 failures, the entry is force-marked `expiry_finalized=True` to prevent infinite - retry loops. +TTL, LRU and pressure eviction commit the live transcript to memory providers before +soft-releasing clients. Active turns remain protected; terminal, browser and background +process resources survive soft release. Routing-entry pruning preserves the canonical +SQLite transcript, and live processes protect their routing entries from pruning. +Historical `expiry_finalized` flags remain recovery fences but are no longer written +by a timer watcher. --- @@ -586,7 +520,7 @@ preserve prompt caching across turns. - **Max size:** 128 entries (`agent.agent_cache.max_size`, default `_AGENT_CACHE_MAX_SIZE`). - **Eviction policy:** Least-recently-used (LRU via `OrderedDict`). - **Idle TTL:** 3600s (1h) — `agent.agent_cache.idle_ttl_secs`, enforced by - `_session_expiry_watcher`. + `_session_housekeeping_watcher`. - **Memory budget:** `agent.agent_cache.memory_high_mb` (default `auto`) — see below. - **Lock:** `_agent_cache_lock` (threading) for thread safety. @@ -595,8 +529,7 @@ preserve prompt caching across turns. A cached agent pins `_session_messages`, the full live transcript including tool outputs — tens of MB on a session with 100+ tool calls. The entry cap and the idle TTL are both blind to that: a gateway serving many chats keeps every warm transcript -resident (agents that took a turn within the TTL are never idle-swept, and the idle -sweep additionally defers finalizable sessions until they expire), so RSS climbs until +resident (agents that took a turn within the TTL are never idle-swept), so RSS climbs until the cgroup throttles and SIGTERM can no longer flush inside systemd's stop timeout (#80764). @@ -641,14 +574,14 @@ Lookup _agent_cache[session_key] run_conversation() → agent processes message │ ▼ -Session expiry watcher evicts agent when session finalizes +Housekeeping soft-releases idle agents without ending transcripts ``` ### Cleanup Flow -When a session expires: -1. `_cleanup_agent_resources(agent)` — shuts down memory provider, closes tool resources. -2. `_evict_cached_agent(key)` — removes from `_agent_cache` so the agent can be GC'd. +Resource eviction removes the cached agent and commits memory before soft-releasing +clients. Full `_cleanup_agent_resources(agent)` teardown is reserved for actual +conversation boundaries and shutdown. --- @@ -675,14 +608,7 @@ rebuilt without deleting canonical messages. See [`docs/state-db-recovery.md`](state-db-recovery.md) for the bounded live failure mode and the explicit repair procedure. -### Reset Policy (per-platform/type, in config.yaml) +### Conversation lifetime -```yaml -session_reset: - mode: none # none (default) | idle | daily | both - at_hour: 4 # daily reset hour (local time) - idle_minutes: 1440 # idle timeout (24h) - notify: true # notify user on auto-reset -``` - -Platform-specific overrides can be set under `platforms..session_reset`. +No idle or daily reset settings are supported. Explicit `/new` and `/reset`, +compaction, suspension and crash recovery retain their separate lifecycle roles. diff --git a/gateway/config_loader.py b/gateway/config_loader.py index aa279b3608..df8f5f6382 100644 --- a/gateway/config_loader.py +++ b/gateway/config_loader.py @@ -346,7 +346,7 @@ def load_yaml_layer(home: Path, gw_data: dict) -> None: yaml_cfg = yaml.safe_load(f) or {} # Managed scope: overlay administrator-pinned values (this loader bypasses - # hermes_cli.config.load_config, so a managed session_reset / quick_commands / stt would otherwise be ignored). + # hermes_cli.config.load_config, so managed quick_commands / stt would otherwise be ignored). from hermes_cli import managed_scope yaml_cfg = managed_scope.apply_managed_overlay(yaml_cfg) diff --git a/gateway/run_turn.py b/gateway/run_turn.py index 88cad72fc9..2e04bea8a9 100644 --- a/gateway/run_turn.py +++ b/gateway/run_turn.py @@ -2795,10 +2795,6 @@ class GatewayTurnMixin: self._thread_metadata_for_progress( source, event_message_id, _progress_thread_id, _relay_prospective_thread_id, ), - # Freshness-gate stale resume_pending zombies (#46934) — but honor an explicit - # ``session_reset.mode: none``: the user opted out of ALL automatic resets, so an expired resume - # marker must fall through to a normal resume of the preserved transcript, never a silent fresh - # session (#61052). platform=source.platform, ) if _native_slack_task_cards: diff --git a/gateway/run_watchers.py b/gateway/run_watchers.py index a84feada51..3d9bc78f51 100644 --- a/gateway/run_watchers.py +++ b/gateway/run_watchers.py @@ -1,4 +1,4 @@ -"""Session expiry / stall / catalog-refresh watcher loops, bound onto ``GatewayRunner`` via the MRO. +"""Session housekeeping / stall / catalog-refresh watcher loops, bound onto ``GatewayRunner`` via the MRO. ``gateway.run`` internals are imported lazily inside method bodies (import cycle), so ``patch("gateway.run.X")`` keeps intercepting them at call time. @@ -35,7 +35,7 @@ async def _interruptible_sleep(runner, seconds: int) -> None: class GatewaySessionWatchersMixin: - """Session expiry / stall / catalog-refresh watcher loops for GatewayRunner.""" + """Session housekeeping / stall / catalog-refresh watcher loops for GatewayRunner.""" async def _session_housekeeping_watcher(self, interval: int = 300): """Reclaim resources without ending durable conversations.""" @@ -49,7 +49,6 @@ class GatewaySessionWatchersMixin: async def _session_housekeeping(self) -> None: """Idle/pressure agent-cache sweeps plus the hourly SessionStore prune.""" - # Sweep idle agents regardless of reset policy: long/"never" windows would pin memory. try: if evicted := self._sweep_idle_cached_agents(): logger.info("Agent cache idle sweep: evicted %d agent(s)", evicted) diff --git a/gateway/session.py b/gateway/session.py index 3a1fc4340f..17f3587122 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -1,5 +1,5 @@ """Gateway session management: message sources, the persisted routing index (SessionStore), -reset policy and the dynamic "Current Session Context" system prompt section.""" +explicit resets and the dynamic "Current Session Context" system prompt section.""" import asyncio import hashlib @@ -483,20 +483,14 @@ class SessionEntry: estimated_cost_usd: float = 0.0 cost_status: str = "unknown" last_prompt_tokens: int = 0 # last API-reported prompt tokens (compression pre-check) - # Created because the previous session expired; consumed once to inject a notice. + # Suspension replacement metadata; historical automatic-reset rows retain these fields. was_auto_reset: bool = False - auto_reset_reason: Optional[str] = None # "idle" or "daily" - reset_had_activity: bool = False # the expired session had messages - prev_session_id: Optional[str] = None # replaced by auto-reset; feeds the continuity note - # Explicit /new or /reset; consumed once to re-inject topic/channel skills. Distinct from - # was_auto_reset, whose "expired due to inactivity" notice is wrong for a manual reset. - # Set by reset_session() when the user explicitly sends /new or /reset. Consumed once by - # _handle_message_with_agent to trigger topic/channel skill re-injection on the first message of the new - # session. We can't reuse was_auto_reset for this because that flag fires the "session expired due to - # inactivity" user-facing notice and a misleading context-note prepend — both wrong for an explicit - # manual reset. See issue #6508. + auto_reset_reason: Optional[str] = None + reset_had_activity: bool = False + prev_session_id: Optional[str] = None # feeds the continuity note + # Explicit /new or /reset triggers topic/channel skill re-injection on the first turn. is_fresh_reset: bool = False - # Set by the expiry watcher after finalizing; persisted so restarts don't re-run finalization. + # Historical finalization fence; timers no longer write it. expiry_finalized: bool = False # Next get_or_create_session() auto-resets; set by /stop to break stuck-resume loops. # When True the next call to get_or_create_session() will auto-reset this session (create a new @@ -895,7 +889,7 @@ class SessionStore( with self._lock: self._ensure_loaded_locked() observed = self._entries.get(session_key) - # Phase 1b (no lock): compression tip + stale check + reset policy. + # Phase 1b (no lock): compression tip + stale check + explicit suspension. checks = None if not force_new and observed is not None: sid = observed.session_id @@ -960,7 +954,7 @@ class SessionStore( session_key, entry.session_id, ) if stale_hit or reset_reason: - # Honour an expiry/reset decision instead of silently reopening via recovery. + # Honour an explicit suspension/reset decision instead of silently reopening via recovery. if reset_reason: decision.schedule_reset(reset_reason, entry, entry.last_prompt_tokens > 0) self._entries.pop(session_key, None) @@ -1043,9 +1037,8 @@ class SessionStore( """Persist a small JSON-serializable metadata value. Deliberately does NOT advance ``updated_at``: a background write must not make an idle session look fresh. - Metadata writes are internal bookkeeping and deliberately do NOT advance ``updated_at``: it is the - user-activity clock that drives idle/daily reset policy and the restart-resume freshness gate - (#85709), and a background write must not make an idle session look fresh. + Internal bookkeeping must not advance the user-activity clock used by housekeeping + and restart recovery. """ return self._update_entry(session_key, lambda e: e.metadata.__setitem__(key, value)) @@ -1103,7 +1096,7 @@ class SessionStore( return new_entry # Compression repoint is store bookkeeping, not user activity — leave ``updated_at`` alone so a - # background compression on an idle session cannot make it look fresh to reset policy or the + # background compression on an idle session cannot make it look fresh to the # restart-resume freshness gate (#85709). def switch_session(self, session_key: str, target_session_id: str) -> Optional[SessionEntry]: """Point a session key at an existing session ID (``/resume``): ends the current row and diff --git a/gateway/session_recovery.py b/gateway/session_recovery.py index 4b7219428b..dc2ce76a7d 100644 --- a/gateway/session_recovery.py +++ b/gateway/session_recovery.py @@ -324,7 +324,7 @@ class SessionRecoveryMixin: display_name: Optional[str], during: str = "") -> None: """SQLite side of a routing transition, outside ``_lock``: promote the predecessor row to an explicit reset boundary (with the specific reason so state.db is auditable, e.g. - ``resume_pending_expired`` vs plain ``session_reset``), then INSERT the new row + routing + ``suspended`` vs plain ``session_reset``), then INSERT the new row + routing peer. Both best-effort: failures are warned and self-healed by the next peer refresh.""" if self._db_for_key(session_key) and end_session_id: self._promote_session_reset( diff --git a/hermes_cli/cli_info_mixin.py b/hermes_cli/cli_info_mixin.py index d126d61be7..44e158f052 100644 --- a/hermes_cli/cli_info_mixin.py +++ b/hermes_cli/cli_info_mixin.py @@ -524,12 +524,7 @@ class CLIInfoMixin: print(f" ○ {name:<12} Not configured ({env_var})") print() - print(" Session Reset Policy:") - print(" " + "-" * 55) - policy = config.default_reset_policy - print(f" Mode: {policy.mode}") - print(f" Daily reset at: {policy.at_hour}:00") - print(f" Idle timeout: {policy.idle_minutes} minutes") + print(" Conversations persist until /new or /reset.") print() print(" To start the gateway:") print(" python cli.py --gateway") diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 39ab228613..566fe7a7f4 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -433,7 +433,7 @@ _TOOL_PROGRESS_HELP = ( " log — Silent in chat; write every tool call to ~/.hermes/logs/tool_calls.log (gateway only)", ) def setup_agent_settings(config: dict): - """Configure agent behavior: iterations, progress display, compression, session reset.""" + """Configure agent behavior: iterations, progress display and compression.""" print_header("Agent Settings") _info(f" Guide: {_DOCS_BASE}/user-guide/configuration", None) diff --git a/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py b/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py index 098a6cc51c..bad94b186b 100644 --- a/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py +++ b/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py @@ -147,7 +147,7 @@ MIGRATION_OPTION_METADATA: Dict[str, Dict[str, str]] = { }, "session-config": { "label": "Session configuration", - "description": "Import session reset policies (daily/idle) into Hermes session_reset config.", + "description": "Archive advanced session settings; automatic reset timers are not imported.", }, "full-providers": { "label": "Full model provider definitions", @@ -915,7 +915,6 @@ class Migrator: "hooks-config", "agent-config", "gateway-config", - "session-config", "full-providers", "deep-channels", "browser-config", @@ -2573,48 +2572,9 @@ class Migrator: self.record("session-config", None, None, "skipped", "No session configuration found") return - hermes_cfg_path = self.target_root / "config.yaml" - hermes_cfg = load_yaml_file(hermes_cfg_path) - sr = hermes_cfg.get("session_reset") or {} - changes = False - - # OpenClaw uses session.reset (structured) and session.resetTriggers (string array) - reset = session.get("reset") or {} - reset_triggers = session.get("resetTriggers") or session.get("reset_triggers") or [] - - if reset: - # Structured reset config: has mode, atHour, idleMinutes - mode = reset.get("mode", "") - if mode == "daily": - sr["mode"] = "daily" - elif mode == "idle": - sr["mode"] = "idle" - else: - sr["mode"] = mode or "none" - if reset.get("atHour") is not None: - sr["at_hour"] = reset["atHour"] - if reset.get("idleMinutes"): - sr["idle_minutes"] = reset["idleMinutes"] - changes = True - elif isinstance(reset_triggers, list) and reset_triggers: - # Simple string triggers: ["daily", "idle"] - has_daily = "daily" in reset_triggers - has_idle = "idle" in reset_triggers - if has_daily and has_idle: - sr["mode"] = "both" - elif has_daily: - sr["mode"] = "daily" - elif has_idle: - sr["mode"] = "idle" - changes = True - - if changes: - hermes_cfg["session_reset"] = sr - if self.execute: - self.maybe_backup(hermes_cfg_path) - dump_yaml_file(hermes_cfg_path, hermes_cfg) - self.record("session-config", "openclaw.json session.resetTriggers", - "config.yaml session_reset", "migrated") + if session.get("reset") or session.get("resetTriggers") or session.get("reset_triggers"): + self.record("session-config", "session reset timers", None, "skipped", + "Hermes conversations do not reset on idle or daily timers") # Archive full session config (identity links, thread bindings, etc.) complex_keys = {"identityLinks", "threadBindings", "maintenance", "scope", "sendPolicy"} diff --git a/website/docs/developer-guide/gateway-internals.md b/website/docs/developer-guide/gateway-internals.md index 2b7b5b378c..7982c62c81 100644 --- a/website/docs/developer-guide/gateway-internals.md +++ b/website/docs/developer-guide/gateway-internals.md @@ -248,8 +248,8 @@ When a session is reset, resumed, or expires: The gateway runs periodic maintenance alongside message handling: - **Cron ticking** — checks job schedules and fires due jobs -- **Session expiry** — cleans up abandoned sessions after timeout -- **Memory flush** — proactively flushes memory before session expiry +- **Session housekeeping** — reclaims cached resources without ending transcripts +- **Memory flush** — commits memory before soft cache eviction - **Cache refresh** — refreshes model lists and provider status ## Process Management diff --git a/website/docs/guides/migrate-from-openclaw.md b/website/docs/guides/migrate-from-openclaw.md index 9680717dc3..3a729ee75f 100644 --- a/website/docs/guides/migrate-from-openclaw.md +++ b/website/docs/guides/migrate-from-openclaw.md @@ -96,15 +96,9 @@ Skill conflicts are handled by `--skill-conflict`: `skip` leaves the existing He | Docker sandbox | `agents.defaults.sandbox.backend` | `terminal.backend` | "docker" → "docker" | | Docker image | `agents.defaults.sandbox.docker.image` | `terminal.docker_image` | Direct copy | -### Session reset policies +### Session lifetime -| OpenClaw config path | Hermes config path | Notes | -|---------------------|-------------------|-------| -| `session.reset.mode` | `session_reset.mode` | "daily", "idle", or both | -| `session.reset.atHour` | `session_reset.at_hour` | Hour (0–23) for daily reset | -| `session.reset.idleMinutes` | `session_reset.idle_minutes` | Minutes of inactivity | - -Note: OpenClaw also has `session.resetTriggers` (a simple string array like `["daily", "idle"]`). If the structured `session.reset` isn't present, the migration falls back to inferring from `resetTriggers`. +Idle and daily reset timers are not imported: Hermes conversations persist until an explicit `/new` or `/reset`. Advanced session settings (identity links, thread bindings, maintenance, scope and send policy) remain archived for reference. ### MCP servers @@ -233,7 +227,7 @@ The migration resolves all three formats. For env templates and SecretRef object 5. **Test messaging** — if you migrated platform tokens, restart the gateway: `systemctl --user restart hermes-gateway` -6. **Check session policies** — run `hermes config show` and verify the `session_reset` value matches your expectations. +6. **Check session archives** — review archived advanced settings; idle and daily reset timers are intentionally not imported. 7. **Re-pair WhatsApp** — WhatsApp uses QR code pairing (Baileys), not token migration. Run `hermes whatsapp` to pair. diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index 63734a2d6a..25a400ece1 100644 --- a/website/docs/reference/cli-commands.md +++ b/website/docs/reference/cli-commands.md @@ -1620,7 +1620,7 @@ Migrate your OpenClaw setup to Hermes. Reads from `~/.openclaw` (or a custom pat The migration covers 30+ categories across persona, memory, skills, model providers, messaging platforms, agent behavior, session policies, MCP servers, TTS, and more. Items are either **directly imported** into Hermes equivalents or **archived** for manual review. -**Directly imported:** SOUL.md, MEMORY.md, USER.md, AGENTS.md, skills (4 source directories), default model, custom providers, MCP servers, messaging platform tokens and allowlists (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost), agent defaults (reasoning effort, compression, human delay, timezone, sandbox), session reset policies, approval rules, TTS config, browser settings, tool settings, exec timeout, command allowlist, gateway config, and API keys from 3 sources. +**Directly imported:** SOUL.md, MEMORY.md, USER.md, AGENTS.md, skills (4 source directories), default model, custom providers, MCP servers, messaging platform tokens and allowlists (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost), agent defaults (reasoning effort, compression, human delay, timezone, sandbox), approval rules, TTS config, browser settings, tool settings, exec timeout, command allowlist, gateway config, and API keys from 3 sources. **Archived for manual review:** Cron jobs, plugins, hooks/webhooks, memory backend (QMD), skills registry config, UI/identity, logging, multi-agent setup, channel bindings, IDENTITY.md, TOOLS.md, HEARTBEAT.md, BOOTSTRAP.md. diff --git a/website/docs/user-guide/messaging/slack.md b/website/docs/user-guide/messaging/slack.md index 6a0a396571..85f1f817ff 100644 --- a/website/docs/user-guide/messaging/slack.md +++ b/website/docs/user-guide/messaging/slack.md @@ -1024,7 +1024,7 @@ slack: Notes: - The binding matches by channel ID. For threaded messages in a bound channel, the thread inherits the parent channel's binding. -- The skill is loaded only at session start (new session or after auto-reset). If you change the binding, run `/new` or wait for the session to auto-reset for it to take effect. +- The skill is loaded only at session start (new session). If you change the binding, run `/new` for it to take effect. - Combine with `channel_prompts` for per-channel tone/constraints on top of the skill's instructions. ## Troubleshooting diff --git a/website/docs/user-guide/sessions.md b/website/docs/user-guide/sessions.md index 45a65a61a1..bd9768d5ec 100644 --- a/website/docs/user-guide/sessions.md +++ b/website/docs/user-guide/sessions.md @@ -870,7 +870,7 @@ Key tables in `state.db`: ### Automatic Cleanup -- Gateway sessions auto-reset based on the configured reset policy +- Gateway conversations persist across inactivity; use `/new` or `/reset` for an explicit boundary - Before reset, the agent saves memories and skills from the expiring session - Auto-pruning (**on by default** since #54189): when `sessions.auto_prune` is `true`, ended sessions inactive for `sessions.retention_days` (default 90) are pruned at CLI/gateway/cron startup - After a prune that actually removed rows, `state.db` is `VACUUM`ed to reclaim disk space only when **both** gates pass: at least `sessions.min_vacuum_interval_days` (default 30) have elapsed since the last successful `VACUUM`, **and** more than 25% of the file's pages are reclaimable (`PRAGMA freelist_count / page_count`). A dense database never pays for a full rewrite to reclaim a few MB (SQLite does not shrink the file on plain DELETE) diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/gateway-internals.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/gateway-internals.md index e560e44b4c..60db451e9d 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/gateway-internals.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/gateway-internals.md @@ -235,7 +235,7 @@ AIAgent._invoke_tool() ### 内存刷写生命周期 -当会话被重置、恢复或过期时: +当会话被重置或恢复时: 1. 内置内存刷写至磁盘 2. 内存提供者的 `on_session_end()` hook 触发 3. 临时 `AIAgent` 运行仅含内存的对话轮次 @@ -246,8 +246,8 @@ AIAgent._invoke_tool() Gateway 在处理消息的同时运行周期性维护任务: - **Cron 计时** — 检查任务计划并触发到期任务 -- **会话过期** — 超时后清理废弃会话 -- **内存刷写** — 在会话过期前主动刷写内存 +- **会话维护** — 回收缓存资源,但不结束会话记录 +- **内存刷写** — 在软释放缓存前提交记忆 - **缓存刷新** — 刷新模型列表和提供者状态 ## 进程管理 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/migrate-from-openclaw.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/migrate-from-openclaw.md index 5827597754..65fcdbec89 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/migrate-from-openclaw.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/migrate-from-openclaw.md @@ -88,15 +88,9 @@ Skill 冲突由 `--skill-conflict` 处理:`skip` 保留现有 Hermes skill,` | Docker 沙箱 | `agents.defaults.sandbox.backend` | `terminal.backend` | "docker" → "docker" | | Docker 镜像 | `agents.defaults.sandbox.docker.image` | `terminal.docker_image` | 直接复制 | -### 会话重置策略 +### 会话生命周期 -| OpenClaw 配置路径 | Hermes 配置路径 | 备注 | -|---------------------|-------------------|-------| -| `session.reset.mode` | `session_reset.mode` | "daily"、"idle" 或两者 | -| `session.reset.atHour` | `session_reset.at_hour` | 每日重置的小时(0–23) | -| `session.reset.idleMinutes` | `session_reset.idle_minutes` | 不活跃分钟数 | - -注意:OpenClaw 还有 `session.resetTriggers`(简单字符串数组,如 `["daily", "idle"]`)。若结构化的 `session.reset` 不存在,迁移将回退到从 `resetTriggers` 推断。 +不会导入空闲或每日重置计时器。Hermes 会话会持续保留,直到显式使用 `/new` 或 `/reset`。身份关联、线程绑定、维护、作用域和发送策略等高级设置仍会归档以供参考。 ### MCP 服务器 @@ -225,7 +219,7 @@ OpenClaw 配置中 token 和 API 密钥的值支持三种格式: 5. **测试消息平台** — 若迁移了平台 token,重启 gateway:`systemctl --user restart hermes-gateway` -6. **检查会话策略** — 验证 `hermes config get session_reset` 是否符合预期。 +6. **检查会话归档** — 查看已归档的高级设置;空闲和每日重置计时器不会导入。 7. **重新配对 WhatsApp** — WhatsApp 使用二维码配对(Baileys),不支持 token 迁移。运行 `hermes whatsapp` 进行配对。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/cli-commands.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/cli-commands.md index 70611ad70b..cf2e8f6d01 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/cli-commands.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/cli-commands.md @@ -1112,7 +1112,7 @@ hermes claw migrate [options] 迁移涵盖 30+ 个类别,包括 persona、memory、skill、模型 provider、消息平台、agent 行为、会话策略、MCP 服务器、TTS 等。条目要么**直接导入**到 Hermes 等效项,要么**归档**以供手动审查。 -**直接导入:** SOUL.md、MEMORY.md、USER.md、AGENTS.md、skill(4 个源目录)、默认模型、自定义 provider、MCP 服务器、消息平台 token 和许可名单(Telegram、Discord、Slack、WhatsApp、Signal、Matrix、Mattermost)、agent 默认值(推理努力程度、压缩、人工延迟、时区、沙箱)、会话重置策略、审批规则、TTS 配置、浏览器设置、工具设置、执行超时、命令许可名单、gateway 配置以及来自 3 个来源的 API 密钥。 +**直接导入:** SOUL.md、MEMORY.md、USER.md、AGENTS.md、skill(4 个源目录)、默认模型、自定义 provider、MCP 服务器、消息平台 token 和许可名单(Telegram、Discord、Slack、WhatsApp、Signal、Matrix、Mattermost)、agent 默认值(推理努力程度、压缩、人工延迟、时区、沙箱)、审批规则、TTS 配置、浏览器设置、工具设置、执行超时、命令许可名单、gateway 配置以及来自 3 个来源的 API 密钥。 **归档以供手动审查:** Cron 任务、plugin、hook/webhook、memory 后端(QMD)、skill 注册表配置、UI/身份、日志、多 agent 设置、频道绑定、IDENTITY.md、TOOLS.md、HEARTBEAT.md、BOOTSTRAP.md。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/slack.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/slack.md index e823deb5f2..8f0d812b6d 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/slack.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/slack.md @@ -567,7 +567,7 @@ slack: 注意事项: - 绑定按频道 ID 匹配。对于绑定频道中的话题消息,话题继承父频道的绑定。 -- 技能仅在会话开始时加载(新会话或自动重置后)。如果更改绑定,请运行 `/new` 或等待会话自动重置以使其生效。 +- 技能仅在会话开始时加载(新会话开始时)。如果更改绑定,请运行 `/new` 使其生效。 - 与 `channel_prompts` 结合使用,可在技能指令之上为每个频道设置语气/约束。 ## 故障排除 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md index c92acb87e2..40fe14bc98 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md @@ -633,7 +633,7 @@ platforms: 带有 `skill` 字段的话题会在该话题中新会话开始时自动加载该技能。这与在对话开始时输入 `/skill-name` 完全相同——技能内容会注入到第一条消息中,后续消息在对话历史中可以看到它。 -例如,带有 `skill: arxiv` 的话题会在其会话重置时(由于空闲超时、每日重置或手动 `/reset`)预加载 arxiv 技能。 +例如,带有 `skill: arxiv` 的话题会在其会话重置时(手动 `/new` 或 `/reset`)预加载 arxiv 技能。 :::tip 在配置之外创建的话题(例如通过手动调用 Telegram API)会在 `forum_topic_created` 服务消息到达时自动被发现。你也可以在 gateway 运行时向配置中添加话题——它们会在下次缓存未命中时被拾取。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/sessions.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/sessions.md index 471cb89187..376f97432c 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/sessions.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/sessions.md @@ -571,7 +571,7 @@ state.db 后可安全删除。 ### 自动清理 -- Gateway session 根据配置的重置策略自动重置 +- Gateway 会话会持续保留;请使用 `/new` 或 `/reset` 显式开始新会话 - 重置前,agent 保存即将过期 session 中的记忆和技能 - 可选自动清理:当 `sessions.auto_prune` 为 `true` 时,在 CLI/gateway 启动时清理早于 `sessions.retention_days`(默认 90)天的已结束 session - 实际删除了行的清理操作完成后,如果距离上次成功执行 `VACUUM` 已达到 `sessions.min_vacuum_interval_days`(默认 30)天,`state.db` 会执行 `VACUUM` 以回收磁盘空间(SQLite 在普通 DELETE 后不会缩小文件)