From 4c0460a902ad45534e1a8a05a83926feaedb4915 Mon Sep 17 00:00:00 2001 From: webtecnica Date: Mon, 27 Jul 2026 13:14:27 -0300 Subject: [PATCH 1/4] fix(skill): update Himalaya skill to v2.0.0 config schema (#72734) --- skills/email/himalaya/SKILL.md | 81 ++++----- .../himalaya/references/configuration.md | 156 +++++++----------- 2 files changed, 96 insertions(+), 141 deletions(-) diff --git a/skills/email/himalaya/SKILL.md b/skills/email/himalaya/SKILL.md index c35f264648466..8962ed054dac4 100644 --- a/skills/email/himalaya/SKILL.md +++ b/skills/email/himalaya/SKILL.md @@ -1,7 +1,7 @@ --- name: himalaya description: "Himalaya CLI: IMAP/SMTP email from terminal." -version: 1.1.0 +version: 2.0.0 author: community license: MIT platforms: [linux, macos, windows] @@ -48,13 +48,20 @@ cargo install himalaya --locked ## Configuration Setup -Run the interactive wizard to set up an account: +Run the interactive wizard (bare `himalaya` — no subcommand) to set up an account: ```bash -himalaya account configure +himalaya ``` -Or create `~/.config/himalaya/config.toml` manually: +The wizard tests IMAP and SMTP connectivity, then prints a ready-to-save +TOML config to stdout. Redirect it directly: + +```bash +himalaya > ~/.config/himalaya/config.toml +``` + +Or create `~/.config/himalaya/config.toml` manually using per-backend tables: ```toml [accounts.personal] @@ -62,56 +69,41 @@ email = "you@example.com" display-name = "Your Name" default = true -backend.type = "imap" -backend.host = "imap.example.com" -backend.port = 993 -backend.encryption.type = "tls" -backend.login = "you@example.com" -backend.auth.type = "password" -backend.auth.cmd = "pass show email/imap" # or use keyring - -message.send.backend.type = "smtp" -message.send.backend.host = "smtp.example.com" -message.send.backend.port = 587 -message.send.backend.encryption.type = "start-tls" -message.send.backend.login = "you@example.com" -message.send.backend.auth.type = "password" -message.send.backend.auth.cmd = "pass show email/smtp" - -# Folder aliases (himalaya v1.2.0+ syntax). Required whenever the -# server's folder names don't match himalaya's canonical names -# (inbox/sent/drafts/trash). Gmail is the common case — see -# `references/configuration.md` for the `[Gmail]/Sent Mail` mapping. -folder.aliases.inbox = "INBOX" -folder.aliases.sent = "Sent" -folder.aliases.drafts = "Drafts" -folder.aliases.trash = "Trash" +imap.server = "imap.example.com:993" +imap.sasl.plain.username = "you@example.com" +imap.sasl.plain.password.raw = "your-password" + +smtp.server = "smtp.example.com:587" +smtp.starttls = true +smtp.sasl.plain.username = "you@example.com" +smtp.sasl.plain.password.raw = "your-password" + +mailbox.alias.inbox = "INBOX" +mailbox.alias.sent = "Sent" +mailbox.alias.drafts = "Drafts" +mailbox.alias.trash = "Trash" ``` -> **Heads up on the alias syntax.** Pre-v1.2.0 docs used a -> `[accounts.NAME.folder.alias]` sub-section (singular `alias`). -> v1.2.0 silently ignores that form — TOML parses fine, but the -> alias resolver never reads it, so every lookup falls through to -> the canonical name. On Gmail this means save-to-Sent fails *after* -> SMTP delivery succeeds, and `himalaya message send` exits non-zero. -> Any caller (agent, script, user) that retries on that exit code -> will re-run the entire send — including SMTP — producing duplicate -> emails to recipients. Always use `folder.aliases.X` (plural, dotted -> keys, directly under `[accounts.NAME]`). +> **Heads up on the alias syntax.** Himalaya v2.0.0 renamed +> `folder.aliases.*` to `mailbox.alias.*`. The old dotted keys are +> silently ignored — TOML parses fine, but the alias resolver never reads +> them. On Gmail this means save-to-Sent fails *after* SMTP delivery +> succeeds, and `himalaya message send` exits non-zero. Always use +> `mailbox.alias.X` in v2.0.0+. ## Hermes Integration Notes - **Reading, listing, searching, moving, deleting** all work directly through the terminal tool - **Composing/replying/forwarding** — piped input (`cat << EOF | himalaya template send`) is recommended for reliability. Interactive `$EDITOR` mode works with `pty=true` + background + process tool, but requires knowing the editor and its commands -- Use `--output json` for structured output that's easier to parse programmatically -- The `himalaya account configure` wizard requires interactive input — use PTY mode: `terminal(command="himalaya account configure", pty=true)` +- Use `--json` before the subcommand for structured output that's easier to parse programmatically (e.g. `himalaya --json envelope list`) +- The bare `himalaya` wizard requires interactive input — use PTY mode: `terminal(command="himalaya", pty=true)` ## Common Operations -### List Folders +### List Mailboxes ```bash -himalaya folder list +himalaya mailbox list ``` ### List Emails @@ -275,11 +267,10 @@ himalaya attachment download 42 --downloads-dir ~/Downloads ## Output Formats -Most commands support `--output` for structured output: +Most commands support `--json` (before the subcommand) for structured output: ```bash -himalaya envelope list --output json -himalaya envelope list --output plain +himalaya --json envelope list ``` ## Debugging diff --git a/skills/email/himalaya/references/configuration.md b/skills/email/himalaya/references/configuration.md index 5ccba6cbc3211..adf5161836e8c 100644 --- a/skills/email/himalaya/references/configuration.md +++ b/skills/email/himalaya/references/configuration.md @@ -11,29 +11,22 @@ display-name = "Your Name" default = true # IMAP backend for reading emails -backend.type = "imap" -backend.host = "imap.example.com" -backend.port = 993 -backend.encryption.type = "tls" -backend.login = "user@example.com" -backend.auth.type = "password" -backend.auth.raw = "your-password" +imap.server = "imap.example.com:993" +imap.sasl.plain.username = "user@example.com" +imap.sasl.plain.password.raw = "your-password" # SMTP backend for sending emails -message.send.backend.type = "smtp" -message.send.backend.host = "smtp.example.com" -message.send.backend.port = 587 -message.send.backend.encryption.type = "start-tls" -message.send.backend.login = "user@example.com" -message.send.backend.auth.type = "password" -message.send.backend.auth.raw = "your-password" - -# Folder aliases — required whenever server folder names differ -# from himalaya's canonical names. See "Folder Aliases" below. -folder.aliases.inbox = "INBOX" -folder.aliases.sent = "Sent" -folder.aliases.drafts = "Drafts" -folder.aliases.trash = "Trash" +smtp.server = "smtp.example.com:587" +smtp.starttls = true +smtp.sasl.plain.username = "user@example.com" +smtp.sasl.plain.password.raw = "your-password" + +# Mailbox aliases — required whenever server folder names differ +# from himalaya's canonical names. See "Mailbox Aliases" below. +mailbox.alias.inbox = "INBOX" +mailbox.alias.sent = "Sent" +mailbox.alias.drafts = "Drafts" +mailbox.alias.trash = "Trash" ``` ## Password Options @@ -41,23 +34,18 @@ folder.aliases.trash = "Trash" ### Raw password (testing only, not recommended) ```toml -backend.auth.raw = "your-password" +imap.sasl.plain.password.raw = "your-password" +# smtp.sasl.plain.password.raw = "your-password" ``` ### Password from command (recommended) ```toml -backend.auth.cmd = "pass show email/imap" -# backend.auth.cmd = "security find-generic-password -a user@example.com -s imap -w" +imap.sasl.plain.password.cmd = "pass show email/imap" +# imap.sasl.plain.password.cmd = "security find-generic-password -a user@example.com -s imap -w" ``` -### System keyring (requires keyring feature) - -```toml -backend.auth.keyring = "imap-example" -``` - -Then run `himalaya account configure ` to store the password. +Then run `himalaya` to set up an account (the wizard prints a ready-to-save TOML config). ## Gmail Configuration @@ -67,31 +55,24 @@ email = "you@gmail.com" display-name = "Your Name" default = true -backend.type = "imap" -backend.host = "imap.gmail.com" -backend.port = 993 -backend.encryption.type = "tls" -backend.login = "you@gmail.com" -backend.auth.type = "password" -backend.auth.cmd = "pass show google/app-password" - -message.send.backend.type = "smtp" -message.send.backend.host = "smtp.gmail.com" -message.send.backend.port = 587 -message.send.backend.encryption.type = "start-tls" -message.send.backend.login = "you@gmail.com" -message.send.backend.auth.type = "password" -message.send.backend.auth.cmd = "pass show google/app-password" +imap.server = "imap.gmail.com:993" +imap.sasl.plain.username = "you@gmail.com" +imap.sasl.plain.password.raw = "app-password" + +smtp.server = "smtp.gmail.com:587" +smtp.starttls = true +smtp.sasl.plain.username = "you@gmail.com" +smtp.sasl.plain.password.raw = "app-password" # Gmail folder mapping. Without these, save-to-Sent fails after # SMTP delivery succeeds (Gmail's Sent folder is `[Gmail]/Sent Mail`, # not `Sent`), and `himalaya message send` exits non-zero. Any # caller that retries on that error will re-run SMTP — duplicate # emails to recipients. Always include this block for Gmail. -folder.aliases.inbox = "INBOX" -folder.aliases.sent = "[Gmail]/Sent Mail" -folder.aliases.drafts = "[Gmail]/Drafts" -folder.aliases.trash = "[Gmail]/Trash" +mailbox.alias.inbox = "INBOX" +mailbox.alias.sent = "[Gmail]/Sent Mail" +mailbox.alias.drafts = "[Gmail]/Drafts" +mailbox.alias.trash = "[Gmail]/Trash" ``` **Note:** Gmail requires an App Password if 2FA is enabled. @@ -103,62 +84,47 @@ folder.aliases.trash = "[Gmail]/Trash" email = "you@icloud.com" display-name = "Your Name" -backend.type = "imap" -backend.host = "imap.mail.me.com" -backend.port = 993 -backend.encryption.type = "tls" -backend.login = "you@icloud.com" -backend.auth.type = "password" -backend.auth.cmd = "pass show icloud/app-password" - -message.send.backend.type = "smtp" -message.send.backend.host = "smtp.mail.me.com" -message.send.backend.port = 587 -message.send.backend.encryption.type = "start-tls" -message.send.backend.login = "you@icloud.com" -message.send.backend.auth.type = "password" -message.send.backend.auth.cmd = "pass show icloud/app-password" +imap.server = "imap.mail.me.com:993" +imap.sasl.plain.username = "you@icloud.com" +imap.sasl.plain.password.raw = "app-password" + +smtp.server = "smtp.mail.me.com:587" +smtp.starttls = true +smtp.sasl.plain.username = "you@icloud.com" +smtp.sasl.plain.password.raw = "app-password" ``` **Note:** Generate an app-specific password at appleid.apple.com -## Folder Aliases +## Mailbox Aliases -Map himalaya's canonical folder names (`inbox`, `sent`, `drafts`, -`trash`) to whatever the server actually calls them. Use the -v1.2.0 `folder.aliases.X` syntax (plural, dotted keys, directly -under `[accounts.NAME]`): +Map himalaya's canonical mailbox names (`inbox`, `sent`, `drafts`, +`trash`) to whatever the server actually calls them: ```toml [accounts.default] # ... other account config ... -folder.aliases.inbox = "INBOX" -folder.aliases.sent = "Sent" -folder.aliases.drafts = "Drafts" -folder.aliases.trash = "Trash" +mailbox.alias.inbox = "INBOX" +mailbox.alias.sent = "Sent" +mailbox.alias.drafts = "Drafts" +mailbox.alias.trash = "Trash" ``` -The equivalent TOML sub-section form also works in v1.2.0: +The equivalent TOML sub-section form also works: ```toml -[accounts.default.folder.aliases] +[accounts.default.mailbox.aliases] inbox = "INBOX" sent = "Sent" drafts = "Drafts" trash = "Trash" ``` -> **Don't use the singular `alias` form.** Pre-v1.2.0 docs showed -> `[accounts.NAME.folder.alias]` (singular). v1.2.0 silently -> ignores that sub-section — TOML parses without error, but the -> alias resolver never reads it. Every lookup then falls through -> to the canonical name. On Gmail (where `sent` is actually -> `[Gmail]/Sent Mail`) this means save-to-Sent fails *after* SMTP -> delivery succeeds, and `himalaya message send` exits non-zero. -> Any caller (agent, script, user) that retries on that error -> code will re-run the send — including SMTP — producing duplicate -> emails to recipients. Always use `folder.aliases.X` (plural). +> **Note on v2.0.0 change.** Himalaya v2.0.0 renamed `folder.aliases.*` +> to `mailbox.alias.*`. If you are upgrading from v1.x, update your +> config accordingly — the old `folder.aliases.*` keys are ignored by +> v2.0.0. ## Multiple Accounts @@ -184,21 +150,19 @@ himalaya --account work envelope list ```toml [accounts.local] email = "user@example.com" - -backend.type = "notmuch" -backend.db-path = "~/.mail/.notmuch" +# Config structure for notmuch differs — see himalaya docs. ``` ## OAuth2 Authentication (for providers that support it) ```toml -backend.auth.type = "oauth2" -backend.auth.client-id = "your-client-id" -backend.auth.client-secret.cmd = "pass show oauth/client-secret" -backend.auth.access-token.cmd = "pass show oauth/access-token" -backend.auth.refresh-token.cmd = "pass show oauth/refresh-token" -backend.auth.auth-url = "https://provider.com/oauth/authorize" -backend.auth.token-url = "https://provider.com/oauth/token" +# IMAP SASL OAuth2 +imap.sasl.oauth2.client-id = "your-client-id" +imap.sasl.oauth2.client-secret.cmd = "pass show oauth/client-secret" +imap.sasl.oauth2.access-token.cmd = "pass show oauth/access-token" +imap.sasl.oauth2.refresh-token.cmd = "pass show oauth/refresh-token" +imap.sasl.oauth2.auth-url = "https://provider.com/oauth/authorize" +imap.sasl.oauth2.token-url = "https://provider.com/oauth/token" ``` ## Additional Options From a8bf938d6e2a5c27a3f4789478163d8688a7fc60 Mon Sep 17 00:00:00 2001 From: webtecnica Date: Mon, 27 Jul 2026 15:02:24 -0300 Subject: [PATCH 2/4] fix(cli): display model name correctly in footer (#72777) --- cli.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/cli.py b/cli.py index d7042309a0001..fede74cea4b8c 100644 --- a/cli.py +++ b/cli.py @@ -129,10 +129,13 @@ def format_duration_compact(*args, **kwargs): def _reverse_alias_for_display(model_name: str) -> str: """Return the shortest configured alias for ``model_name``, or ``model_name``. - Looks up both ``model_aliases:`` (dict-based, full DirectAlias entries) - and ``model.aliases:`` (string-based, set via ``hermes config set``) - from config.yaml. Multiple aliases pointing at the same model — the - shortest wins, so ``opus47`` beats ``palantir-claude47``. + Only looks up ``model_aliases:`` (dict-based, full DirectAlias entries + intentionally set by the user for display purposes) — NOT + ``model.aliases:`` (string-based aliases auto-generated by the model + picker, which are convenience shorthands for resolution and typically + *less* readable than the model name itself). Multiple aliases pointing + at the same model: the shortest wins, so ``opus47`` beats + ``palantir-claude47``. """ global _REVERSE_ALIAS_CACHE if not model_name: @@ -149,16 +152,6 @@ def _reverse_alias_for_display(model_name: str) -> str: m = str(entry.get("model", "") or "").strip() if m and (m not in rmap or len(alias) < len(rmap[m])): rmap[m] = alias - mdl = cfg.get("model", {}) or {} - if isinstance(mdl, dict): - simple = mdl.get("aliases") - if isinstance(simple, dict): - for alias, val in simple.items(): - if isinstance(val, str) and val.strip(): - v = val.strip() - m = v.split("/", 1)[1] if "/" in v else v - if m and (m not in rmap or len(alias) < len(rmap[m])): - rmap[m] = alias except Exception: pass _REVERSE_ALIAS_CACHE = rmap From 888cbfc80e878252778492742dad57a38722a25c Mon Sep 17 00:00:00 2001 From: webtecnica Date: Mon, 27 Jul 2026 15:05:22 -0300 Subject: [PATCH 3/4] fix(plugins/photon): clear stale token and re-enable channel after setup (#72763) Two related bugs in hermes photon setup / gateway_setup: Bug 1 -- stale token reused (401) ---------------------------------- _cmd_setup reused an existing dashboard token without validation. The device token has a short TTL (~3-4 days observed); reusing a stale token caused every management API call (find_project_by_name, regenerate_project_secret, etc.) to fail with 401. The operator saw confusing "spectrum provisioning failed: 401" errors. Fix: check GET /api/auth/get-session before using the stored token. On 401/403, clear the stale token with clear_photon_token() and fall back to a fresh device-login flow automatically. Bug 2 -- channel left disabled after successful setup ----------------------------------------------------- After all five provisioning steps completed, config.yaml still had photon.enabled: false, so the gateway never loaded the Photon adapter. Every inbound iMessage hit Photon's offline auto-responder without the operator being notified. Fix: call write_platform_config_field('photon', 'enabled', True, raw=True) as a final setup step so the gateway picks up the freshly configured channel on its next start. New public API in auth.py: - clear_photon_token() -- discard stored token from auth.json - check_photon_token_valid(token) -- lightweight session-check test References: #72763 --- plugins/platforms/photon/auth.py | 39 ++++++++++++++++++++++++++++++++ plugins/platforms/photon/cli.py | 28 +++++++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/plugins/platforms/photon/auth.py b/plugins/platforms/photon/auth.py index e4e2421538bac..13feddb1b2d98 100644 --- a/plugins/platforms/photon/auth.py +++ b/plugins/platforms/photon/auth.py @@ -143,6 +143,45 @@ def store_photon_token(token: str) -> None: _save_auth(auth) +def clear_photon_token() -> None: + """Remove any stored Photon dashboard token from auth.json. + + Used to discard a stale/expired token before re-authentication. + """ + auth = _load_auth() + pool = auth.get("credential_pool", {}) + photon = pool.get("photon", []) + if isinstance(photon, list) and photon: + pool["photon"] = [] + _save_auth(auth) + # Also clear the legacy shape if present. + providers = auth.get("providers", {}) + if "photon" in providers: + providers["photon"] = {} + _save_auth(auth) + + +def check_photon_token_valid(token: str) -> bool: + """Return True if the token is accepted by the dashboard API. + + Makes a lightweight ``GET /api/auth/get-session`` call. A non-401 + response (including non-auth errors like network blips) is treated as + "probably valid" so transient failures don't force unnecessary re-login. + Only a definitive 401 / 403 is treated as stale. + """ + if not token: + return False + try: + resp = _dashboard_get("/api/auth/get-session", token) + if resp.status_code in (401, 403): + return False + return True + except Exception: + # Transient error — don't force a re-auth; let the caller's + # management-call error propagate if the token really is bad. + return True + + def load_project_credentials() -> Tuple[Optional[str], Optional[str]]: """Return the runtime SDK creds ``(spectrum_project_id, project_secret)``. diff --git a/plugins/platforms/photon/cli.py b/plugins/platforms/photon/cli.py index 89e1c6bc8bc4d..af8b334418881 100644 --- a/plugins/platforms/photon/cli.py +++ b/plugins/platforms/photon/cli.py @@ -127,10 +127,23 @@ def _print_code(code): def _cmd_setup(args: argparse.Namespace) -> int: - # 1. Login (skip if we already have a token). + # 1. Login (skip if we already have a valid token). token = photon_auth.load_photon_token() + if token: + # Validate the existing token — the dashboard token has a short TTL + # and can go stale between runs (observed: ~3-4 days). Reusing a + # stale token causes every management call to fail with 401 and + # leaves the operator confused about why setup "succeeds" but nothing + # works. Check upfront so we fail fast and fall back to fresh login. + print("[1/5] Checking existing Photon token...") + if photon_auth.check_photon_token_valid(token): + print(" ✓ token is valid") + else: + print(" ✗ token is stale (dashboard rejected it) — re-authenticating") + photon_auth.clear_photon_token() + token = None if not token: - print("[1/5] No Photon token found — running device login...") + print("[1/5] No valid Photon token found — running device login...") rc = _run_device_login(args) if rc != 0: return rc @@ -270,6 +283,17 @@ def _cmd_setup(args: argparse.Namespace) -> int: if rc != 0: return rc + # 7. Ensure the photon platform is enabled in config.yaml so the + # gateway loads it on next start. Without this the channel stays + # disabled even after a successful provisioning run, silently + # keeping iMessage offline. + try: + from hermes_cli.config import write_platform_config_field + write_platform_config_field("photon", "enabled", True, raw=True) + print(" ✓ photon platform enabled in config.yaml") + except Exception as e: + print(f" (could not enable Photon in config: {e})", file=sys.stderr) + print() print("✓ Photon setup complete.") print(" Start the gateway: hermes gateway start") From 20fb735f02cc6064f46578a344fe0fc5ebe92547 Mon Sep 17 00:00:00 2001 From: webtecnica Date: Mon, 27 Jul 2026 15:06:02 -0300 Subject: [PATCH 4/4] fix: close SessionDB FDs on timeout and lazy recall paths (#72782) --- agent/agent_init.py | 1 + cron/scheduler.py | 30 +++++++++++++++++++++++++++++- run_agent.py | 12 ++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/agent/agent_init.py b/agent/agent_init.py index 5c022f4f6fb75..ba676e03216f3 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -1517,6 +1517,7 @@ def init_agent( # SQLite session store (optional -- provided by CLI or gateway) agent._session_db = session_db + agent._owns_session_db = False # caller-injected; do not close on teardown agent._parent_session_id = parent_session_id # A close flush and the worker's turn-start flush can overlap. The durable # marker is attached to each in-memory message dict, so its test-and-append diff --git a/cron/scheduler.py b/cron/scheduler.py index 91c3168268ce5..7e2f3f460530a 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -48,6 +48,23 @@ logger = logging.getLogger(__name__) +def _close_late_session_db_fds(future: "concurrent.futures.Future") -> None: + """Done-callback: retrieve and close a SessionDB that completed after its timeout. + + When ``run_job``'s SessionDB init times out, the worker thread is abandoned + and the future's eventual result would leak SQLite FDs for the process + lifetime. This callback is attached to that future so that if/when the + constructor completes, the SessionDB (and its open .db / WAL / SHM handles) + are immediately closed. + """ + try: + db = future.result(timeout=60) + if db is not None: + db.close() + except Exception: + pass + + def _set_cron_session_title(session_db, session_id, base_title): """Robustly title a finished cron session before it is closed. @@ -2927,8 +2944,19 @@ def run_job( if _session_db_timeout > 0: _session_db_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) + _session_db_future = _session_db_pool.submit(SessionDB) try: - _session_db = _session_db_pool.submit(SessionDB).result(timeout=_session_db_timeout) + _session_db = _session_db_future.result(timeout=_session_db_timeout) + except concurrent.futures.TimeoutError: + # The worker is abandoned (shutdown below doesn't wait). If + # SessionDB() later completes inside it, the future's result + # would be orphaned and its SQLite FDs (.db, WAL, SHM) would + # leak until process exit. Register a done callback that + # retrieves and closes any eventual late result. + _session_db_future.add_done_callback( + lambda _f: _close_late_session_db_fds(_f) + ) + raise finally: # Don't wait for a wedged connect() to unwind — abandon the # worker thread (same pattern as the agent inactivity timeout diff --git a/run_agent.py b/run_agent.py index aabaf78b87b7c..fcb1835c0391f 100644 --- a/run_agent.py +++ b/run_agent.py @@ -599,6 +599,7 @@ def _get_session_db_for_recall(self): from hermes_state import SessionDB self._session_db = SessionDB() + self._owns_session_db = True return self._session_db except Exception as exc: logger.debug("SessionDB unavailable for recall", exc_info=True) @@ -3946,6 +3947,17 @@ def close(self) -> None: session_db.end_session(session_id, "agent_close") except Exception: pass + # 8. Close the SQLite session store if this agent owns it (lazy-recall + # path). Caller-injected gateway/CLI stores remain open; they are + # shared across agents and closed by their original owner. + try: + if getattr(self, "_owns_session_db", False): + db = getattr(self, "_session_db", None) + if db is not None: + db.close() + self._session_db = None + except Exception: + pass def _hydrate_todo_store(self, history: List[Dict[str, Any]]) -> None: """