Commit Graph

35023 Commits

Author SHA1 Message Date
teknium1
acb2c45e35 fix(cron): self-removal excuses only a missing record; replacement records stay fail-closed
Follow-up to the salvaged #111044 commits:

- self_removal_delivery_allowed() now also requires that no record currently
  holds the job id. The marker alone said "this run removed its record"; it did
  not say the id is still empty. A replacement record (another owner reclaiming
  the id) must be treated as a stolen claim, not a self-removal.
- Drop the allow_self_removed kwarg on fire_claim_fence: the fence already has
  the job_id and the ContextVar marker, so it can decide on its own; the caller
  no longer threads a flag it computed from the same predicate.
- _FireOwnership.lost(): keep the explicit lost event and the no-owner short
  circuit ahead of the self-removal check so an interrupted run is still
  reported as lost even after it removed its record.
- _finish_completed_run: skip mark_job_run entirely for a self-removed job
  (nothing to mark) instead of calling it and then excusing the False.
- Tests trimmed to two invariants, both A/B'd against origin/main: the
  self-removing run delivers after a post-removal heartbeat tick (RED on main),
  and a self-removal followed by a replacement record is still discarded
  (GREEN on main, guards the new predicate).
- Docs: user-guide cron.md notes that a job may remove itself and still report.
2026-09-15 03:42:40 -07:00
KoNit-K
8b3059fc6e fix(cron): keep self-removed runs alive across post-removal heartbeats
A run that deletes its own job after the first heartbeat interval was still marked stale because the fire-claim loop treated a missing record as lost ownership before the self-removal marker could win.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 03:42:40 -07:00
KoNit-K
31b4001167 fix(cron): deliver completed self-removing runs 2026-09-15 03:42:40 -07:00
teknium1
ca5822b5a5 test(desktop): mark the loud scope note semantically instead of by class
Asserting the loud variant via the Tailwind `font-medium` class couples
the test to styling: any restyle of the accent breaks it without the
behaviour changing. Give the note role="status" (it announces which
profile edits land in) and a `data-scope-loud` flag for the non-default
variant, and assert those. Also fixes the padding-line lint warning in
settings-scope.test.ts.
2026-09-15 03:42:20 -07:00
teknium1
d85aaa3049 test(desktop): stub the new settings-scope atoms in the ConfigSettings test
config-settings.test.tsx mocks @/store/settings-scope with a hand-written
subset of exports; SettingsProfileScope now also reads
$settingsScopeProfile and $settingsScopeEditsNonDefault, so the partial
mock threw at render time in CI (JS & TS checks red).
2026-09-15 03:42:20 -07:00
teknium1
d7d9209795 fix(desktop): derive the loud scope note from the shared settings-scope store
Rewrite of the original component-local `editingNonDefault` computation
onto the existing scope architecture (owner's ask: "use the same
existing architecture" as the Capabilities/toolset scope handling).

- store/settings-scope.ts gains `$settingsScopeEditsNonDefault`, a
  computed over `$settingsScopeProfile` × `$profiles`, so "the settings
  pages are editing a non-default profile" is a store fact any surface
  can subscribe to, not a per-component recomputation.
- profile-scope.tsx reads `$settingsScopeProfile` for the selected chip
  (instead of re-deriving `override ?? active`) and the new selector for
  the loud/quiet note. User-visible outcome is unchanged: accented note
  for any non-default target, override or not; quiet note for an explicit
  override onto the default; nothing when following the active default.
- Review: an unloaded roster (no `is_default` entry yet) used to suppress
  the note exactly at the landing moment where a bot may already be the
  active profile. The selector now assumes the root profile's canonical
  key as the default, so an unknown default fails loud, not quiet. The
  four-cell truth table is documented at the JSX conditional.
- settings-scope.test.ts pins the selector across active-profile,
  override and unloaded-roster inputs; the component tests from the
  original PR are kept as-is and still pass.
2026-09-15 03:42:20 -07:00
Teknium
b047b5db4c fix(desktop): settings pages state loudly when they edit a non-default profile's config
After any Bot Mode chat the active gateway profile is the bot's, so the
settings scope silently followed it — edits landed in
profiles/<bot>/config.yaml with only a faint chip tint as the tell
(#89190/#89162/#89597 report class, live-repro'd: Max Agent Steps written
to scout's config). The applies-to note now renders for ANY non-default
target, override or not, accented; default-profile editing stays quiet.
2026-09-15 03:42:20 -07:00
teknium1
8b9df066e2 test(gateway): trim block-loop wording coverage to two invariants; CLI keeps the needs_input distinction
The salvaged parametrized set collapsed to one neutral case (transient) plus
the needs_input positive control. hermes kanban block now mirrors the
notifier: 'needs a human decision' only when the block was typed
needs_input, 'orchestration attention needed' otherwise.
2026-09-15 03:42:00 -07:00
KoNit-K
5c970d9745 fix(kanban): neutral block-loop wording on the CLI, Desktop toast, wake text and docs
The sibling surfaces of the gateway ping rendered the same false claim:
`hermes kanban block` said "needs a human decision", the Desktop toast title
said "needs a decision", the wake status line (locales/*.yaml
gateway.kanban.wake.block_loop_detected) said "needs a decision" and the
docs described the triage route as "for a human decision". A repeated-block
circuit breaker only establishes that orchestration attention is needed.

Surface sweep from PR #111131 (notifier/test hunks dropped in favour of the
typed-kind formatter from PR #111132).
2026-09-15 03:42:00 -07:00
KeyArgo
302334ff89 fix(gateway): neutral orchestration wording for block-loop triage pings
A repeated-block circuit breaker routes a task to triage and establishes that
orchestration attention is needed — it does not establish that a human
decision exists. The notifier unconditionally rendered every
block_loop_detected event as "needs a human decision", overclaiming owner
intent for dependency waits, capability gaps, and transient failures.

Branch on the typed block kind in the event payload: only needs_input (the
one kind carrying a concrete question for the owner) keeps the decision
wording; dependency/capability/transient/None get neutral orchestration
wording. TRIAGE visibility, the reason, and recurrence count are preserved.

Closes #111125
2026-09-15 03:42:00 -07:00
teknium1
5ba3a698d9 test: fold host spillover verification into the size-probe invariant
The PR added four test functions for one fix. The host-side short-write and
multibyte cases are the same invariant as the sandbox size probe (an archive
that is not byte-exact is never referenced to the model), so they become two
parametrized rows of test_size_probe_decides_lossless, which now also uses
multibyte content so every row pins the byte-vs-char comparison. Net new
tests for the fix: 2 functions.

Also names in _write_to_sandbox why the +1 tolerance is keyed on heredoc
mode only: the payload backend delivers stdin verbatim and is expected to be
byte-exact. The three bare write_text() calls the footgun scanner flags in
this file gain encoding= while it is being touched.
2026-09-15 03:41:43 -07:00
teknium1
fb2e623008 test: collapse sandbox size-probe cases into one parametrized invariant
Five near-identical MagicMock scripts pinned the same contract (byte-exact
or discard, heredoc gets exactly one extra byte); one parametrized test
plus the unprobeable-backend case cover it. Drop the host-side happy-path
test that duplicated test_multibyte_content_verified_by_byte_count.
Docstring now names the real heredoc wrapper
(BaseEnvironment._embed_stdin_heredoc) and notes the extra exec RTT per
oversized result (review point).
2026-09-15 03:41:43 -07:00
Teknium
fc93a1b55a Port from lobehub/lobehub#18258: verify persisted tool-result archives before referencing them
Oversized tool results are archived to disk and replaced in-context with a
'Full output saved to: <path>' reference. Until now the write was trusted
blind: a partially-flushed host file (ENOSPC/quota races) or a lossy sandbox
write (API-body truncation on payload backends) still produced the archive
reference, so the model was told the full result was recoverable when bytes
had silently vanished.

Both persistence paths now round-trip-verify size before building the
reference and fail closed to the bounded inline truncation otherwise:

- _write_to_spillover: byte-count check via os.stat after write; mismatched
  archives are deleted and the caller falls through to inline truncation.
- _write_to_sandbox: wc -c probe after the cat; heredoc-mode backends get a
  +1 byte tolerance (wrap_modal_stdin_heredoc appends one newline by
  construction), unprobeable backends stay best-effort success.

Regression tests fail without the fix (verified by stashing the source
change: 4 failed). E2E-verified against a temp HERMES_HOME with real file
I/O including multibyte content and a simulated short write.
2026-09-15 03:41:43 -07:00
teknium1
0468acdce6 fix(desktop): key voice-fields autosave on the profile scope string
Parents (profile-config.tsx) build the `profile` scope as a fresh object
literal on every render, so `useMemo(..., [profile])` produced a new
cache writer each time and the autosave effect, which depends on both,
tore down and re-armed its 550ms timer on every unrelated re-render.
Key both on profileScopeKey(profile) — the same string the query cache
already uses as the scope's identity — so only a real scope change
re-arms them.
2026-09-15 03:41:04 -07:00
teknium1
ab1ef0c88e test(desktop): pin that a scoped Capabilities TTS panel saves into its scope
Review on the PR: the load-bearing direction (profile B's scope
forwarded into saveHermesConfigRecord) was only covered by the live
E2E; the unit test asserted just the unscoped default. Renders the panel
with profile={profile:'scout', connectionId:'gw-2'} and asserts the
autosave carries exactly that scope. The @/hermes full-replacement mock
gains profileScopeKey, which use-config-record reaches when a scope is
present. Sabotage (drop the profile prop from <VoiceProviderFields>)
fails this test with `expected undefined to deeply equal {profile:
'scout', …}`.
2026-09-15 03:41:04 -07:00
Teknium
bdac5c8e2e fix(desktop): Capabilities TTS voice fields write the scoped profile's config, not the active one
ToolsetConfigPanel threads its profile scope into every fetch but rendered
VoiceProviderFields without it, and the fields were hard-wired unscoped —
configuring profile B's TTS from the Capabilities selector read and
autosaved profile A's whole config record. New capability-scoped
saveHermesConfigRecord (symmetric with getHermesConfigRecord), profile
prop threaded through, per-scope cache write-through. Unscoped callers
(Settings → Voice) unchanged.
2026-09-15 03:41:04 -07:00
teknium1
f9da9e8385 fix(web): lock the remaining off-loop config RMWs; keep model probes outside the lock
local_models._set_runtime_enabled (quickstart/activate/stop job threads) and
profiles._disable_unselected_skills ran load_config -> mutate -> save_config
without _CONFIG_MUTATION_LOCK, so the dashboard's debounced PUT /api/config
autosave could erase their writes exactly like the routers this PR already
fixed. Both spans now hold the lock.

POST /api/model/set held the global lock across switch_model's catalog
fetches / endpoint probes, stalling every other config writer for the
duration of a network round-trip. The main-slot validation is split into
_prepare_main_assignment (runs under the profile scope only) and the
load -> apply -> save half runs under the lock. The Nous entitlement refresh
(force_fresh) stays inside the write half: it must read the on-disk config
it mutates, and it is bounded by the portal timeout.
2026-09-15 03:40:26 -07:00
teknium1
131ea24026 test(web): deterministic lost-write race through TestClient; drop the source-text tripwire
The PR's race test slowed save_config and hoped for an interleave, and
called the six handlers as web_server attributes that no longer exist
(web_server.py is a facade over web_routers/). Its second test read
inspect.getsource() for the lock name — a change-detector on source text.

Replace both with two invariant tests that drive the real endpoints via
TestClient and FORCE the interleaving: writer 1 is held inside save_config
until writer 2 has run load_config (or the lock kept it out). Unlocked the
second save erases the first mutation; locked the writes serialize.
Verified: reverting hermes_cli/web_routers/ to main fails both tests
(KeyError 'providers' / 'aggregator' = the lost write), the fix passes.
2026-09-15 03:40:26 -07:00
Teknium
eaf0dd0970 fix(web): config RMW handlers hold _CONFIG_MUTATION_LOCK so concurrent saves stop dropping writes
Only PUT /api/config took the span lock; model.set, moa, custom-endpoint
create/activate/delete, memory-provider saves, and the profile-dir model
write ran load→mutate→save unlocked in worker threads. The desktop fires
these concurrently with its debounced autosave — whichever save landed
second silently dropped the other's mutation (#88913/#89184 lost-write
flavor). Race test with slowed save proves both writes now survive.
2026-09-15 03:40:26 -07:00
teknium1
5bccc4e238 fix(model_switch): clear key_env only when the route changes; drop it on custom activation
`model.key_env` is not custom-only: the Desktop settings UI stores REGISTRY
provider keys there (e.g. HERMES_CUSTOM_LMSTUDIO_API_KEY with provider
lmstudio, #106336) and auth._model_level_key_env honours it. The previous
predicate (`not custom or route_changed`) therefore wiped that pointer on a
same-provider same-base_url model re-pick and silently broke the user's
credential. The pointer now clears ONLY when provider or base_url changed;
the inline api_key/api rule is unchanged.

Custom-endpoint activation (model_setup_flows_custom) popped base_url /
api_key but never key_env, so a stale pointer from a previous endpoint
outranked the credential it had just written — pop it alongside.

Tests: the same-route re-pick case now covers a registry provider (red on
the old predicate), and the two clear_model_endpoint_credentials tests are
folded into one invariant.
2026-09-15 03:39:45 -07:00
teknium1
1987b5e461 fix(model_switch): provider switch clears model.key_env/api_key_env in the canonical persist shape
Main no longer clears credentials in web_server's _apply_main_model_assignment;
every /model surface (CLI, gateway, TUI, dashboard) persists through
model_selection_config_updates(), which only dropped api_key/api on a route
change. Custom-endpoint activation writes model.key_env with NO inline key, so
a pointer-only model block survived every provider switch and routed the new
provider's requests to the old endpoint's env var (the PR's Bug 5) — live
repro on main: activate custom_myep -> /api/model/set openrouter left
key_env: CUSTOM_MYEP_API_KEY on disk.

Port the PR's web_server hunk to the one shape function: key_env/api_key_env
clear under the same route-changed rule as api_key (same-route re-pick keeps
them). The dashboard's _resolve_assignment_credentials re-adds the TARGET
provider's own pointer after this, so custom->custom still ends with the new
endpoint's key_env. One invariant test in the one-shape suite.
2026-09-15 03:39:45 -07:00
Teknium
68ebb2c996 fix(config): provider switch clears the stale model.key_env pointer
Custom-endpoint activation writes model.key_env, but
clear_model_endpoint_credentials never popped it and the switch-clears
trigger only fired on inline keys — a pointer-only model block survived
every provider switch, routing the new provider's requests to the old
endpoint's env var. key_env/api_key_env now clear under clear_api_key;
the trigger fires on pointer-only blocks too. All key_env writers set it
after the clear, so custom-to-custom switches keep the new pointer.
2026-09-15 03:39:45 -07:00
teknium1
88f2844d46 fix(agent): cover the remaining refusal-only surfaces and fold the tests
- codex_runtime._CODEX_PROGRESS_DELTA_TYPES gains response.refusal.delta so the
  stream watchdog sees progress on a refusal-only stream instead of timing it
  out as idle.
- auxiliary_client._parse_codex_final_response reads type=refusal content
  parts; without it an aux refusal-only turn parsed to content=None and hit the
  empty-response path the main loop was just taught to avoid.
- tests: parametrize test_streamed_refusal_accumulated (refusal-only /
  alongside-content) so there is one test per surface; drop upstream product
  references from docstrings (credit stays in the PR body); pass encoding= to
  the read_text calls flagged by the Windows footgun scanner.
- docs: fallback-providers notes that a streamed refusal is a terminal
  content_filter result, not an empty response to retry.
2026-09-15 03:39:07 -07:00
Hermes Agent
e27f16365d fix(agent): preserve streamed refusals as text (port of anomalyco/opencode#43343)
A model that declines mid-stream delivers the explanation on the
structured refusal channel (chat_completions delta.refusal; Responses
response.refusal.delta / refusal content parts) and leaves content
empty. The streaming accumulators dropped that channel entirely, so a
streamed refusal assembled into an empty message and fell into the
empty/invalid-response retry loops - burning paid retries reproducing a
deterministic refusal - while the non-streaming path had already fixed
this class in #46013.

- chat_completions streaming: accumulate delta.refusal (incl.
  model_extra), expose message.refusal on the assembled mock response so
  ChatCompletionsTransport.normalize_response applies the existing
  sole-payload -> content_filter promotion; count refusal deltas in the
  zero-chunk guard; carry refusal in the Relay final-response dict.
- Codex Responses stream consumer: collect response.refusal.delta as
  answer text so a refusal-only stream no longer raises 'did not emit a
  terminal response' with zero usable content.
- Responses normalizer: read type=refusal content parts in
  _extract_responses_message_text (attr and dict shapes).

Sabotage-verified: each new test fails with its wiring line disabled.
E2E: refusal-only stream -> terminal content_filter with explanation;
refusal-alongside-content stays a normal usable turn; plain-text
streams unchanged.
2026-09-15 03:39:07 -07:00
teknium1
021ab58a23 fix: hindsight update-time dep resolution survives a BOM in config.json
`_provider_pip_dependencies` still read ~/.hermes/hindsight/config.json with
strict utf-8 inside a bare `except Exception`, so a Windows-editor BOM made the
`mode` lookup silently fail and `hermes update` reinstalled only
`hindsight-client`, leaving the embedded daemon broken — the exact #70636
symptom this helper exists to prevent. Route it through the shared
`read_json_or_empty` (utf-8-sig, {} on missing/corrupt) like the other memory
readers in this PR, and add the reader to the parametrized BOM invariant.
2026-09-15 03:38:29 -07:00
teknium1
c97e36db3f test: one parametrized BOM invariant over the shared reader + 2 direct readers
mem0, hindsight and the honcho CLI all read through utils.read_json_or_empty,
so the seven per-loader tests collapse to one parametrized invariant plus the
Qwen creds case. The per-loader fix landed in the shared reader (one site, not
six), which is the salvage bar: <=2 invariant tests, no change-detectors.
2026-09-15 03:38:29 -07:00
Teknium
5705b68f70 fix: memory-plugin and Qwen-CLI config JSON survives Windows BOM
Port from earendil-works/pi#8337 (UTF-8 BOM normalization in text inputs):
sibling sites the merged #81967 BOM sweep missed. json.loads hard-fails on
a leading U+FEFF and every one of these loaders swallows the exception and
silently falls back to defaults — a user who edited mem0.json, honcho.json,
hindsight/config.json, or supermemory.json in Notepad lost their whole
config with no error, and Qwen CLI OAuth creds saved with a BOM raised
qwen_auth_read_failed.

- plugins/memory/{honcho,mem0,hindsight,supermemory}: 13 read sites -> utf-8-sig
- hermes_cli/auth.py: _read_qwen_cli_tokens -> utf-8-sig
- tests: BOM regression tests per loader (sabotage-proven) + plain-UTF-8 guard
2026-09-15 03:38:29 -07:00
teknium1
e860b8e4e4 fix(context): compute-host /context and session.context_breakdown carry the per-file manifest; report blocked files
Why: the tui_gateway live formatter (`_format_live_context_output`, used when
the session runs on a compute host) renders its own summary and never got the
"Context files" block, and `session.context_breakdown` had no structured rows,
so Desktop's popover could not show them. The formatter now appends
render_context_file_lines() with the session cwd bound (the RPC thread has no
session context, so the discovery walk would key on the backend's cwd), and
the RPC payload gains a `context_files` list (contract + generated TS/OpenRPC
+ Desktop type). The docs sentence is scoped to the surfaces that render it.

A file whose content _scan_context_content replaces with a BLOCKED marker was
reported "loaded"; the manifest now runs the same scan and reports `blocked`.
The module docstring names the frontmatter-strip / chain-cap approximations
and drops the product-name attribution (credit stays in the PR body).
2026-09-15 03:37:49 -07:00
teknium1
f271ba09b0 fix(context): derive the /context file listing from the builder's own discovery walk
Review follow-up (Enough1122) on the salvaged #91272: the original
list_context_file_sources() hand-mirrored the priority ladder inside
build_context_files_prompt, so the two would drift the moment the builder
gained a context type or changed precedence — misreporting what the prompt
holds is worse than not showing it.

Now prompt_builder exposes one candidate finder per context type
(_CONTEXT_FILE_CANDIDATES → discover_context_files) and BOTH the loaders and
the manifest walk it. The manifest lives in the new sibling
agent/context_file_sources.py (not appended to the facade) and:
- reports empty / unreadable files truthfully instead of "✓ 0 tokens",
- mirrors the install-tree guard ("suppressed") so a Desktop session that
  fell back into the Hermes tree sees why nothing loaded,
- lists every .cursor/rules/*.mdc as loaded, matching the builder which
  concatenates all of them,
- measures truncation on the rendered "## label" section like the builder.

The block now renders on every surface that shows the /context category
table: CLI/TUI (hermes_cli/cli_info_mixin.py) and the messaging gateway
(gateway/slash_commands_status.py). The Desktop popover consumes the raw
session.context_breakdown payload (no text table) and is left as-is.

Tests trimmed to the two invariants: manifest/prompt parity across every
context type at once, and truncated/suppressed follow the builder.
2026-09-15 03:37:49 -07:00
Teknium
5349aa609d Inspired by Copilot CLI: /context now lists each context file with load status and token cost
Copilot CLI 1.0.81-6 shows each user instruction file separately in
/instructions. Hermes loaded AGENTS.md/.hermes.md/CLAUDE.md/.cursorrules/
SOUL.md through a priority ladder but gave the user no visibility into
WHICH files were discovered, which one won, which were shadowed, or how
much context each costs — the /context 'rules' category was one opaque
number.

- agent/prompt_builder.py: list_context_file_sources() — read-only
  manifest mirroring build_context_files_prompt discovery (priority
  ladder, AGENTS.md directory chain with AGENTS.override.md precedence,
  cwd-only CLAUDE.md/.cursorrules, SOUL.md from profile home) with
  per-file chars, est_tokens, and loaded/truncated/shadowed status
- cli.py /context: 'Context files' section rendering the manifest with
  status glyphs and shadowing/truncation notes; zero prompt/cache impact
- docs: reference/slash-commands.md /context row
- tests/agent/test_context_file_sources.py: 11 tests incl. E2E parity
  with build_context_files_prompt shadowing
2026-09-15 03:37:49 -07:00
kshitijk4poor
e112da7578 docs(desktop): preflight comments describe the OAuth-only branch; scope assertion via objectContaining 2026-09-15 16:07:43 +05:30
kshitijk4poor
2821cb2d8c refactor(desktop): one profile-order sort for the active strip and the at-rest groups
sortByProfileOrder moves to a pure lib module with a key selector so
buildRestGroups sorts its named squares directly, replacing the collator
sort that the component then re-sorted with a different comparator. The
fleet rail test no longer needs importOriginal (and three store mocks) to
reach the helper.
2026-09-15 16:07:43 +05:30
kshitijk4poor
8751b3edd4 test(desktop): keep the unscoped getProfiles invariant separate from the scoped one 2026-09-15 16:07:43 +05:30
kshitijk4poor
9bb985c5d4 fix(desktop): OAuth REST preflight gets its own dial budget
Sharing the remainder of SWITCH_DIAL_TIMEOUT_MS meant a slow-but-successful
socket dial left the preflight 0 ms and the switch failed as "Timed out
connecting" although the socket had just opened.
2026-09-15 16:07:43 +05:30
kshitijk4poor
b591df7c42 fix(desktop): at-rest rails read the roster only; active gateway keeps its single render path
The renderer's per-connection list no longer feeds buildRestGroups: with no
roster reconciler it could outlive a profile deleted elsewhere. The active
gateway is never rendered through FleetRestGroup — that path routed its own
squares through selectConnection (full dial + wipe) instead of selectProfile's
live swap. What survives from the original change is the order parity: at-rest
named squares follow $profileOrder like the active strip.
2026-09-15 16:07:43 +05:30
kshitijk4poor
256edfc1f5 fix(desktop): profile-list ownership follows the published source, without a roster reconciler
The per-connection list cache is kept only to repaint $profiles on re-home.
The $fleetRoster listener is dropped: a roster landing while the active
source's own /api/profiles read was in flight invalidated that read, so
$profiles stayed empty/stale after every switch or focus refresh that the
roster IPC won. Both are reads of the same backend; neither is "older".

A null descriptor is a reconnect blip (setConnection's contract) and keeps
the current owner instead of blanking the rail; the first published
descriptor adopts whatever list is already loaded. Legacy sources are keyed
by endpoint rather than a JSON tuple. Tests cover the roster race and the
null blip; the two use-session-actions tests now publish the descriptor
before seeding $profiles, matching the runtime order.
2026-09-15 16:07:43 +05:30
Zeus-Deus
5b49051276 fix(desktop): keep profile switches on the selected gateway 2026-09-15 16:07:43 +05:30
kshitijk4poor
6cd9fe1d5b chore: map Zeus-Deus contributor email (salvage #105139) 2026-09-15 16:07:43 +05:30
teknium1
d8053f4806 fix(video_gen): cap LTX 2.5 at 10s for 1440p/2160p; omit unset enum durations
fal's LTX 2.5 fast endpoints accept 6-20s only up to 1080p — "At 1440p and
2160p, all frame rates support up to 10 seconds" — so a 4K request with the
family's 20s ceiling was rejected by the vendor. Families can now declare
`duration_cap_by_resolution`, applied after the enum snap / range clamp on the
resolved resolution enum.

An unset duration on a duration_enum family also snapped to enum[0] (6s),
silently overriding the endpoint's own "auto" default; None now omits the key
for enum families exactly as it already did for range families.

test_managed_media_gateways asserts the alibaba/happy-horse/ namespace by
prefix rather than the exact v1.1 literal so the next version bump doesn't
flip an unrelated gateway test.
2026-09-15 03:37:11 -07:00
teknium1
1c1980dc1f fix(video_gen): make the duration enum explicit instead of sniffing tuple shape
`durations` carried two meanings told apart only by len==2 and gap>1: a
(min, max) range to clamp, or an enum to snap. A family with exactly two
legal values would have been misread as a range (review finding on #91311).
`durations` is now always the (min, max) window (what capabilities()/
list_models() read) and families with discrete values add `duration_enum`;
_clamp_duration takes the family and branches on the key, not the shape.

Also: restore the exact v1.1 endpoint assertion in the gateway namespace
test (a startswith/endswith check would not catch a silent version drift),
add the ltx-2.5 i2v snap case, and keep happy-horse on audio_native (the
schema test forbids audio+audio_native together, and v1.1 audio is always on).
2026-09-15 03:37:11 -07:00
Teknium
b30df1303c test: pin happy-horse namespace invariant, not exact 1.0 endpoint strings
The managed-gateway test asserted the literal v1.0 endpoint ids; its
stated purpose is verifying the alibaba/ (not fal-ai/) namespace. Assert
prefix+modality-suffix instead so version bumps don't break it.
2026-09-15 03:37:11 -07:00
Teknium
37286d3064 feat(video_gen): LTX 2.5 + Kling O3 families; Happy Horse upgraded to v1.1
Adds two new FAL video families and upgrades one:

- ltx-2.5 (cheap tier): lightricks/ltx-2.5/{text,image}-to-video/fast.
  Lightricks' open-source audio-video model. Native audio, 6-20s integer
  duration enum, 720p-2160p (i2v), $0.09/s at 720p. duration_int + 2k/4k
  resolution aliases; no seed key in the schema.
- kling-o3 (premium tier): fal-ai/kling-video/o3/standard/{text,image}-to-video.
  Kuaishou's frontier multi-shot model, 3-15s, optional native audio
  ($0.084/s off, $0.112/s on). String durations, i2v drops aspect_ratio,
  no seed/resolution keys.
- happy-horse upgraded from the sparse-docs 1.0 endpoints to
  alibaba/happy-horse/v1.1/{text,image}-to-video with the full published
  schema: nine aspect ratios, 720p/1080p, 3-15s integer durations, seed
  supported, audio native (no generate_audio key), i2v drops aspect_ratio.

All flags derived from each endpoint's llms.txt schema. Payload builder
asserted locally against the schemas; test for the old Happy Horse
"prompt-only" contract updated to pin the v1.1 schema, plus new payload
tests for ltx-2.5 and kling-o3.
2026-09-15 03:37:11 -07:00
teknium1
bdc7916196 fix(ux): plain-language, actionable user-facing messages (dashboard)
Squashed integration of the user-facing message audit for this surface set.
Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
2026-09-15 03:36:22 -07:00
teknium1
754a9ef4e7 test(stt): give the silent-stall test the same idle budget
test_silent_stall_still_times_out kept the 0.1s idle window and flaked
once locally under heavy load (load avg ~250): the child was killed
before its single stderr line was read, so the pre-stall-output
assertion saw an empty stderr. Same class as the progress test this PR
de-flakes; give it the same 0.25s budget. The 30s sleep still trips the
window, so the must-time-out direction is unchanged.
2026-09-15 03:36:11 -07:00
teknium1
7ee9fa52c0 test(stt): correct the de-flake provenance in the idle-timeout comment
andrexibiza's review is right: the base test already printed tick 0
before its first sleep, so this change never removed a
sleep-before-first-tick race. The actual de-flake is the larger idle
budget (0.1s -> 0.25s) plus a longer heartbeat sequence whose ~400ms
runtime still exceeds the idle window. Say exactly that so the causal
record is accurate.
2026-09-15 03:36:11 -07:00
andrexibiza
a70ea873cf test(stt): stabilize idle-timeout progress test against spawn latency
The stderr-progress idle-timeout test used a 0.1s idle window with 0.04s
ticks — shorter than Windows process spawn, so the first chunk could never
arrive in time (deterministic failure on Windows, flake under Linux CI
load). Verified failing identically on pristine main before the change.

Fix: emit the first tick immediately, tick every 50ms for ~400ms total,
250ms idle window (5x tick period). The pass still depends on the progress
extension while tolerating real spawn/scheduling latency.

Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
2026-09-15 03:36:11 -07:00
teknium1
b00ebf6210 fix(skills): live-dashboard credits the human author and drops the product-name intro
Authoring standard 4 requires the human first in `author`; the skill was
drafted with Hermes so the tool was credited instead. The intro cited a
third-party product, which is allowed only in LICENSE/credit lines. Also
adds metadata.hermes.category and lowercases tags to match sibling
skills; docs page regenerated for this skill only.

Tests: the two prose tests asserted sentence literals (change-detectors);
they now assert structure — three Procedure phases, a "Done when" per step,
standard headings, and the tool wiring (cronjob/desktop_preview/[SILENT]).
2026-09-15 03:35:33 -07:00
teknium1
788601358d refactor(skills): live-dashboard becomes an optional skill reconfigured for the Desktop app
Why: the web dashboard is being deprecated in favour of the Electron Desktop
app, and a skill that ships a bespoke cron blueprint should not be bundled by
default. Reconfigure instead of just rebasing:

- Move skills/productivity/live-dashboard -> optional-skills/productivity/
  live-dashboard (install with `hermes skills install
  official/productivity/live-dashboard`); register it like every other
  optional skill: per-skill docs page under user-guide/skills/optional/,
  optional-skills-catalog row, sidebars entry.
- Desktop reality: add a "Show the dashboard" step — when `desktop_preview`
  is in the toolset (Desktop/GUI sessions) render index.html in the in-app
  preview pane after every build/tick and on request; otherwise report the
  absolute file path. Prerequisites section added (Enough1122 review).
- Drop the hard-wired cron/blueprint_catalog.py entry: the curated catalog
  is for bundled skills and would preload a skill that may not be
  installed. Use the skills-pipeline blueprint instead —
  `metadata.hermes.blueprint` on the SKILL.md registers a daily
  all-dashboards sweep as a /suggestions entry at install time (opt-in,
  never auto-scheduled), which is exactly the mechanism main provides for
  optional skills.
- Never hardcode ~/.hermes in prose the agent executes: refer to the Hermes
  home directory's dashboards/<slug>/ and write absolute paths into cron
  prompts (also answers the review's "tick prompt must name the state-file
  path" point).
- Tests follow the skill to optional-skills/, the catalog-blueprint tests
  are replaced by one parse_blueprint/blueprint_to_job_spec invariant and
  one desktop_preview-with-path-fallback invariant.
2026-09-15 03:35:33 -07:00
Teknium
2154458685 Inspired by Energy: one-sentence live dashboards — bundled skill + automation blueprint
Energy (getenergy.com) ships natural-language persistent dashboards:
describe what you want to see in one sentence and the agent builds a
self-updating status page fed by email threads, signed-in websites, and
files. This ports the concept onto Hermes's existing cron + connector
architecture:

- skills/productivity/live-dashboard: setup/tick split skill — pin the
  dashboard contract, verify one live read per source before scheduling,
  keep dashboard.json as source of truth with a self-contained HTML
  projection, stale-read discipline, deliver only on material change.
- cron/blueprint_catalog.py: live-dashboard automation blueprint
  (purpose/sources/time/recurrence/deliver slots) rendering to the
  dashboard form, /blueprint command, and hermes:// deep-link.
- tests/skills/test_live_dashboard_skill.py: skill standards + blueprint
  registration + real fill_blueprint E2E.
- docs: per-skill page, skills catalog row, sidebar entry.
2026-09-15 03:35:33 -07:00
teknium1
59c20a51aa test: drop product name from inbox-triage test docstring
Competitor product names are allowed only in LICENSE attribution and
salvage credit lines; the test docstring had an inspiration tag left over
from the port.
2026-09-15 03:34:55 -07:00