* rebase #7160 onto master (report unverified Agent updates, no auto-restart)
Co-authored-by: hrdwdmrbl <hrdwdmrbl@users.noreply.github.com>
* harden agent-update marker read: no-follow, fstat regular-file, bounded (fixes gate CORE)
The Agent update marker is attacker-adjacent shared state. _read_live_agent_update()
used Path.read_text(), which follows symlinks, blocks forever on a FIFO, and reads an
unbounded regular file — a rejected/stale-runtime request could hang or OOM. Now opens
O_RDONLY|O_NONBLOCK|O_NOFOLLOW, fstat-verifies a small regular file, and reads a bounded
max, classifying anything else as 'unknown' (fail-closed). Adds FIFO/oversized/symlink/
happy-path regression tests.
Co-authored-by: hrdwdmrbl <hrdwdmrbl@users.noreply.github.com>
* fix marker-read portability: gate os.open fast-path on O_NONBLOCK+O_NOFOLLOW
os.O_NONBLOCK is Unix-only; accessing it unconditionally raised AttributeError
on native Windows, breaking every stale-revision barrier (chat/compression/etc).
Now both flags resolve via getattr and the os.open() fast path is taken only
when BOTH are available (_MARKER_SAFE_OPEN_AVAILABLE); otherwise fall back to an
lstat-only classification (absent vs unknown) so a fallback platform can never
symlink-traverse or crash. Adds a Windows-fallback regression test.
Co-authored-by: hrdwdmrbl <hrdwdmrbl@users.noreply.github.com>
* docs(changelog): stamp #7160 stale-runtime report + hardened marker read
---------
Co-authored-by: n <a@n>
Co-authored-by: hrdwdmrbl <hrdwdmrbl@users.noreply.github.com>
* fix(sse): scope gateway SSE probe and honor Last-Event-ID on /api/chat/stream
Two narrowly-scoped cross-client SSE fixes from the hermes-android#58
follow-up:
1. The /api/sessions/gateway/stream?probe=1 response only describes the
optional gateway/agent-sessions stream, but its payload gave clients no
way to tell that. A 404 'agent sessions not enabled' result could be
misread as SSE being unsupported server-wide, even though the persistent
per-session stream (/api/session/stream) is always on and independent of
the show_cli_sessions setting. Add additive scope markers to the probe
payload (scope, session_stream_available, session_stream_path) and
clarify the handler docstring.
2. /api/chat/stream emitted id: stream_id:seq on every journaled event
but ignored the Last-Event-ID request header, so any spec-compliant SSE
client that auto-reconnects (browser EventSource, Android/CLI clients)
replayed the journal from seq 0 and double-rendered the transcript.
_chat_stream_resume_after_seq() now resolves the resume cursor with
explicit after_event_id/after_seq query params first and the
Last-Event-ID header as fallback — the same resolution chain
api/kanban_bridge.py already uses. Both the dead-stream journal replay
and the live-subscribe gap check consume it; direct callers of the gap
check that only pass query params keep historical behavior.
Also documents the cross-client SSE endpoint contract in
docs/sse-streams.md.
Related hermes-webui/hermes-android#58
* fix(sse): split cursor presence from validity on /api/chat/stream resume
Address PR #6886 review (gate-fail): the resume logic conflated "no
cursor" with "invalid cursor" into a single None, which broke replay on
three edges. Root cause and fix:
Cursor presence vs validity — _chat_stream_resume_cursor() now returns
(after_seq, resume_requested) instead of a bare seq. A client that
SUPPLIED any cursor (query params, replay=1, or Last-Event-ID) asked to
resume; an invalid / foreign-run / unparseable cursor resolves to
after_seq=None but keeps resume_requested=True, and is normalized to
replay-from-start so no journal events are silently skipped. A genuinely
cursor-less request stays a fresh subscribe.
1. Explicit-query precedence is now decided by parameter PRESENCE, not
successful parsing: a malformed/foreign after_event_id/after_seq never
falls through to let the Last-Event-ID header override it and skip
events.
2. Invalid/foreign cursors no longer bypass replay + gap detection and
silently drain a truncated buffer; an ahead-of-stream cursor is no
longer clamped onto the snapshot's terminal fence (which emptied the
dead-stream replay by filtering the terminal frame).
3. The runner-local observe path now receives the resolved header cursor
(retaining explicit-query precedence) so header-only runner reconnects
resume instead of duplicating events from the start.
Regression tests cover invalid-cursor replay-from-start, ahead-of-stream
terminal survival, header-only runner reconnect, and explicit-query
precedence; the stale clamp test is updated to the corrected contract.
Related hermes-webui/hermes-android#58
* fix(sse): consume resolved resume cursor at every consumer on /api/chat/stream
Address PR #6886 r2 review (gate-fail). The r1 presence/validity split
fixed the entry resolution, but the individual consumers still re-derived
ahead/equality/re-parse decisions, so the invariant leaked at the edges.
This makes every consumer honor the resolved (after_seq, resume_requested)
pair and normalizes ahead-of-stream in one place.
1. Equality off-by-one: a valid cursor EQUAL to the snapshot cutoff now
enters the drain dedup bound (the event at the cursor was already
delivered), instead of being treated as "ahead" and double-sending the
retained buffer. Ahead is now strictly after_seq > snapshot_cutoff_seq.
2. Numeric ahead-of-stream cursors are normalized to replay-from-start:
on the active path, after_seq > snapshot_cutoff_seq resets to 0 before
gap-coverage/replay so a truncated buffer can no longer falsely declare
coverage and lose the pre-tail events; on the dead-stream path the
cursor is normalized against the journal summary's authoritative
last_seq before _replay_run_journal, so an ahead cursor returns the
full replay instead of an empty SSE body.
3. Runner-local now consumes the resolved cursor instead of re-parsing
after_event_id or re-reading Last-Event-ID. The resolver also returns
the opaque raw cursor so the runner uses the already-precedence-checked
value; explicit opaque cursor= keeps precedence, invalid/foreign
cursors pass no cursor (full replay), and the header is honored only
when no explicit cursor was supplied.
Regression tests cover the equality dedup, active+dead ahead-of-stream
normalization (with after_seq-filtering mocks that assert the real replay
body), and the three runner precedence probes (malformed-explicit blocks
header, foreign passes no cursor, opaque cursor= wins).
Related hermes-webui/hermes-android#58
* fix(sse): carry runner-cursor provenance through /api/chat/stream resume
Address PR #6886 r3 review (gate-fail). The (after_seq, resume_requested,
raw_cursor) tuple carried the cursor VALUE but not its PROVENANCE, so the
runner handoff couldn't distinguish a valid opaque runner cursor from a
journal-shaped one from a malformed one — and gated the runner handoff on
journal-specific validity, which is wrong for the runner adapter (its
cursors are opaque).
The resolver now returns a distinct, centrally-resolved runner cursor:
- a valid after_seq PAIRS with whatever after_event_id shape was supplied
— including an opaque runner id like event:2 that the journal parser
reads as a foreign run — so the paired runner cursor resumes at the seq
(never None / full replay, which would duplicate tokens and tool
events). after_seq is read directly (via _parse_run_journal_after_seq_value)
rather than through the after_event_id-first journal parser, which would
swallow a paired opaque runner id.
- a header-only opaque runner id (Last-Event-ID: event:2) is preserved
as-is so the runner resumes from it.
- a malformed or foreign explicit cursor WITHOUT a valid paired after_seq
still yields no runner cursor, blocking Last-Event-ID and replaying from
start — the r2 malformed-blocks-header guarantee is unchanged.
Regression tests pin the paired opaque id, header-only opaque id, and
malformed-with-valid-seq runner resumes, plus the unchanged
malformed-without-pair replay-from-start behavior.
Related hermes-webui/hermes-android#58
* fix(sse): normalize ahead cursor when the snapshot cutoff is unknown
Address PR #6886 r4 review (gate-fail). The ahead-cursor normalization
was guarded by `snapshot_cutoff_seq is not None`, so when the subscribe
snapshot had no parseable last_event_id (channel has not seen an
id-bearing frame yet), a positive ahead cursor (e.g. Last-Event-ID
run_1:999) was never normalized — it was installed verbatim as the live
dedup bound, filtering EVERY queued frame including the terminal
stream_end fence. The reconnect stalled on heartbeats with an empty SSE
body. origin/master ignored the header on this path and delivered the
events, so this was a new regression.
Fix: treat an unknown, non-truncated snapshot cutoff as fence 0 before
the ahead-cursor normalization, so a positive client cursor can't become
the live dedup bound with no cutoff to bound it — it fails closed to
replay-from-start, delivering the buffered events rather than silently
losing them. Frames dropped while the cutoff is unknown still cannot
prove gap coverage (cutoff None → not covered), so the recovery_control
fail-closed path is preserved.
Regression test: Last-Event-ID run_1:999 + snapshot_cutoff_seq=None
delivers queued events run_1:1 and run_1:2 exactly once, emits the
terminal stream_end, and releases the subscriber. Without the fix the
test stalls on heartbeats (pytest-timeout kills it), proving the stall.
Related hermes-webui/hermes-android#58
* Release — SSE Last-Event-ID resumption fix (#6886)
---------
Co-authored-by: Paladin173 <35980893+Paladin173@users.noreply.github.com>
Co-authored-by: n <a@n>
* Add scoped extension Configure hook
* docs: fail-closed optional chaining in Configure consumer example + old-Core regression
The documented consumer used ext?.settings.registerConfigure(...), which throws
TypeError on an older E0 Core where settings exists but registerConfigure does not.
Use full optional chaining (ext?.settings?.registerConfigure?.()) and add a Node
regression proving the consumer survives a Core lacking the capability.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
---------
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: n <a@n>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
* fix(tests): make mtime_invalidation and glm_5_3 tests order-independent (#7100)
* fix: expose max reasoning for GPT-5.6 models
* fix(recovery): do not reattach cancelling runs
ACTIVE_RUNS tracks worker lifecycle, which is deliberately broader than
"a turn a browser may attach to". cancel_stream() keeps the row as
phase="cancelling" while the worker unwinds so a successor turn cannot
start on top of it, but the client has already reached a terminal state
for that stream: its run journal ends in a terminal event.
The recovery lookups treated every same-session ACTIVE_RUNS row as
attachable. An idle session holding a cancelling row therefore received a
recovered server_turn_started on every /api/session/stream subscription:
the client attached, replayed the terminal event, tore the renderer down,
resubscribed, and the server replayed the same frame again. The result is
an endless attach/replay loop that rebuilds the transcript repeatedly.
Separate the two meanings instead of narrowing one call site:
- api/config.py gains active_run_is_attachable() and
active_run_cancel_is_stale() as the shared predicates.
- active_stream_id_for_session() (browser recovery) returns attachable
rows only; _session_has_active_turn() (busy check) keeps counting a
fresh cancellation so a successor cannot overlap the unwinding worker.
- _live_active_stream_id() applies the same rule to the hidden-tab
status poller, on both the STREAMS and ACTIVE_RUNS paths.
- routes._cancelled_run_is_stale() now delegates to the shared predicate
rather than keeping a parallel copy of the staleness rule.
- A cancelling row past a bounded unwind window with no live STREAMS
channel is reclaimed from ACTIVE_RUNS and its stream owner released, so
a wedged worker cannot suppress background wakeups forever. Age alone
does not reclaim a row that still owns a live channel.
Staleness anchors on cancelled_at, falling back to started_at, so a
long-running turn cancelled moments ago is never treated as an orphan.
tests/test_cancelling_run_not_attachable.py covers both directions of
each rule. The six behavioral tests fail on the unpatched tree and pass
with the fix; two targeted mutations (forcing the attachability predicate
true, and disabling the staleness reaper) each turn the suite red.
* docs(rfc): document cancellation attach/admission split
Records the runtime contract introduced by the recovery fix in the WebUI
run-state consistency RFC, so the distinction is discoverable instead of
living only in code comments.
- Adds ACTIVE_RUNS to the State Layers table as the worker-lifecycle
registry, explicitly not the set of runs a browser may attach to.
- Adds invariant 9: lifecycle-busy is not client-attachable. Cancellation
splits the two meanings, recovery paths must exclude cancelling rows,
and admission checks must keep counting them.
- Documents the bounded cancellation-unwind window: reclamation needs
both age and the absence of a live STREAMS channel, and staleness is
anchored on the cancellation timestamp.
- Extends the review checklist with the admission-vs-attachment question
and the evidence required when changing a reclamation window.
* docs(changelog): note #7096 cancelling-run reattach, #7083 GPT-5.6 max reasoning, #7101 test order-independence
---------
Co-authored-by: webtecnica <webtecnica@gmail.com>
Co-authored-by: Abdulrahman Elkenany <boudy.elkenany123@gmail.com>
Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
Co-authored-by: n <a@n>
* feat: add GLM-5.3 to Z.AI model list
Add glm-5.3 as the newest zai entry in _PROVIDER_MODELS and a matching
zai/glm-5.3 entry in _FALLBACK_MODELS, and bump the Z.AI onboarding
default_model from glm-5.1 to glm-5.3 (Z.ai's current flagship; legacy
GLM-5.2/5.1 requests are routed to GLM-5.3 per docs.z.ai).
Reasoning gating needs no change: _zai_glm_classification() treats
GLM >= 5.2 as effort-ladder capable, so glm-5.3 is already covered and
pinned by tests/test_zai_reasoning_effort_gating.py.
New regression coverage in tests/test_glm_5_3_catalog.py: catalog
presence, newest-first ordering, fallback entry, onboarding default,
full reasoning_effort ladder, and the get_available_models() payload.
* test: restore _cfg_fingerprint in catalog test fixture
Review follow-up (#7017): the isolation fixture snapshot restored cfg,
_cfg_mtime, and _cfg_path but left _cfg_fingerprint pointing at the
temporary config loaded by the payload test. api/config.py uses that
fingerprint to distinguish in-memory overrides from changed files
(config.py:371), so a stale value could make later same-process tests
skip reloading a changed config. Snapshot and restore it like the rest.
* fix: keep Z.AI onboarding default at glm-5.1 until direct API serves GLM-5.3
Review follow-up (#7017): GLM-5.3 is live on Z.ai's Coding Plan
endpoint only; the direct api.z.ai endpoint the zai provider uses
still lists the GLM-5.3 API as coming soon. Defaulting new direct-API
users onto glm-5.3 would fail their first message, so the catalog
addition stays (opt-in) and the default stays glm-5.1. Bump the
default in a follow-up once the direct endpoint serves GLM-5.3.
* fix(docker): probe the configured state dir before /workspace for UID/GID (#7027)
UID/GID auto-detection probed /workspace before the configured
HERMES_WEBUI_STATE_DIR. In a stock single-container image none of the
priority-1 candidates exist, but /workspace does — owned by the image's
build-time 1024:1024. Detection therefore returned the image's own owner,
which carries no information about the host, while the one directory whose
owner *is* the host UID by definition — the state-dir bind mount — was never
probed. With a host-owned state mount and no explicit WANTED_UID the
container remapped to 1024, failed its own state-dir writability check, and
restart-looped.
The log line made this expensive to debug: 1024 is also the fallback default,
so "Auto-detected workspace UID: 1024" read as if detection had found nothing.
- probe ${HERMES_WEBUI_STATE_DIR:-/app/data} first, for both UID and GID
- keep /workspace as a lower-priority signal (unchanged for setups that
actually bind-mount it), and keep the hermes-home probes from #668
- stop treating an explicitly supplied 1024 as "unset": the sentinel and a
valid UID were the same number, so an operator who deliberately ran as 1024
got it overwritten by detection. The explicit/detected origin is persisted
next to the value because `su` drops the environment when the script
re-enters as the runtime user, so the second pass would otherwise see an
explicit choice as a detected one.
Tests: tests/test_7027_state_dir_uid_probe.py runs the real resolution block
under bash with `stat` stubbed, covering the non-1024 state mount, the
explicit 1024 override (including across the privilege drop), and the
pre-existing /workspace + hermes-home fallback paths. A new state-dir-uid job
in the Docker smoke workflow boots a real container on a host-owned state
mount and gates on /health.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Release batch B: Docker UID fix + GLM-5.3 catalog (experimental)
Two independently gate-passed contributor PRs, rebased fresh onto master and
re-gated as a combined stage (Codex SAFE TO SHIP; full suite green except the 8
pre-existing approval-test failures that fail identically on clean origin/master
— CI green on the same commit; tracked separately for a fix).
- #7027 (@jorgejiro) probe configured state-dir before /workspace for Docker
UID/GID; persist explicit marker so a supplied 1024 survives root->su re-entry
(fixes the single-container restart loop) (#7034)
- #7017 (@rh-id) add GLM-5.3 to the Z.AI model list (onboarding default stays glm-5.1)
Co-authored-by: jorgejiro <jorgejiro@users.noreply.github.com>
Co-authored-by: rh-id <rh-id@users.noreply.github.com>
---------
Co-authored-by: Ruby Hartono <58564005+rh-id@users.noreply.github.com>
Co-authored-by: Jorge <jorgejiro@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: n <a@n>
Co-authored-by: jorgejiro <jorgejiro@users.noreply.github.com>
Co-authored-by: rh-id <rh-id@users.noreply.github.com>
* feat(docker): enable gateway API and password forwarding in compose files
- Added API_SERVER_KEY forwarding to enable the gateway API listener in multi-container setups.
- Configured HERMES_WEBUI_PASSWORD to ensure password authentication works as expected.
- Updated documentation to clarify API_SERVER_KEY requirements and forwarding behavior.
- Added regression tests to cover the new functionality and ensure proper configuration.
* build(docker): force LF line endings for shell scripts via .gitattributes
A CRLF shebang (#!/bin/bash\r) breaks locally-built Docker images: the
kernel can't resolve the interpreter /bin/bash\r, so 'docker compose up
-d --build' on Windows with core.autocrlf=true fails with
'exec /hermeswebui_init.bash: no such file or directory'. The repo blobs
and CI/GHCR builds are LF and unaffected; only Windows local builds
baked the CRLF checkout into the image.
* fixup(docker): dedup API_SERVER_HOST after rebase onto #6968 (which already added it)
* docs(changelog): stamp #6917 (multi-container gateway API forwarding)
---------
Co-authored-by: Yang <yang_qwq@qq.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix(title): honor auxiliary.title_generation.enabled=false in WebUI title paths (#6814)
Hermes Agent gates title generation on auxiliary.title_generation.enabled
(default true), but the WebUI orchestration only used the aux config to pick
a model route, so enabled=false still triggered LLM title calls via the
active chat model or the aux route, and could persist a fallback title.
Add a profile-scoped _aux_title_generation_enabled() helper beside
_get_aux_title_config() with the same default/truthy semantics as the Agent
(boolean and string forms). Gate three automatic paths on it:
- _run_background_title_update(): skip with status skipped /
title_generation_disabled, provisional title untouched, no fall-through
to the active agent model.
- _run_background_title_refresh(): skip with refresh_skipped /
title_generation_disabled.
- generate_session_title_for_session(): explicit (None,
'title_generation_disabled', '') so manual regenerate returns an
explicit disabled response and imported-session auto-titling stays quiet.
Checks run inside the profile-scoped worker context so a background job
cannot read another profile after a switch.
Tests (tests/test_issue6814_title_generation_enabled_gate.py, 20 cases):
default-on when omitted, disabled with/without aux route (no LLM call,
title untouched, skipped status, stream_end still emitted), string/boolean
false forms, adaptive refresh path, manual regenerate explicit response,
helper parsing matrix. fail-before: 19 failed on unpatched code.
Regression: title family 154 passed (incl. test_2235 routing cases).
* docs: document auxiliary.title_generation.enabled behavior (#6814)
Addresses the Greptile #6877 review note: document the newly enforced
setting and the manual-regenerate disabled response in
docs/advanced-chat-setup.md.
* fix(title): mirror Hermes agent's canonical is_truthy_value contract exactly
Gate review (Codex) found the enabled-parser diverged from the agent's canonical
contract: it used a 'disable on false/0/no/off' blocklist, but the agent uses
is_truthy_value(value, default=True) — an allowlist (utils.TRUTHY_STRINGS =
{1,true,yes,on}). So None wrongly routed through the string branch and empty/
unrecognized strings wrongly stayed ENABLED. Now: None->True (default), bool
unchanged, str truthy ONLY for {1,true,yes,on}, else bool(value) — byte-for-byte
with agent/title_generator._auto_title_enabled. Test matrix corrected + expanded.
Co-authored-by: X-Shihai <X-Shihai@users.noreply.github.com>
---------
Co-authored-by: X-Shihai <48356232+xushihai@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: X-Shihai <X-Shihai@users.noreply.github.com>
* feat(extensions): token-v1 proxy->sidecar auth boundary
Mint a per-extension secret core injects (X-Hermes-Sidecar-Token) on every
proxied request; sidecars validate it. Closes the hole where a loopback sidecar
port is reachable by any local process and cannot distinguish a proxied request
from a direct one.
- api/extension_sidecar_auth.py: per-extension token lifecycle (atomic mint,
re-read-verified so an unpersisted token is never injected, per-request
mtime-cached read for live rotation, path-escape-safe)
- manifest proxy_auth negotiation: absent=legacy, token-v1=enforce, unknown=fail-closed
- proxy: inject token, strip inbound + response x-hermes-*, auth-off posture
(loopback-only local_unprotected, else 503), fail-closed when token unavailable
- consent-time auth_required in status payload; mint-on-consent
- docs/EXTENSIONS.md proxy_auth section
- 7 new tests (26/26 green)
* fix(extensions): address Codex+Fable gate on token-v1 (6 findings)
- align token-module extension-id grammar with core _EXTENSION_ID_RE (was
narrower -> legally-named ext consented then 503'd forever)
- resolve token dir dynamically (mirror _extension_state_dir) -> real test
isolation, no import-time STATE_DIR cache
- cross-process mint: O_CREAT|O_EXCL no-clobber claim (was os.replace clobber)
- rotation cache keyed on full fingerprint (ino/dev/mtime/ctime/size) +
re-fingerprint after read -> same-size/mtime replacement no longer stale
- validate token format on read (url-safe, 16-256) -> malformed file can't
leak via a ValueError echoed in a 502
- consent fails 503 when token can't be provisioned (was silent-swallow ->
persisted consent then 503 forever)
- rename status auth_required -> posture enum (protected|local_unprotected):
nothing is blocked for loopback, so 'required' was misleading
- panels.js: render local_unprotected warning on the consent row (+ CSS)
- docs: token-path resolution order, 401-vs-503, explicit 'legacy' acceptance
- tests: route-level token-injection+response-strip test; fix illusory isolation
in token-module test; 27/27 green
* fix(extensions): close 2 token-mint races (Codex re-gate round 2)
- mint via temp-file + atomic os.replace (not O_CREAT|O_EXCL) so the final
path is never observed empty/half-written — a concurrent loser can no longer
read an empty token file and 503
- single _stable_read helper (fingerprint-read-refingerprint, bounded retry on
mid-read change) used by BOTH ensure_token and current_token — a token that
changes during the read is never returned or cached stale
- stress-verified: 20 concurrent first-mints converge on 1 persisted token; 27/27
* fix(extensions): atomic no-clobber token publish via os.link (Codex re-gate round 3)
os.replace fixed empty-file exposure but still clobbered cross-process: two
processes could both write+replace and a reader between them got a token no
longer on disk -> 401. Switch to the repo's TOCTOU-safe os.link create-or-fail
idiom (session_recovery.py:627): write temp -> link into place (fails if a
winner already published) -> loser drops its temp and reads the winner via
_stable_read. Proven: 16 concurrent PROCESSES converge on 1 persisted token,
all matching disk. 27/27 tests green.
* fix(extensions): resolve Frank+Greptile #6331 review — token-v1 fail-closed when auth off (consent+resolution), token-v1-only proxy_auth/posture status fields, fullmatch ext-id validator
* fix(extensions): finish Frank and Greptile sidecar review
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
* docs: fold in Rod's PR-review feedback into GUIDELINES + CONTRIBUTING
Based on rodboev's feedback distilled from 50+ recent PRs. Four accepted
points plus one clause, folded into existing rules rather than adding new ones:
- Rule 6: load the reporter's shipped reproduction, don't rebuild a fixture
from your reading of it (the one genuine hole — a fix and test from the same
wrong model agree with each other and certify a no-op).
- Rule 2: confirm a value is authoritative (declared at the point of intent),
not inferred from id prefix / content shape / emptiness / DOM state.
- Rule 4: read prior PRs and review threads to find a subsystem's real variants
instead of inventing axes from the single case handed to you.
- Rule 1: the chokepoint is the smallest boundary that contains the fault, not
the widest you can reach (don't disable a whole pipeline to suppress one output).
- "Show your work": name who owns the truth for any claim the repo doesn't own.
CONTRIBUTING.md carries the two contributor-facing points (repro-loading,
proof-ownership) in the PR-description section, deferring detail to GUIDELINES.md.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* docs: tighten Rule 6 escape hatch per Rod — gate on shape under-specified, not file absent
Rod's review: the #5749 no-op came from a fully prose-specified repro (fenced JSON
+ field-level conditions + steps), not a missing file. The old hatch ('why the issue
gave you nothing to load') reads as 'no downloadable attachment', letting someone walk
past a binding JSON block. Gate the hatch on the SHAPE being under-specified instead:
a fenced JSON structure / field conditions / step list pin the shape as bindingly as a
file; if pinned, satisfy every condition and don't add a property the shape never had
to make a guard fire; only say 'constructed, assumed X' when the shape is truly unpinned.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
* docs: distill Rule 6 repro-shape guidance into a principle (Rod style note)
Rod: 'distill into durable principles, don't enumerate lists; strip negative
conditions that read like narration.' Reworked the addition to lead with the
principle ('a reproduction is whatever pins the bug's shape'), collapse the
capture/JSON/conditions/steps enumeration into flowing prose, and convert the
'don't add a property...' negative into a positive imperative ('bind your fixture
to that shape: satisfy every condition... instead of granting...'). Same for the
CONTRIBUTING bullet.
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Distilled habits that get a PR merged in one review round instead of several:
four root causes + ten rules + show-your-work PR-description contract. Full doc
in docs/GUIDELINES.md, compressed 10-point form in AGENTS.md (agent entry point),
pointer added to CONTRIBUTING.md reading list.
The RFC (docs/rfcs/session-sse-contract-v1.md) cited hardcoded absolute
line numbers like `api/routes.py:12345-12346` and `api/routes.py:16177`,
and tests/test_issue4812_session_sse_contract_rfc.py validated those exact
lines against source. Any PR that shifted routes.py lines (e.g. #5543,
#5534) broke the test — a chronic brittle-failure class (#5542).
Root fix: anchor by SYMBOL, never by line number.
RFC:
- Strip every `api/routes.py:<NNNN>` / `<NNNN>-<NNNN>` and
`api/streaming.py:<NNNN>` line-number suffix; keep the symbol name
(route string, function/constant names) + the file, e.g.
"_handle_session_events_stream() in api/routes.py".
- Update the inventory note to state names are the stable anchors and the
RFC deliberately avoids line numbers so a source-layout shift can't
invalidate the doc or its contract test.
Test:
- Rewrite the two line-anchor tests to assert (a) each cited symbol still
exists in api/routes.py and (b) the RFC names that symbol — dropping the
`api/routes.py:<line>` regex bounds-checks entirely. Invariant preserved:
"the RFC's cited symbols are real and named in the doc", NOT "the cited
line numbers are exact".
- Add test_rfc_uses_no_hardcoded_routes_line_numbers as a regression guard
that fails if any `*.py:<line>` anchor is reintroduced into the RFC.
Proven de-brittled: prepending 31 lines to api/routes.py (scratch, reverted)
leaves all 34 tests green.
Refs #5542, #5513
Make focused compression-recovery continuation creation idempotent by marking child sessions and reusing the existing child on repeated start requests. Also surface malformed successful recovery responses in the UI instead of silently re-enabling the action.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Codex re-gate finding: line 83 cited api/routes.py:15703-15762 for live SSE id:
emission, but that range is replay/helper code — the actual _sse_with_id() call
sites are 15918 (live /api/chat/stream), 15811 (runner-observe), 15721/15734
(journal replay). Corrected the bullet with the real, distinct anchors.
Also extended the test (test_rfc_run_journal_anchors_land_on_real_source) to
validate each named-symbol anchor (_parse_run_journal_event_id/_after_seq,
_runner_event_id, _replay_run_journal) + the live-emission anchor against the
actual source token, so no stale line number can silently pass.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Codex Phase-1 gate found the RFC cited stale/wrong source anchors (docs-only, but
actively misleading to future implementers): /api/sessions/events is routed at
routes.py:12345-12346 (RFC said 12320-12321 = /api/chat/cancel);
_handle_session_events_stream at 16177 (RFC said 16142 = a gateway-sse comment);
replay reads after_event_id/after_seq QUERY PARAMS (RFC said Last-Event-ID header
— that's the *proposed* new-endpoint contract, not current). Corrected all six
routes.py anchors + the replay-mechanism description, verified each against master.
The accompanying test asserted the WRONG anchors and passed (codifying the error).
Rewrote test_rfc_cites_current_global_endpoint_source to parse every routes.py
anchor from the RFC and verify it lands on the real /api/sessions/events route +
_handle_session_events_stream definition — so it now catches drift instead of
locking in a stale number.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Rebase PR #5162 (rename busy_input_mode -> default_message_mode; flip the
default from 'queue' to 'steer') onto current origin/master WITHOUT dropping
the shipped #5170 localStorage persistence mirror.
Rename the mirror machinery to the new setting name for consistency:
_BUSY_INPUT_MODES -> _DEFAULT_MESSAGE_MODES (values unchanged)
_normalizeBusyInputMode -> _normalizeDefaultMessageMode (fallback now 'steer')
_persistBusyInputMode -> _persistDefaultMessageMode
_readPersistedBusyInputMode -> _readPersistedDefaultMessageMode
window._busyInputMode -> window._defaultMessageMode (+ renamed exports)
localStorage: write the new 'hermes-default-message-mode' key; read it with a
fallback to the legacy 'hermes-busy-input-mode' key so an existing user's
persisted preference survives the rename.
Preserve #5170 behavior at every mirror site under the new names:
- boot success -> window._defaultMessageMode=_persistDefaultMessageMode(...)
- boot FAILURE -> window._defaultMessageMode=_readPersistedDefaultMessageMode()
(NOT a hardcoded 'steer' — a saved 'interrupt'/'queue' must still apply when
the server is unreachable; do not regress #5167/#5132)
- preferences autosave, settings-panel load, and _applySavedSettingsUi all
persist through _persistDefaultMessageMode(...)
Tests updated for the rename while keeping the persistence-behavior assertions
(test_1062, test_5145, test_5167); test_5167 gains explicit guards that the
load-failure path reads the persisted pref and never hardcodes a literal mode,
plus autosave/panel-load mirror-write coverage.
Co-authored-by: Rod Boev <rod.boev@gmail.com>