Commit Graph
7972 Commits
Author SHA1 Message Date
taljeon 270832cf4c docs: bound Serve to restricted tailnets 2026-09-04 13:52:45 +09:00
taljeon c45ee592f2 docs: make Serve the canonical remote entry point 2026-09-04 13:42:54 +09:00
taljeon b6aa179fd1 docs: locate runit and s6 launch commands 2026-09-04 13:41:11 +09:00
taljeon ff69c0342d docs: add supervisor transition commands 2026-09-04 13:36:28 +09:00
taljeon 816ebd5e47 docs: preserve lifecycle owner for Serve setup 2026-09-04 13:31:45 +09:00
taljeon 56df987311 docs: cover supervised Serve rebinding 2026-09-04 13:29:32 +09:00
taljeon 833b44a42f docs: require restart before Serve rebinding 2026-09-04 13:25:16 +09:00
taljeon d275815c1f docs: protect checkout password settings 2026-09-04 13:21:10 +09:00
taljeon ca4d2b0dc6 docs: clarify reverse-proxy cookie settings 2026-09-04 13:07:43 +09:00
taljeon 8adcabc4fa docs: pin systemd Serve configuration 2026-09-04 11:20:51 +09:00
taljeon 38852d8311 docs: secure Serve session cookies 2026-09-04 11:08:12 +09:00
taljeon ccc6b189c2 docs: require auth before enabling Serve 2026-09-04 10:44:35 +09:00
taljeon a24bfc8867 docs: respect launcher configuration precedence 2026-09-04 10:36:51 +09:00
taljeon 84d7d75f0b docs: pin Serve backend to loopback port 2026-09-04 10:34:36 +09:00
taljeon 21c6e7e5d7 docs: add Tailscale Serve setup 2026-09-04 10:28:28 +09:00
nesquena-hermes e168b67e42 Merge pull request #7307 from nesquena/release/exp-v0.52.264
Release exp-v0.52.264: fast regenerate via bounded sidecar-anchored tail read (#7204, @webtecnica)
2026-08-25 18:03:26 -07:00
n ebc631f53a Release exp-v0.52.264: fast regenerate via bounded sidecar-anchored tail read (#7204, @webtecnica)
Adds the CHANGELOG entry for #7204 (closes #6826). Release-metadata only;
the code change ships via the merge of #7204 (contributor webtecnica).
2026-08-26 01:01:19 +00:00
nesquena-hermes 134327aa41 Merge pull request #7204 from webtecnica/fix/6826-issue
Bounded sidecar-anchored tail read for regenerate (closes #6826). Thanks @webtecnica — six rounds of adversarial re-gate to convergence.
2026-08-25 18:00:52 -07:00
nandwebtecnica 3fac79bb59 lint: drop redundant unused import in regeneration test (#6826)
Removes the local re-import of api.streaming._session_payload_with_full_messages
inside test_in_tail_duplicate_guard_refuses_bounded_and_full_revision_accepted;
it is already imported at module level (line 17) and unused in the function body,
tripping F401 on CI's hosted lint. Mechanical maintainer fix per auto-fix policy.

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-08-26 00:24:07 +00:00
webtecnica b36fbdd633 fix(regeneration): refuse bounded tail on in-tail duplicate keys + real WAL interleaving test (#6826 r5) 2026-08-26 00:24:07 +00:00
webtecnica 7abd4773ca fix(regeneration): snapshot reuses canonical projection+ordering; WAL data_version guard (#6826 r4) 2026-08-26 00:24:07 +00:00
webtecnica b33aff9d09 fix(regeneration): occurrence-count guard + single-snapshot (no TOCTOU) for bounded tail read (#6826 r3 round-3) 2026-08-26 00:24:07 +00:00
webtecnica 40ff74b6fc fix(regeneration): bounded tail read only when skipped state.db prefix is provably identical; compression-anchor coverage (#6826 r3) 2026-08-26 00:24:07 +00:00
webtecnica 3672e0038b fix(regeneration): bounded sidecar-anchored tail read instead of full transcript refetch (#6826) 2026-08-26 00:24:07 +00:00
webtecnica 8c9f505fab fix(ui): avoid full transcript fetch on regenerate (#6826) 2026-08-26 00:24:07 +00:00
3b9c632a1f test: make model cache regressions hermetic (#7235)
* test: make model cache regressions hermetic

* test: wait for model cache worker cleanup

* test: fully invalidate model cache state

* ci: re-trigger checks (first-time contributor workflow approval)

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
Co-authored-by: n <a@n>
2026-08-24 02:53:47 -07:00
nesquena-hermesandn fcb28248a1 Release exp-v0.52.263: batch sidebar subagent source lookup (#7231, @starship-s) (#7266)
Co-authored-by: n <a@n>
2026-08-24 02:21:27 -07:00
starship-sandnesquena-hermes 037a35abfd perf(sessions): batch sidebar subagent source lookup (#7231)
Reuse the authoritative batched state.db sidebar metadata to classify delegated subagents before route filtering. Keep direct lookups for single-session recovery and mutation guards.

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-24 02:18:48 -07:00
nesquena-hermesandn df65251d58 release: exp-v0.52.262 — native-image mirror reconciliation (#7230) (#7236)
Co-authored-by: n <a@n>
2026-08-22 15:48:23 +00:00
starship-s 78b0425902 fix(sessions): reconcile native image message mirrors (#7230)
* fix(sessions): reconcile native image message mirrors

Keep the rich WebUI sidecar row when Hermes Agent stores the same
exact-timestamp native image turn as scalar screenshot-marker text.

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna
Assisted-by: OpenCode:opencode/x-preview-f-free
Assisted-by: Claude Code:claude-opus-5

* docs(replay): document native image mirror contract

Record the exact cross-store identity requirements and fail-closed
behavior for native image session reconciliation.

Assisted-by: Hermes Agent:gpt-5.6-sol

* fix(sessions): fail closed on state mirror identity gaps

Treat present-versus-absent candidate identity fields as ambiguous so
session restore preserves distinct state-only turns and their metadata.

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna
2026-08-22 08:47:08 -07:00
nesquena-hermesandn a00b02fccb Release: transcript-reopen bounded state.db cache (#7212, @ruizanthony) (#7226)
Co-authored-by: n <a@n>
2026-08-22 04:51:22 +00:00
ruizanthonyandAnthony Ruiz 00e4c5af6f perf(api): skip state load on cached transcript reopen (#7212)
* perf(session-load): memoize the compression-lineage display stitch

_webui_sidecar_lineage_messages_for_display re-merged multi-thousand-row
parent+child transcripts on EVERY GET /api/session (measured 5.1s per
request on a real 5.2k-message lineage, py-spy: merge_session_messages_
append_only + _matching_visible_duplicate dominate). The merge result is
now cached per session id, validity keyed by the exact stat signature of
the child sidecar AND every snapshot parent involved, bounded LRU(16).
Any write to any involved sidecar invalidates naturally; cache hits and
the populating call hand out shallow-copied rows so caller-side display
metadata cannot corrupt cached state.

Measured: 5.17s cold -> 0.002s warm on the real session; invalidation
after touch re-merges. Tests: tests/test_lineage_display_cache.py
(correctness, copy isolation, child/parent invalidation, no pollution
for lineage-less sessions).

* perf(api): memoize inactive-session display merges

* perf(api): skip state load on cached transcript reopen

* test(api): remove unused lineage-cache import

* fix(api): reject streaming key without state database

* test(api): isolate display cache fixture

* fix(api): bind cache publication to read signature

* fix(api): harden display cache invalidation

* fix(api): bind cache hits to lineage provenance

* fix(api): close remaining display cache scope races

* fix(api): scope display cache revision to target

* fix(api): evict unverifiable lineage provenance

* fix(api): keep incomplete lineage provenance uncached

---------

Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
2026-08-21 21:49:59 -07:00
nesquena-hermesandnesquena-hermes cfcc39194a Release exp-v0.52.260: opt-in bindable ports for the three-container Docker stack (#7145, @mercael91) (#7202)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-21 03:33:33 -07:00
43eb46c17f Fix Hermes agent port configuration (#7145)
* Fix Hermes agent port configuration

* fix: bind dashboard to 0.0.0.0 so port-forwarded hosts can reach it

Restore the full three-container compose file (previous commit replaced
it wholesale) and change only the dashboard port binding: 127.0.0.1:9119
prevents access via the server IP, which is exactly the issue reported
in #6876.

* compose: restore loopback-only dashboard binding, document opt-in

The dashboard runs --insecure; publishing it on 0.0.0.0 exposes an
unauthenticated service on every host interface. Restore the
127.0.0.1:9119:9119 binding that matches the file convention (all
services are loopback by default) and document the opt-in exactly
like the webui service does (#6876).

* fix: bindable ports for remote access (issue #6876)

---------

Co-authored-by: mercael91 <mercael91@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-21 10:28:45 +00:00
nesquena-hermesandnesquena-hermes 139b8d6703 Release exp-v0.52.259: resolve @provider:model through the shared parser (#7183, @darthzen) (#7200)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-20 23:48:29 -07:00
05ab4173cf fix(profiles): resolve @provider:model through the shared parser (#7183)
_split_webui_provider_model_value() and _strip_webui_provider_prefix()
split provider-qualified model values at the last colon. That amputates
any model name carrying its own tag: @ollama:qwen3.8:27b-mtp-q8_0
resolved to model 27b-mtp-q8_0 with provider ollama:qwen3.8, and the
truncated name was sent upstream, which 404'd. The splitter also cannot
distinguish a tagged model from a multi-segment custom provider ID, so
@custom:mykey:model-a:free was affected the same way.

Both sites now delegate to config._parse_provider_qualified_model_id(),
the grammar routes.py and the gateway adopted in #6723. The import is
function-local, matching the existing api.config imports in this module.

_split_webui_provider_model_value() persists its result into profile
config, so the mangled name outlived the session that produced it.

Fixes #7182.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-21 06:45:52 +00:00
nesquena-hermesandnesquena-hermes c54d3faca5 Release exp-v0.52.258: resolve session exports against document base URI (#7189, @lowlandsheperd) (#7199)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-20 23:38:26 -07:00
Kenanandnesquena-hermes 6c0f2c765c fix(export): resolve session exports against document base URI (#7189)
* fix(export): resolve session exports against document base URI

* test(export): cover subpath deep links

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-21 06:32:50 +00:00
nesquena-hermesandnesquena-hermes 8066462fcc test(#1699): make static-catalog invalidation assertions overflow-aware (#7197)
The two static-catalog-change invalidation tests asserted the newly-appended
opencode-go model appeared in the group's visible `models` list. That held when
the test was written (opencode-go had 14 static entries), but the curated list
has since grown to 27 — past the model-picker overflow threshold — so the
rebuilt group now splits into a visible `models` head (capped) plus an
`extra_models` overflow tail, and the appended entry (added at the end) lands in
`extra_models`. Cache invalidation itself works correctly; the assertion was
stale.

Assert the appended model is surfaced across the combined picker catalog
(`models` + `extra_models`) so the test stays a pure cache-invalidation
regression check, independent of the overflow cap. Verified non-vacuous (a
genuinely-absent id still fails). No production change.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-20 22:35:33 -07:00
nesquena-hermesandnesquena-hermes 23df7bd8f2 test(mcp): skip mcp_server tests cleanly on an unsupported mcp SDK (#7196)
The test module only did importorskip('mcp'), which checks the package is
present but not that it exposes the 1.x Server.list_tools/call_tool decorator
API that mcp_server.py is built on. On an environment carrying mcp>=2.0.0
(which dropped that API), importing mcp_server hard-exits (SystemExit) at its
own SDK version guard, so every test errored at setup instead of skipping.

Reuse the exact predicate mcp_server.py uses (hasattr(Server, 'list_tools'))
to module-level-skip when the installed SDK is out of the supported range.
CI, which pins mcp>=1.28,<2 via requirements-dev.txt, still runs the full
module; a box with a newer mcp now skips cleanly instead of producing
spurious errors/failures.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-20 22:24:00 -07:00
nesquena-hermesandnesquena-hermes 5f81ed2b1c Release exp-v0.52.257: fence compression with durable transcript revisions (#6554, @ruizanthony) (#7194)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-20 22:06:04 -07:00
c27f3d67a8 fix(compression): fence durable transcript revisions (#6554)
Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-20 22:01:52 -07:00
nesquena-hermesandnesquena-hermes 46c734a721 Release exp-v0.52.256: suppress false dashboard loopback warning (#6573, @webtecnica) (#7192)
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-08-20 21:35:11 -07:00
webtecnicaandnesquena-hermes 65c185478c fix(ui): suppress false loopback-only warning when public URL is configured (#6573)
* fix(ui): suppress false loopback-only warning when public URL is configured

When a user configures status.browser_url (e.g., via a reverse proxy
at https://dashboard.example.com), the Dashboard link correctly opens
that public URL, but _applyDashboardStatus() still displayed the
'loopback-only' warning because it only checked the browser's hostname.

Fix: suppress the warning when status.browser_url is present — the
user has explicitly configured a public URL, so the loopback bind
is intentional and the warning is a false positive.

Closes #6459

* fix(ui): base loopback warning on resolved dashboard target hostname (#6573)

* fix(ui): complete loopback classifier for dashboard warning (#6573)

---------

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-20 21:32:08 -07:00
nesquena-hermesandn 069a55db58 Release exp-v0.52.255: new chat inherits current conversation workspace (#7180, @ruizanthony) (#7186)
Co-authored-by: n <a@n>
2026-08-20 23:38:22 +00:00
ruizanthonyandAnthony Ruiz eee72f942e fix(workspace): new chat inherits the current conversation's workspace (#7180)
* fix(workspace): new chat inherits the current conversation's workspace

`newSession()` resolved the workspace of a new chat as:

    switchWs || S._profileDefaultWorkspace || S.session.workspace

`S._profileDefaultWorkspace` is global to the profile, hydrated from
`/api/profile/active`. Preferring it over the current conversation meant a new
chat opened from a MES conversation landed on the profile-global value instead
of MES. With several conversations running in parallel on different workspaces,
the context the user was actually in was systematically lost.

The precedence is worse than it looks, because that "profile default" is not a
stable configured value: `get_profile_default_workspace()` reads
`last_workspace.txt` BEFORE the configured key (api/workspace.py ~419-427), and
for the `default` profile that file is the GLOBAL one. `set_last_workspace()`
rewrites it on every `/api/chat/start` and `/api/session/update` of ANY
conversation. So "profile default wins over the current session" degrades in
practice to "whatever workspace another conversation last used wins over the
conversation I am in" -- observed live: the pointer flipped mid-operation
because a concurrent turn wrote to it.

New precedence:

    switchWs || S.session.workspace || S._profileDefaultWorkspace

Preserved invariants:
- the one-shot profile-switch flag still wins first and is still consumed (#823);
- a blank page (no loaded session) still falls back to the profile default,
  which remains required for the empty-composer workspace chip (#804/#5169);
- `_profileDefaultWorkspace` is never nulled by `newSession()` (#823).

This deliberately diverges from upstream #4755, which made the profile default
win. That decision assumed a stable configured default; on a shared/global
`last_workspace.txt` it inverts its own intent. The upstream test is rewritten
with the rationale inline so a future rebase does not silently restore the bug,
plus a new test that evaluates the real expression under node and proves the
blank-page fallback still holds.

Pre-existing failures in tests/test_workspace_stale_recovery.py (3) were
verified identical with and without this change.

* fix(workspace): recover stale inherited workspace

---------

Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
2026-08-20 16:36:39 -07:00
nesquena-hermesandn 1f5e45527a Release exp-v0.52.254: skip redundant sidecar rewrites during a turn (#7172, @ruizanthony) (#7175)
Co-authored-by: n <a@n>
2026-08-20 11:49:13 +00:00
ruizanthonyandAnthony Ruiz 0e5a0fadae perf(streaming): skip redundant sidecar rewrites during a turn (#7172)
The periodic checkpoint thread fires whenever a tool call completes and
unconditionally rewrites the whole session sidecar. A completed tool call
is not proof that anything the checkpoint persists actually changed:
`run_conversation()` mutates an internal copy of the transcript, and the
turn bookkeeping fields (`active_stream_id`, `pending_user_message`,
`pending_started_at`) are written once before the run starts. So the
recurring rewrite normally re-persists byte-identical state.

Each rewrite serializes the entire session while holding the GIL, so its
cost scales with transcript size. Because the HTTP server and the agent
workers share one interpreter, that cost is added latency for every
concurrent request — not just for the session being written.

Measured on this deployment (1,622 sidecars, 6.65 GB, median 2.31 MB,
28.8% above 5 MB, max 35.9 MB):

  sidecar    write    per 10-min turn (40 wakeups)
  1.8 MB    ~100 ms    4.1 s  ->  0.2 s
  24.7 MB   ~1.4 s    41.6 s  ->  2.1 s

A checkpoint now writes only when the state it persists actually moved,
compared through a cheap fingerprint that allocates nothing. The
fingerprint covers every field the checkpoint can legitimately advance
mid-run: session identity (compression rotates the SID), turn
bookkeeping, and the length of the persisted message projections.

Fail-closed on both sides: an unreadable fingerprint returns None and
forces the write, and a 5-minute idle refresh keeps `updated_at` from
going stale on a long quiet turn (the sidebar sorts on it). Crash
recovery is unchanged — any real mutation is still persisted within one
15 s interval.

Co-authored-by: Anthony Ruiz <ruizanthony@users.noreply.github.com>
2026-08-20 04:47:38 -07:00
nesquena-hermesandn 45659362e7 Release exp-v0.52.253: honor agent.clarify_timeout incl 0=unlimited (#7163, @BBaFSE) (#7171)
Co-authored-by: n <a@n>
2026-08-20 09:30:17 +00:00
BBaFSEandnesquena-hermes 0b5d2f3698 fix(clarify): honor agent.clarify_timeout and support unlimited wait (#7163)
The WebUI clarify bridge resolved the prompt timeout from a local, divergent
copy (legacy clarify.timeout, 120s fallback) and treated 0 as invalid, so
agent.clarify_timeout: 0 (unlimited) could not be honored and there was no
way to make a clarification card wait for the user.

Resolve the timeout with the agent core's canonical resolve_clarify_timeout()
(clarify.timeout -> agent.clarify_timeout -> 3600, <= 0 = unlimited) against
the session's own profile config (the streaming worker thread does not inherit
the request's per-profile context), wait with a null deadline when unlimited,
and stop the frontend rendering an expired countdown for non-positive timeouts.

- api/streaming.py: per-session profile config resolution, canonical resolver
  with a compatibility fallback, and an extracted _await_clarify_response()
  helper that polls in 1s slices and honours cancellation.
- api/clarify.py: preserve timeout_seconds <= 0 and emit no expires_at.
- static/messages.js: _clarifyExpiryMs() returns 0 for non-positive timeouts
  so the card stays up without a countdown.
- tests/test_sprint42.py: resolution matrix, per-session profile divergence,
  unlimited resolve-later / cancel-exit, and no-expiry payload.

Closes #7159

Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-08-20 09:28:23 +00:00