Commit Graph

37567 Commits

Author SHA1 Message Date
chenxue
a11bac476b plugin-catalog: add aihubmix (community/models provider) 2026-09-18 18:23:48 -07:00
rajitkhanna
249526bc2d chore(plugin-catalog): update Prism pin 2026-09-18 18:23:48 -07:00
rajitkhanna
5a3f3e7ce5 fix(plugin-catalog): pin clean Prism model IDs 2026-09-18 18:23:48 -07:00
rajitkhanna
fde394c426 feat(plugin-catalog): add Prism provider 2026-09-18 18:23:48 -07:00
Edward Irby
2d0ea1bc2e plugin-catalog: add 'you' (You.com skills + MCP, community)
Agent Plugins v1 package (plugin.json + mcp.json + skills/) from
youdotcom-oss/agent-skills, pinned at 08f3d80 (tag v2026.09.17-08f3d80).
Owner-submitted.
2026-09-18 18:23:48 -07:00
devin
6c7a0e59c6 feat(catalog): add Search1API plugin
Registers superagents-lab/hermes-search1api (v0.1.0) — five tools backed by
the Search1API API: search1api_search, search1api_news, search1api_crawl,
search1api_sitemap, search1api_trending. Vendored official Python SDK; only
third-party dependency is httpx (already a Hermes core dependency).

hermes plugins validate (main @ 6005aa1): all checks pass, security scan
safe; declared tools match registrations.

Submitted by the plugin repo owner (superagents-lab).
2026-09-18 18:23:48 -07:00
teknium
b73a290de4 chore: map contributor email for @eloktev 2026-09-18 18:09:26 -07:00
Egor Loktev
c24358032f feat(catalog): add pinned artifact-relay plugin 2026-09-18 18:09:26 -07:00
drkpxl
aac41a3d3d chore: map contributor email 2026-09-18 18:07:41 -07:00
drkpxl
71950e61bb Bump SHA: add portable plugin.json manifest to morning-briefing repo
The pinned tree now carries an Agent Plugins v1 plugin.json at the repo
root, so 'hermes plugins validate' passes and the catalog installer
finds its manifest. Security scan clean (no cautions).

New pin: 347ce44a1e71173285b2943a7a97bbdece8327b2
2026-09-18 18:07:41 -07:00
drkpxl
dd254399e4 Scope catalog entry to the dedicated morning-briefing repo
Split out from drkpxl/drkpxl-skills so the entry matches the product:
one skill, one repo, name on the tin. The new repo contains only the
morning-briefing skill. Entry name, repo URL, SHA pin, description,
and category all updated.
2026-09-18 18:07:41 -07:00
drkpxl
7317669fde Add drkpxl-skills to plugin catalog
Community skills collection by the repo owner (drkpxl). Anchor skill:
morning-briefing — a personal daily newspaper that gathers weather,
calendar, AQI, curated 36-hour news (Reddit RSS + X + web search with
per-story QR codes), and newsletter digests, renders a single 8.5x11
newsprint page, and prints it every morning.

Also bundles: Tiny Air (US AQI via MCP), Bro, Copywriting, Delegate to
pi, Prototype, Research, Requesting Code Review.

SKILL.md follows the HARDLINE authoring standards (validated: 54-char
description, modern section order, native tool references). SHA pinned
at d81a9b5ee427a378839828d4a8eab3925468b7ce.
2026-09-18 18:07:41 -07:00
Dafka
0b78a26903 feat(plugin-catalog): add hermes-security-audit 2026-09-18 18:05:50 -07:00
Adolanium
a9dbbe9fcc feat(catalog): add OpenAlex research tools 2026-09-18 18:02:54 -07:00
teknium
60fd4e8723 chore: map contributor email for @Zoeille 2026-09-18 17:55:00 -07:00
Chloé
402743e044 plugin-catalog: add pstack (community) 2026-09-18 17:55:00 -07:00
teknium1
be0a214452 fix(gateway): a NUL-stdin Windows gateway start is non-interactive too
The Windows CRT reports isatty()==True for every character device, the NUL
device included, so `hermes gateway start < NUL` (stdin=DEVNULL) still took
the TTY branch: prompt_yes_no() hit EOF, defaulted to Yes and registered a
Scheduled Task — the silent install #113977 describes. Confirm isatty with
GetConsoleMode(STD_INPUT_HANDLE); a handle no console accepts is treated
like a pipe. Pipes and HERMES_NONINTERACTIVE keep their behaviour.
2026-09-18 17:49:41 -07:00
teknium1
ff6b9f722d fix(gateway): a non-interactive Windows gateway start never installs login auto-start
`start()` asked "Install it now so the gateway starts on login?" with default Yes, then called
`install()` which asked two more default-Yes questions; on a redirected stdin (`< /dev/null`,
scripts, agent tool calls) all three answered themselves and a bare `start` wrote a Startup-folder
login item, spawned the gateway, and start() spawned it a second time (#113977).

- The persistence question is answered only explicitly: `HERMES_GATEWAY_INSTALL_START_ON_LOGIN`
  (which start() never consulted before) or a prompt on a real TTY. Without a TTY, or under
  `HERMES_NONINTERACTIVE=1`, the answer is No: the gateway starts, nothing persistent is written,
  and the explicit `hermes gateway install` command is printed.
- Declining on a TTY also starts the gateway — the command is `start`; only persistence was declined.
- A Yes hands `start_now=True, start_on_login=True` to install(), which spawns and reports (including
  the UAC hand-off), so start() no longer double-spawns and no longer prints the false
  "Gateway install did not complete in this process" after an install that was skipped by intent.
- `install --no-start-on-login` without `--start-now` now hints `hermes gateway run` instead of the
  `hermes gateway start` that used to offer the very auto-start just declined.

Supersedes #113981 (@KoNit-K), which flipped the first prompt's non-TTY default but left the
env override, the hint text and the false warning in place.
2026-09-18 17:49:41 -07:00
teknium1
a51143fbbe fix(bedrock): resolve application-inference-profile ARNs before the prompt-cache allowlist match
build_converse_kwargs gated cachePoint markers on the raw model id, so a profile ARN wrapping
Claude matched nothing in _CACHE_POINT_PATTERNS and silently lost Bedrock prompt caching.
_model_supports_prompt_cache now resolves the profile through the per-process-cached
_resolve_inference_profile_model_id first; the request still targets the profile ARN.

Part of #114476
2026-09-18 15:15:02 -07:00
teknium1
418a19cfc7 chore(contributors): map yagnavudathu@gmail.com to @whyyagswhy 2026-09-18 15:15:02 -07:00
teknium1
11445cc54e fix(bedrock): application inference profile ARNs size from the wrapped model on the real path
The cherry-picked #114482 never resolved a profile in production: the only
caller, agent/model_metadata.py::_resolve_bedrock_context_length, invokes
get_bedrock_context_length(model, probe=False) with no region, and the
resolver was gated on `region`; and it called
get_inference_profile(inferenceProfileId=...) where botocore requires
`inferenceProfileIdentifier`, so even with a region the call raised
ParamValidationError, was swallowed, and the 128k default applied with only
the new warning. Live against a botocore Stubber: 128000 before, 1000000 after.

- resolve in the ARN's own region (field 4), then the passed region, then
  the standard AWS chain; the runtime region / base_url may differ
- inferenceProfileIdentifier is the only request parameter GetInferenceProfile has
- match only `application-inference-profile/`: system-defined
  `inference-profile/us.anthropic...` ARNs embed the model id and need no call
- no nested-profile recursion: GetInferenceProfile lists foundation-model ARNs
  and the wrapped ARN itself satisfies the static-table substring match
- cache both outcomes per process (runs on every context-length resolution),
  cleared by reset_client_cache()
- tests trimmed to two invariants on the production call shape; restore the
  TestBedrockContextProbe class header the cherry-pick clobbered
- docs: bedrock:GetInferenceProfile IAM permission and the fallback WARNING

Co-authored-by: Yagna Vudathu <yagnavudathu@gmail.com>
2026-09-18 15:15:02 -07:00
liuhao1024
4c28be68c2 fix(bedrock): size inference-profile ARNs from the wrapped foundation model
An application-inference-profile ARN names no model, so the context probe
error text and the static substring table both miss and the 128k default
silently applies: a profile wrapping a 1M-context Sonnet compacts at ~96k.
Resolve the wrapped foundation-model id via GetInferenceProfile (reusing
the control-plane client; an application profile's id IS its ARN, so the
call works on both API shapes), then let the existing probe/table paths
run on that id. Resolution failures (missing bedrock:GetInferenceProfile,
no creds) keep today's behaviour but now log a WARNING naming the
explicit model.context_length escape hatch instead of staying silent.

Fixes #114476
2026-09-18 15:15:02 -07:00
kshitijk4poor
1250a3e4eb fix(backup): an incomplete archive never prunes the last complete ones
Gate review: `--keep` pruning ran after the summary regardless of `errors`,
so a timer hitting the same unreadable file every run would exit 1 each time
and still rotate every complete `hermes-backup-*.zip` out after N runs,
leaving only incomplete archives. Prune only after a complete backup; the
test pins a pre-existing good archive surviving an incomplete run with
`--keep 1`. The summary no longer hard-codes the caller's exit code.
2026-09-19 03:43:58 +05:30
leomcamilo
ccb3d968ce fix(backup): exit non-zero when a full backup is incomplete
`hermes backup` recorded per-file failures, printed `Backup incomplete: <path>`
and still returned shell status 0, so a cron job or systemd timer would publish
"successful" archives missing state.db indefinitely.

`run_backup()` now returns whether the archive is complete and `cmd_backup()`
maps False to exit status 1. The zip is kept so the operator can still restore
the rest; hard failures keep their SystemExit(1)/(2). `--quick` is unchanged.

Slim redo of #101096 on current main (the branch predates the run_backup /
_run_backup_locked split and the backup lock); same policy, same exit codes.
Supersedes #68866 (@jbryce) which proposed the policy first.
2026-09-19 03:43:58 +05:30
kshitijk4poor
07fccd3114 fix(runtime): drop the dead urlparse import; pin the bare-host-default fail-open
A provider whose catalog default has no path (api.anthropic.com) has no
sibling OpenAI-compatible path to infer an override's protocol from, so the
declared transport stays; the docstring says so and a test case pins it.
2026-09-19 03:43:03 +05:30
kshitijk4poor
da2b6a47b9 fix(runtime): share URL path parsing with the host check; pin catalog defaults in tests
Round-2 gate folds: utils.base_url_path sits next to base_url_hostname on
the same scheme-tolerant parser, so a scheme-less override (api.minimax.io)
keeps host and path checks agreeing; a default with no path is an explicit
"nothing to compare" case; the dead `or "chat_completions"` after
determine_api_mode is gone; the test fixture seeds the catalog defaults
unconditionally so a warm models.dev cache cannot change what the contract
compares against, and the stale #53054 test comment states the new contract.
2026-09-19 03:43:03 +05:30
kshitijk4poor
dbdcb83d77 fix(runtime): compare against the catalog default path, keep declared on empty input
Gate findings folded: the accepted path is derived from the provider's
catalog default (/anthropic for MiniMax, /plan/anthropic for Tencent) instead
of a hardcoded /anthropic prefix, so /anthropic-compat no longer passes and a
non-/anthropic default provider's own endpoint does; an empty base_url keeps
the declared transport like an unknown default; the catalog read is
allow_network=False (determine_api_mode just warmed it); _base_url_path is
shared with URL detection. Tests pin the catalog defaults they compare
against so the contract holds offline.
2026-09-19 03:43:03 +05:30
kshitijk4poor
ae2a8cd819 fix(runtime): declared Anthropic transport applies only on the provider's own endpoint
33a2f29a6 made the runtime fallback honour the provider's declared transport
when URL detection has no opinion. For minimax/minimax-cn that is
anthropic_messages, which is right on api.minimax.io/anthropic but wrong for a
model.base_url override at an OpenAI-compatible relay (LiteLLM, token proxy,
corporate egress) or at the provider's own /v1 path: every request is built
Messages-shaped with x-api-key and 401s, or silently reroutes to a fallback
provider.

The fallback now keeps anthropic_messages only when base_url shares the
catalog default's host and is the bare host or a path under /anthropic;
everywhere else it lands on chat_completions. Other declared transports are
untouched (openai-api on a custom proxy still keeps codex_responses, as
33a2f29a6 intended), and URL detection still runs first.

Salvage of PR 76859 by webtecnica, rebuilt on the refactored module with the
path check teknium asked for in review.

Co-authored-by: webtecnica <webtecnica@gmail.com>
2026-09-19 03:43:03 +05:30
teknium1
420feb6307 docs: session-bound Desktop/TUI settings writes and dashboard launch-profile fallback are per profile 2026-09-18 15:12:27 -07:00
teknium1
55c92fa32a fix(desktop): every Custom Endpoints completion is dropped once its profile view unmounts
The salvaged guard covered only the save path. Validate, activate and delete completions
from a profile-A instance could still set state, fire onConfigSaved / onMainModelChanged
and toast after SettingsView remounted the panel under profile B — the identical stale-
completion race, three more doors. The same `mounted` ref now gates all four handlers
and `refresh()`.

Test: the salvaged vitest file now partially mocks `@/hermes` (the profile-scope note
subscribes to `setApiRequestProfile` at import time) and stubs `ActiveProfileNote`.

Closes #69451 (renderer `profileScoped()` forwarding, backend `_config_profile_scope`
and the profile-keyed remount landed on main earlier; this closes the last atom).
Salvage of #69452.
2026-09-18 15:12:27 -07:00
Sun Haoyuan
0eb0a6d78b fix(desktop): drop stale custom endpoint saves 2026-09-18 15:12:27 -07:00
liguoyu
a8cccc22f2 fix(dashboard): profile-less chat deep links inherit the launcher's preselected profile
`hermes dashboard` from a named profile re-execs as `-p default dashboard --open-profile P`,
but `--open-profile` only reached the one URL `_maybe_open_browser()` opened. A `/chat?resume=<id>`
deep link without `?profile=` initialised `ProfileProvider` with the empty (launch) scope, so the
embedded chat ran in the default profile — no MCP servers, wrong model/skills — while the switcher
showed P. No error, no indication.

The server now records `initial_profile` on `app.state` and injects it into the SPA bootstrap as
`window.__HERMES_INITIAL_PROFILE__` (escaped for the script context); the Vite dev proxy forwards
it. `ProfileProvider` uses it only when the URL carries no `profile` param: an explicit
`?profile=` (including an explicit empty one) still wins, and the sticky-active-profile alignment
no longer replaces a launch-preselected scope.

Closes #73085. Salvage of #73260 (cherry-pick of 99e341cdef0 resolved onto the split
web_server_dashboard.py; start_server assertion dropped as a change-detector).
2026-09-18 15:12:27 -07:00
teknium1
fbe85963de fix(tui): session-bound config.set persists into the session profile, not the launch profile
Desktop app-global remote mode and the Ink TUI send `config.set` with `session_id` and no
`profile` param. `@_profile_scoped` only bound `params["profile"]`, so a write from a focused
worker session round-tripped `_load_cfg_raw()`/`_save_cfg()` against the LAUNCH profile's
config.yaml while the worker profile kept its old value (and reverted on restart).

The decorator now falls back to the named session's `profile_home` when `profile` is absent,
binding the full runtime scope (home + secrets + terminal) the same way an explicit profile
does. `config.set cwd` from a profile-bound session no longer publishes `TERMINAL_CWD` into
the shared process env (that variable belongs to the launch profile).

Closes #85669. Supersedes #85676 (its diff predates the methods_* split).

Co-authored-by: worlldz <cryptoworlldz@gmail.com>
2026-09-18 15:12:27 -07:00
teknium1
3ed40556ce fix(profiles): a child spawned for another profile no longer inherits the spawner's authorization gates
A `hermes -p B` child built from a process that loaded profile A's env (a gateway, the
dashboard, the post-update fleet restart) started with A's `DISCORD_ALLOWED_CHANNELS`,
`TELEGRAM_GROUP_ALLOWED_CHATS`, `GATEWAY_ALLOW_ALL_USERS`... and enforced them as its
own: gates are not credentials (no secret scrub sees them), a unit-file `Environment=`
or operator export is in no dotenv (no name-list strip sees them), and B's own `.env`
rarely defines the key (its dotenv load never overwrites the inherited value). Observed
as profile B's gateway rejecting every message in B's own channel after a per-profile
restart issued from A (#113270).

- `local_env_policy.is_profile_gate_env` / `strip_profile_gate_env`: gates matched by
  shape (`_ALLOWED_`, `_ALLOW_ALL_`, `_ALLOW_FROM`, `_ALLOW_BOTS`, `_IGNORED_CHANNELS`,
  ...), never `HERMES_*`, so a gate added to any adapter is covered without a second edit.
- `strip_launch_profile_env` drops them on its existing routed-home branch: the seam
  `served_profile_child_env`, the kanban dispatcher, cron workers and the dashboard
  action env already funnel through (the dashboard site now calls it for its target).
  Same-home children keep an operator export.
- `update_restart_recovery._child_environment(profile)`: the one site that bypassed
  every helper (bare `os.environ.copy()` relaunching EVERY profile) strips gates when
  the profile is not the one the updater runs as; the module stays stdlib-only at
  import time.

Live: fresh-process `hermes_cli.update_restart_recovery --stdin` with three gates in
the updater env — base hands all three to profile b's relaunch, fixed hands none and
keeps them for the launch profile.

Refs #113270; supersedes #113308 (@yashraj4, static key list + always-strip; this keeps
same-profile children intact and covers the per-adapter gate set).
2026-09-18 15:11:47 -07:00
LeonSGP43
f59c451f81 fix(feishu): avoid threading regular replies 2026-09-19 03:31:00 +05:30
kshitijk4poor
aaaca43c84 refactor(gateway): one wildcard-host predicate for the HTTP listeners
Gate review: tcp_site.py grew its own `_WILDCARD_HOSTS` that already diverged
from shared_ingress.py (dropped "*" and ""). One `is_wildcard_host` in
shared_ingress, used by both the TIME_WAIT rebind guard and
`listener_base_url`.
2026-09-19 03:30:32 +05:30
kshitijk4poor
741d8db2da test(gateway): trim the TIME_WAIT rebind tests to the two invariants
The positive case (bind over a lingering server-side TIME_WAIT socket) lives
once, on the api_server adapter, since both adapters share start_tcp_site; the
webhook file keeps the negative case (a live listener still wins). That test
hung: the blocker never closed the probe connection server-side, so
`wait_closed()` waited forever.
2026-09-19 03:30:32 +05:30
TheBlueHouse75
2d99b7cf5d fix(gateway): rebind webhook and api_server over TIME_WAIT after a restart on macOS
On macOS, an exclusive bind (SO_REUSEADDR disabled) refuses a port that is
still held only by a server-side TIME_WAIT socket for 2*MSL (~30s) after a
previous connection close — the gateway's own shutdown, or any
``Connection: close`` request. A ``/restart`` issued within that window
failed to bind, even though nobody was actually listening.

For an explicit host, a refused connect probe proves nobody is listening:
the kernel still rejects an exact duplicate bind even with SO_REUSEADDR, and
a foreign wildcard listener would answer the probe. So one retry with
SO_REUSEADDR is safe in that case. A wildcard host keeps the strict
exclusive path, since a foreign listener on a non-loopback interface can't
be probed this way. A live listener still wins either way.

The bind, probe, and retry logic is shared by webhook.py and api_server.py
through the new gateway/platforms/tcp_site.py, since both adapters had the
same exposure — api_server additionally marked a spurious TIME_WAIT bind
failure as a non-retryable port conflict.

(cherry picked from commit 289fd06148c447829f1e38b66ff78b9e4bebdeb4)
2026-09-19 03:30:32 +05:30
kshitijk4poor
c0f3d1fedf chore: map contributor email for @TheBlueHouse75 (#108239 salvage) 2026-09-19 03:30:32 +05:30
kshitijk4poor
5db9f02372 fix(weixin): same session-not-ready error on the media leg; cooldown message keeps the raw fields; test + docs
_send_file hit the identical stale -2 after its own tokenless re-send and still
raised the generic "sendmessage error"; it now raises the shared
_session_not_ready_error. The genuine rate-limit cooldown message carries
ret/errcode/errmsg so operators can tell a real -2 frequency limit from the
session case. One parametrised test (stored token / no token) asserts the send
fails once, classify_send_error does not call it rate_limited, and the breaker
stays closed; troubleshooting row in the Weixin docs.
2026-09-19 03:30:09 +05:30
x7peeps
bd39167c56 fix(weixin): ret=-2 "prepare failed" that survives recovery is a session error, not a rate limit (#80125)
iLink answers a bot-initiated send with ret=-2 errmsg="prepare failed" when the
peer's session is not ready (no inbound message yet, or the context token is
gone). That is deterministic: treating it as a frequency limit spent the retry
budget on an error that never clears and opened the 30 s rate-limit breaker for
the whole account, blocking every later send (#80125, #112709).

After the tokenless re-send (46ab37ce35) has been tried — or when there is no
token to drop — a stale-session -2 now fails fast with a descriptive
"session not ready" error and never reaches the rate-limit path. The text
avoids the "rate limit" substring classify_send_error keys on.

Salvage of #80152 (fail fast + keep the breaker closed); #80156 arrived five
minutes later with the same change.

Co-authored-by: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com>
2026-09-19 03:30:09 +05:30
kshitijk4poor
8925c70a1c docs(whatsapp): group access section says what the gateway admits; env reference rows; trim bridge tests
Groups: policy, group-JID allowlist, and that participants are still authorised by
the gateway sender allowlist or pairing (`open` alone admits nobody without one);
`require_mention` defaults to false; WHATSAPP_GROUP_POLICY / WHATSAPP_GROUP_ALLOWED_USERS
rows in the environment reference. The alt-id node tests collapse to one (the
participantAlt case duplicated the first-contact case; the "still resolves via mapping
files" case only re-asserted matchesAllowedUser).
2026-09-19 03:15:40 +05:30
kshitijk4poor
35b38f1f4e fix(whatsapp): bridge gets the adapter's resolved group policy and group allowlist; one allowlist reader
The bridge now gates groups by WHATSAPP_GROUP_POLICY / WHATSAPP_GROUP_ALLOWED_USERS
(#73465), but only the DM policy and DM allowlist were exported from the adapter's
resolved config — a YAML group_allow_from never reached the bridge and, under a
multiplexed gateway, the launch process's WHATSAPP_GROUP_* values did. _bridge_env
exports both like it already did for the DM pair, so unlisted-group traffic is cut
before media download instead of after the POST to Python.

whatsapp_common._select_allowlist generalises the DM precedence reader (config key
presence wins, an explicit empty list stays authoritative, then the first truthy env
carrier); the adapter's group list and the Cloud sibling's group list use it instead
of hand-rolled `or` chains, so `group_allow_from: []` means the same everywhere.

Tests: bridge env carries group policy + group allowlist from the secondary profile's
YAML (scoped precedence file); env fallback test trimmed to the two invariants (red on
the merge base); bare test adapters seed _group_policy/_group_allow_from, which
_bridge_env now reads.
2026-09-19 03:15:40 +05:30
sal
477afd2420 fix(whatsapp): adapter reads WHATSAPP_GROUP_ALLOWED_USERS for the group allowlist (#72529)
The Node bridge receives WHATSAPP_GROUP_ALLOWED_USERS via
_BRIDGE_PASSTHROUGH_ENV and the YAML bridge maps config.yaml
whatsapp.group_allow_from onto the same env carrier, but
WhatsAppAdapter.__init__ seeded _group_allow_from from config.extra
alone — an install that set the group allowlist only through the
documented env var silently ran group gating with an empty allowlist:
with group_policy=allowlist every group chat was rejected at intake
(the second failure mode triaged on #72529, the half with 'no PR yet').

Mirror the DM path's precedence: config keys win by key presence
(explicit empty list stays authoritative), then the profile-scoped
WHATSAPP_GROUP_ALLOW_FROM / WHATSAPP_GROUP_ALLOWED_USERS env CSV —
multiplex-safe through _wenv, like every other WHATSAPP_* read.

Tests pin the full precedence matrix: env-only seeding, the
allow_from-style alias, config-wins-over-env, the legacy groupAllowFrom
key, empty-by-default, the scoped .env read under multiplex, and the
group-policy gating outcome for an env-seeded allowlist.
2026-09-19 03:15:40 +05:30
Waldo
81fd9dc773 fix(whatsapp): honor group ingress policy in bridge
Use the configured group policy and group-JID allowlist at Node bridge intake instead of applying the DM sender allowlist to group participants.

Co-authored-by: Martin Gontovnikas <m@gon.to>
2026-09-19 03:15:40 +05:30
jinlingzi-cmd
3926c4209c fix: WhatsApp group messages dropped when LID sender has no lid-mapping (#72529) 2026-09-19 03:15:40 +05:30
Amit C
8a55373dbf fix(whatsapp): authorize first-contact LID senders 2026-09-19 03:15:40 +05:30
kshitijk4poor
2bfb95b396 chore: map amitcse, eduardo and jinlingzi-cmd contributor emails 2026-09-19 03:15:40 +05:30
kshitijk4poor
8b038f3665 refactor(state): topic rebuild goes through _rebuild_table; heal also swallows a replaced state.db
The hand-rolled CREATE/INSERT/DROP/RENAME duplicated SessionSchemaMixin._rebuild_table,
which already runs inside a caller-owned transaction and has its own atomicity test.
The stranded {table}_new drop stays: older builds crashed with that scratch name.

_execute_write probes the DB generation before running the callback and raises
StateDbReplacedError (a RuntimeError, not sqlite3.Error); _read_ctx never does, so
the heal must not let it escape a read path that promises the empty value.

Atomicity probe now denies the copy into the fresh table: a non-transactional
rebuild leaves an empty v3 table the retry skips (verified red with executescript).
2026-09-19 03:14:20 +05:30
kshitijk4poor
0a5046ec92 test(state): atomicity probe fails the rebuild's final RENAME, not its first DROP
Denying every DROP TABLE on the topic tables now trips on the new
'DROP TABLE IF EXISTS {table}_new' before anything is written, so the probe
passed even with an executescript rebuild. Deny the RENAME of the scratch
table instead: the legacy table is already dropped at that point, and only a
transactional rebuild still has its rows on retry (verified red with
executescript swapped back in).
2026-09-19 03:14:20 +05:30