Commit Graph

25 Commits

Author SHA1 Message Date
teknium1 991b23ad8d refactor(sessions): one session-id minter; QQ update-prompt key from build_session_key
Nine f-string sites minted `YYYYMMDD_HHMMSS_<hex>` independently with the hex width already
drifted (6 on CLI/TUI/agent/import, 8 in the gateway store, 12 in portability imports).
hermes_cli/session_lost_and_found.py classifies schema-less salvage rows by that shape, so a
site drifting the prefix would silently change recovery. hermes_state_ids.new_session_id(now,
hex_len=) is now the only writer and owns SESSION_ID_PATTERN; stdlib-only so agent/, cli.py and
gateway/ can import it without the SessionDB graph.

Widths are kept per site on purpose: the Desktop's session-id candidate regex is pinned to 6 hex
chars for interactive ids; the gateway store and portability importer keep 8/12 (more rows per
second). Not a bug, so not "fixed".

gateway/platforms/qqbot/adapter.py hard-coded `agent:main:qqbot:<scene>:<chat>` for the
update-prompt authz key, ignoring the profile namespace build_session_key applies; a secondary
bot in a multiplexed gateway got `agent:<profile>:...` keys and its clicks were rejected. The key
now comes from the one builder via BasePlatformAdapter._source_session_key.

Behavior change: QQ update-prompt clicks are authorized under the profile-namespaced key
(byte-identical `agent:main:` for the default profile).
2026-09-13 05:21:02 -07:00
TaoMasterCoder 10777823fe fix(sessions): salvage a page-1-header-damaged state.db in the lost_and_found lane
A SIGKILL mid-write (OOM killer, #106667) can leave state.db with a garbage
page-1 header. SQLite refuses the file outright ("file is not a database",
SQLITE_NOTADB) and the sqlite3 shell's .recover opens the file like any other
client, so the lost_and_found lane failed with the same rc=26 twice although
every data page after the header survived. The #106587 quarantine now
preserves such a file as state.db.notadb-<ts>-<pid>.bak; this makes that
preserved file recoverable with `hermes sessions recover --source <bak>
--allow-partial`.

When both .recover attempts fail with "not a database", zero the 100-byte
header of the lane's private snapshot copy and rerun them. .recover trips
only on the magic check and infers page size and layout from the pages
themselves, so a zeroed header is enough; a spliced donor header (the PR's
original mechanism) instead advertises a database size / freelist that
contradicts the file and yields "database disk image is malformed" on a
direct open — verified live on a 139-page fixture, which also showed the
zeroed header recovers 60/60 sessions and 300/300 messages whether the
damage covers 100 bytes or the whole first page. The user's file is never
written; the report carries `sqlite3_cli.header_zeroed` and a warning about
the WAL boundary.

Live repro (sqlite3 shell 3.53.1 on PATH, header overwritten with random
bytes): BEFORE "page-level .recover salvage failed: ... file is not a
database (26)"; AFTER "Recovered 60 sessions and 300 messages", source md5
unchanged.

Salvaged from PR #102808 (intent; trimmed from 302 to ~40 source LOC by
dropping the donor-header/page-size sweep and the redundant preopen probe —
the shell's own refusal is the detector). Independent review on the PR by
@strzhao.

Refs #106667
Refs #106587
Reported-by: TaoMasterCoder
Cross-referenced-by: kshitijk4poor
2026-09-11 06:37:27 -07:00
kshitijk4poor ead7e91dab refactor(recovery): stream the salvaged population; table the shape rules
- Pass 1 no longer materialises every classified record (full
  `messages.content` included) until pass 2; `LayoutEvidence` keeps only the
  capped per-position value sets (+ sessions rows for the one cross-column
  invariant) and pass 2 re-streams the lost_and_found tables. A 276 MB
  corrupted store no longer has to fit in memory.
- `_sentinel_holds` / `_text_shape_holds` if-ladders become rule tables.
- Tests trimmed to the three that bind behaviour (upgraded store maps by
  name; verifier refuses when rows matched no layout; replayed history ends
  at the current schema — the drift guard). No behaviour change; reverting
  inference to "no layout" still fails the name-mapping test.
2026-09-09 18:28:57 +05:30
kshitijk4poor 17c43aba06 fix(recovery): infer the salvaged store's physical layout from its schema history
A store's physical column order depends on which schema it was created at and
which ALTER TABLE ADD COLUMNs it lived through; one hardcoded "upgraded"
layout cannot cover them. `session_schema_history` records the declared
schema of the salvaged tables over time; `reachable_physical_layouts`
replays it to enumerate every physical order a store can have. The mapper
infers the layout once per kind from the whole recovered population (one
store wrote all of them), maps cells by name, and counts records whose width
matched no layout; the recovery verifier refuses to report such a salvage as
healthy.

Rebased onto the simplified session modules (c88d60551e, b9b4600cb2,
7e5a1a11d9, 1915a0d27e); no behaviour change from the pre-rebase branch.
2026-09-09 18:28:57 +05:30
Justin Wilson 7704712168 fix(sessions): map lost_and_found cells by physical column names
Upgraded state.db files gain columns via ALTER TABLE ADD COLUMN, so
physical order diverges from SCHEMA_SQL. Prefix-mapping onto the fresh
template put started_at at 0 and shifted titles/models. Insert by name
using known physical layouts; keep the plausibility gate.

Fixes #101409
2026-09-09 18:28:57 +05:30
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 c88d60551e refactor(hermes_cli): group module-level constants in session modules 2026-09-03 00:06:12 -07:00
Teknium b9b4600cb2 refactor(hermes_cli): lost_and_found attempt record built in one literal 2026-09-03 00:03:41 -07:00
Teknium 7e5a1a11d9 refactor(hermes_cli): lost_and_found direct copy reuses _count_rows 2026-09-02 23:45:26 -07:00
Teknium a0358185dd refactor(hermes_cli): lost_and_found lane literal compaction 2026-09-02 23:22:50 -07:00
Teknium 1915a0d27e refactor(hermes_cli): session_lost_and_found predicate and default-substitute tightening 2026-09-02 23:19:06 -07:00
kshitijk4poor 914d8a0bd6 fix(recovery): name the real state.db in the copy-pasteable recovery banners
The gateway broadcast and turn-failure explanation printed a literal
~/.hermes/state.db; now that the line is a command the operator is meant to
run as-is, interpolate _default_db_path() so profile / HERMES_HOME installs are
pointed at the store that actually failed. Also: split a comment that a merge
fused onto the logger line in session_lost_and_found.py, and fix an inverted
test docstring.
2026-09-03 11:28:21 +05:30
sal 5d9a2110ba fix(recovery): stop pointing sqlite3 .recover guidance at the live state.db
Refs #100368. The forensics thread established that a sqlite3 CLI with
the WAL-reset opener bug (fixed 3.51.3+ / backports 3.50.7 / 3.44.6;
Debian/Ubuntu system shells 3.45.1/3.46.1 are in the vulnerable band)
unlinks the live -wal/-shm pair when pointed at a live state.db whose
writer's DMS lock has been cancelled, splitting the store into two
concurrent generations whose acknowledged writes vanish while both
report integrity_check ok. Hermes' own corruption banners instructed
exactly that command.

- gateway corruption broadcast, run_agent corrupt-cause explanation,
  hermes_state repair-budget and forensic-backup refusals, and the
  kanban manual-recovery hint now route operators to
  `hermes sessions recover --source <db>` (which snapshots the damaged
  bundle before any shell touches it) and warn against a raw sqlite3
  shell on the live file
- find_sqlite3_cli() now refuses a WAL-reset-vulnerable shell for the
  page-level salvage lane even on the snapshot, reusing the canonical
  gate from hermes_cli.sqlite_runtime so the embedded runtime and the
  salvage shell can never disagree
- find_sqlite3_cli_refusal() records why a shell was refused so the
  lost_and_found lane can tell the operator exactly what to install
  instead of a generic "not found"
- regression tests cover the version gate (vulnerable/fixed matrix, the
  mirror check), every refusal reason, and each guidance site

Test plan:
- scripts/run_tests.sh tests/hermes_cli/test_sqlite3_cli_salvage_gate.py
  tests/test_state_db_repair_loop_cap.py
  tests/run_agent/test_corruption_recovery_guidance.py
  tests/hermes_cli/test_session_recovery_lost_and_found.py
  tests/hermes_cli/test_session_recovery.py tests/test_sqlite_wal_reset_gate.py
  tests/hermes_cli/test_sqlite_runtime.py - 91 passed, 1 skipped locally

(cherry picked from commit e62940d1021e80e9b7d6423ced1cbdfe7dd0c37d)
2026-09-03 11:28:21 +05:30
Teknium 679b303ba1 refactor(hermes_cli): drop intra-function blank separators in session modules (AST-identical) 2026-09-02 22:55:35 -07:00
Teknium 9dcff9d5d0 refactor(hermes_cli): unify placeholder-title generator across recovery lanes; repack long string literals 2026-09-02 22:32:12 -07:00
Teknium cec7daa07d refactor(hermes_cli): pack exploded signatures/calls in session recovery modules (AST-identical) 2026-09-02 22:09:28 -07:00
Teknium b7b543b253 refactor(hermes_cli): session_lost_and_found reuses session_recovery helpers; compact docs and mapper branches 2026-09-02 21:51:48 -07:00
Teknium 86d111d77c refactor(hermes_cli): collapse defensive layers in sessions_cmd, join bracket groups in session recovery modules 2026-09-02 21:12:14 -07:00
Teknium cfa327e5dc refactor(hclib): remaining hermes_cli library modules — dead code, unified helpers, flattened branches 2026-09-02 15:03:42 -07:00
kshitijk4poor 88736f6b02 refactor(recovery): share the synthesised-row title prefix between producer and verifier
The plausibility gate excluded stub rows by a hard-coded title literal that
session_lost_and_found builds inline at two sites; rewording either would have
turned every stub into a mis-mapped row. One STUB_TITLE_PREFIX constant.
2026-09-03 03:32:18 +05:30
Teknium 5dfd1e77a8 fix(recovery): register lazy state.db tables in one schema map; cover .recover lane and count-mismatch loss
Follow-up to the salvaged #100350 commits: replace the per-table
'if table == "delivery_obligations"' branches in session_recovery.py and
session_lost_and_found.py with a single _AUXILIARY_TABLE_SCHEMAS registry
(table -> destination DDL initializer) that both the SQL-level and the
lost_and_found lanes consume, so the next lazily-created state.db table is
one entry, not three code paths. The .recover lane now iterates
_CANONICAL_TABLES + _AUXILIARY_TABLES instead of a duplicated literal list.

Tests: the .recover direct-copy lane creates the missing ledger on the
destination; a source-vs-destination obligation count mismatch fails
verification (complete=False) instead of reporting a clean salvage.
Docs: state.db table inventory lists delivery_obligations.

Addresses #100313
2026-09-02 00:00:55 -07:00
HexLab98 0192ca2830 fix(recovery): copy delivery_obligations during session salvage
The lazy gateway outbox was missing from the recovery inventory, so a
verified salvage could drop owed replies even when the rows were still
readable. Initialize the destination schema and copy the table.
2026-09-02 00:00:55 -07:00
Teknium fbaea9bddc feat(sessions): generic 'hidden' session flag (sidebar-hide, still resumable) (#86797)
* feat(sessions): generic 'hidden' session flag (sidebar-hide, still resumable)

Adds a source-orthogonal, archive-orthogonal 'hidden' session flag meaning
'don't show in the global Sessions sidebar, but stay fully resumable by the
surface that owns it'. Mirrors the existing archived/pinned capability end to
end, so it's a generic widening (any plugin that owns its own session lifecycle
- kanban, Bot Mode, future plugins - can keep its sessions out of the shared
recents list) rather than a per-plugin special-case.

- Schema: hidden INTEGER NOT NULL DEFAULT 0 on sessions (additive; lands on
  existing DBs via the declarative _reconcile_columns ADD COLUMN path, same as
  archived/pinned - no version-gated migration).
- DB: SessionDB.set_session_hidden(session_id, hidden) (clones set_session_pinned
  incl. the compression-lineage recursive CTE); list_sessions_rich gains
  include_hidden=False, appending 's.hidden = 0' by default so hidden rows drop
  from every listing path (and the REST sidebar endpoints inherit it with no
  change).
- Gateway: session.set_hidden RPC (mirrors session.title); session.create accepts
  hidden=true, deferred via pending_hidden and applied in _ensure_session_db_row
  when the row is lazily created (mirrors pending_title).
- REST parity: PATCH /api/sessions/{id} accepts+bool-validates 'hidden' ->
  set_session_hidden; _session_response exposes it.

Enables Hermes-Bot-Mode to hide canonical 'Bot Chat' sessions from the sidebar
(NousResearch/Hermes-Bot-Mode#46) WITHOUT retagging source (which would mis-set
the agent platform). Bot Chats keep source=desktop. Gateway RPC needs a
SERVE-backend restart to take effect live. 1 focused test (default-exclude /
include_hidden / unhide round-trip).

* fix: teach lost-and-found recovery about the 55-column sessions layout

Adding the 'hidden' column makes the current sessions table 55 columns. The
SQLite lost-and-found recovery classifier keys off the physical field count
(SESSIONS_LAYOUT_NFIELDS) to identify a salvaged sessions row, so a recovered
current-layout row (nfield=55) would otherwise be unrecognized and dropped.
Add 55 to the frozenset (54/52 stay as historical prefixes) and update the
column-count assertions + synthetic current-layout insert in the recovery test.

---------

Co-authored-by: Teknium <teknium1@users.noreply.github.com>
2026-08-15 00:31:37 -07:00
Teknium 97c06dcfd7 fix(sessions): probe sqlite3 CLI for .recover capability, not just PATH presence
Ubuntu CI (and other distro builds) ship a sqlite3 shell compiled without
the sqlite_dbpage virtual table that .recover requires, so PATH presence
alone let the lane attempt and fail with 'no such table: sqlite_dbpage'.
find_sqlite3_cli() now probes .recover on a scratch DB once; the test skip
gate uses the same probe, and the no-CLI guidance names the capability
requirement.
2026-08-12 19:43:47 -07:00
Teknium 6dad74596e fix(sessions): recover budget exhaustion + lost_and_found last-resort lane
Fixes #80205: when one ordered rowid-edge probe failed,
_salvage_rowid_bounds() substituted the whole SQLite rowid domain and
_copy_table_salvage() burned the 10,000-query budget bisecting a
synthetic tail that could not contain rows, silently omitting readable
boundary rows (field case: message 76882 of 76882). Two-part fix:

* _probe_populated_edge(): gallop outward from the surviving edge with
  doubling offsets; a clean 'no rows beyond X' probe caps the domain in
  O(log range) queries instead of exhausting the budget on it.
* exact-key singleton salvage: a one-row range scan must advance the
  cursor past the hit into the damaged sibling page to prove exhaustion,
  which discards the already-produced row; 'WHERE rowid = ?' stops at
  the hit, recovering the boundary row exactly like sqlite3 .recover.
* the strict-path refusal now points users at --allow-partial.

New last-resort lane for --allow-partial when the sessions/messages
table schemas themselves are unreadable (previously a hard refusal even
though page-level salvage recovers the rows fine). If a sqlite3 CLI is
on PATH, shell out to '.recover --ignore-freelist' into a scratch
lost_and_found DB, then heuristically map rows back into a fresh
SessionDB-schema database (hermes_cli/session_lost_and_found.py):
classification keyed on nfield counts + sentinel columns (session ids
matching ^\d{8}_\d{6}_, roles in user/assistant/tool/system, known
source strings), covering the current 54-col sessions layout, the
52-col historical layout, a 14-col legacy identity-only salvage,
rowid-alias messages rows and 18-col session_model_usage rows. Missing
parent sessions are stubbed (children are never deleted for FK
cleanup), FTS is rebuilt at the end, and output is labeled BEST-EFFORT
everywhere. Without the CLI the error names the sqlite3 requirement
with actionable guidance. Mirrors a successful manual recovery of a
real corrupt state.db (2026-08-12), and this lane was validated against
that preserved file: 32 sessions / 7 messages / 4 usage rows mapped,
integrity_check ok, opens via SessionDB.

Also fixes #72291: the source-fingerprint 'bundle changed while it was
being copied' error now enumerates that the parent interactive CLI
session itself counts as a Hermes process and suggests a fresh shell or
an immutable snapshot.

Tests use real physical page corruption (flipped b-tree/schema header
bytes), skip the CLI-dependent path cleanly when sqlite3 is absent, and
keep the mapper unit tests binary-independent via a synthetic
lost_and_found DB. Sabotage-verified: reverting the fixes makes the
regression tests fail with the exact field failure shape.
2026-08-12 19:43:47 -07:00