Commit Graph

49272 Commits

Author SHA1 Message Date
Teknium
ba03113d96 fix(update): read update.log and receipts from the root home
The writers moved to the root home; the readers did not. `hermes logs update` /
`handoff`, `hermes logs list`, the debug bundle, the dashboard's durable
update-completion marker, the post-swap hand-off file and pm's sync receipts
(same update_receipts dir) now use get_default_hermes_root(), so a sticky
profile no longer hides the update log.
2026-10-06 03:44:49 -07:00
Teknium
c035f2a614 fix(update): keep the unsaved manual-serve restart warning while an update runs
The durable running latest.json carries the previous receipt's manual-serve rows
as `carried_manual_serves` (no plan yet), and the startup warning read only
plan.runtimes + pending_manual_serves: the warning vanished during a run and
after a run killed before its plan stage. Read carried rows too, and carry a
killed run's rows forward into the next run.
2026-10-06 03:44:49 -07:00
Teknium
8e59e0ec20 fix(update): a dependency sync that fails after the tree moved is a follow-up (A6); Ctrl-C says the code moved
The completion bootstrap's PM step (ensure_tools_for_sync / pm.sync_venv) runs
after the checkout moved. Its failure ended in exit 1 with no receipt, which the
Desktop reads as "still on the previous version" while the tree is new.

- update_completion._bootstrap: a failure there is now settled as a post-commit
  follow-up: the tail obligation stays armed, the run's receipt is a success
  naming a `dependencies` follow-up ("dependencies not installed yet — the next
  launch retries"), exit 0 (.update_exit_code 0 in gateway mode). A prepared
  child that dies without a result settles the same way (`completion`).
- The parent resumes paused Windows gateways itself when the bootstrap could
  not (it has no dependencies to do so).
- Ctrl-C after the commit point (parent in run_completion, or the child) closes
  the run as `interrupted` (never `failed`), exit 130, and prints that the new
  code is in place and its remaining steps are owed.
- TAIL_FOLLOWUPS gains `dependencies`.

Restores the dropped zip-completion "preparation" case with the new expectation.
2026-10-06 03:44:49 -07:00
Teknium
f82c091019 refactor(update): move post-restart fleet verification into update_cmd_fleet_verify
update_cmd_fleet.py had grown past the 2,000-line cap (2,230). The verify phase
(_verify_fleet_after_update, the settle-polled fleet snapshot, owed-inventory
recording, legacy-unit warning) is its own topic: it runs after the restart
phase and decides follow-up vs clear. It now lives in a sibling; restart-phase
helpers it uses are read through update_cmd_fleet so their test patches still
apply. update_cmd re-imports _verify_fleet_after_update from the new module.
Tests retarget their patches to where production reads. No behaviour change.
2026-10-06 03:44:49 -07:00
Teknium
045615ac5a fix(update): after the commit point nothing fails hermes update (C3) 2026-10-06 03:44:49 -07:00
kshitijk4poor
22fcbee492 test(e2e): the fixture's shared-metrics answer carries offer_version, so interactive suites stop on no prompt
Since #133571 (b5025017d5, teknium1, 2026-10-05) a shared-metrics "No thanks" without
`offer_version` reads as `reask` and the interactive CLI/TUI offers the prompt once more. The
fake-provider home every e2e suite builds (`write_hermes_home`) wrote exactly that shape
(`enabled: false` only), so every PTY/tmux scenario now opens on the consent picker, never
reaches its ready status bar and times out: 61 failures in the e2e lane, the Windows unicode
CLI test, and the e2e-upgrade interactive-CLI entrypoint on main since the merge.

The fixture now records a complete current answer (`send: false`, `offer_version` imported
from the consent module so the next version bump cannot strand it again).
2026-10-06 03:29:28 -07:00
Teknium
32859234d4 fix(tools): light the Nous row when one web capability is pinned to the gateway
The managed web row answered from the shared web.backend only, so
extract_backend: nous (Desktop "Use for Extract" on Nous Subscription)
left it dark. It now reads the same per-capability resolution as the
vendor rows (incl. legacy use_gateway), shared in _web_serving_backends.
Tests folded into two invariants.
2026-10-06 03:15:35 -07:00
kokhlo
ceb35cc99b fix(tools): let a per-capability override shadow the shared web backend key 2026-10-06 03:15:35 -07:00
kokhlo
6b6976e7cd fix(tools): honor per-capability web backend keys when highlighting picker rows 2026-10-06 03:15:35 -07:00
hermes-seaeye[bot]
6cf3104d74 fmt(js): npm run fix on merge (#133780)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-06 10:06:29 +00:00
Teknium
2eeaeff0ed refactor(web): keep the gateway picker inside the file-size caps
Label the gateway route with the managed row's own name instead of new
i18n keys, and move the route invariant test into its own module.
2026-10-06 02:53:56 -07:00
Teknium
05e8ee35a9 test(web): firecrawl client stub takes the capability argument 2026-10-06 02:53:56 -07:00
Teknium
3861f1da62 fix(web): pick own key or Nous Tool Gateway per web capability in Desktop
Desktop's web picker had no way to send one capability through the Nous
Tool Gateway while the other used your own key. "Use for Search/Extract"
on the Nous Subscription row wrote the row's vendor string (firecrawl)
into web.<cap>_backend, which the runtime reads as BYOK Firecrawl: the
gateway was never used, and the managed and BYOK rows lit up together.

- The managed per-capability pick now stores `nous`; search/extract
  dispatch, the registry and the Firecrawl client resolve the route per
  capability (search: managed Perplexity, extract: managed Firecrawl).
- A whole-toolset Nous pick clears stale per-capability pins (they
  outranked the new selection).
- /api/tools/toolsets/web/config reports search_via_nous /
  extract_via_nous and each row's managed_nous_feature; the panel lights
  the right row, labels the route, and refetches after a pick instead of
  mirroring the vendor string.

Fixes #100513. Builds on the approach in #100539 by @liuhao1024 and the
self-healing pin clear from #122676 by @praggybuilds.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Murphy (praggybuilds) <praggybuilds@users.noreply.github.com>
2026-10-06 02:53:56 -07:00
Teknium
d1a7aa7c14 fix: date a macOS backend's start in UTC so the DST-end hour cannot misdate it
processStartTime read `ps -o lstart` in local time and rebuilt it with a
hand-written month table. In the hour that repeats when DST ends, local
01:30 names two instants and the reconstruction picked the first, putting a
backend started in the second 3600 s off its ledger create_time, so a valid
host backend was rejected. ps now renders lstart with TZ=UTC0 and the value
is parsed as GMT with Date.parse (V8 accepts the exact `ps` format,
including the double-spaced day), which also drops the month table.
2026-10-06 02:53:32 -07:00
Teknium
e4f862b7ca fix: grant the app-report waiver only to a listener proven to be the app's child
appVouchesForBackend tested `ownership !== 'host-ledger'`, so a backend
object that never had its ownership established (the field was optional;
untyped .mjs callers can build one) received the ancestry waiver. The
waiver's premise is ancestry, so it must be stated: localBackendProcess now
returns a BackendProcess whose `ownership` is required, and the waiver
requires `=== 'child'`.
2026-10-06 02:53:32 -07:00
Teknium
5ee8948154 refactor(desktop-smoke): extract assertResolvedIdentity from runInstalledDesktopSmoke
Moves the source-root and predicted-home identity assertions verbatim
into a helper called at the same point with the same inputs; behaviour
unchanged. Brings runInstalledDesktopSmoke's CC back under its main cap
(27 -> 21).
2026-10-06 02:53:32 -07:00
Teknium
eda0fcaaa0 refactor(desktop-smoke): extract appVouchesForBackend from assertBackendOrigin
Moves the app-ancestry evidence predicate (process evidence unreadable,
not a ledger-attached host backend, app reported the same tree) and its
comment into a named helper. Same expression, same short-circuit order,
evaluated at the same point; behaviour unchanged. Brings
assertBackendOrigin's CC back under its main cap (24 -> 19).
2026-10-06 02:53:32 -07:00
Teknium
1946a105b2 test(desktop-smoke): bind ledger-attached backends to their own evidence
A host-ledger backend is not an Electron descendant, so the app-reported
root no longer stands in for its tree when process evidence is unreadable
(Windows). Ledger records must match the live process's start time
(create_time, 2 s like the product), and the ledger is read at the machine
root the app resolves for the home, where the product writes it.
2026-10-06 02:53:32 -07:00
Teknium
c74c3b73b8 build(desktop): list backend-discovery in the e2e typecheck project
The smoke observer now imports parseSpawnLedger from
electron/backend-discovery.ts. tsconfig.e2e.json is composite and excludes
electron/, so tsc failed with TS6307. List the one module explicitly via
"files", which exclude does not filter.
2026-10-06 02:53:32 -07:00
Teknium
047f8aa46f style: format desktop smoke ownership regression 2026-10-06 02:53:32 -07:00
Teknium
35d0cb86e0 test: recognize shared host backends in desktop update smoke 2026-10-06 02:53:32 -07:00
Teknium
4981c6f006 fix(cli): Ctrl+D leaves a curses menu once raw mode is on, like Esc
Before a menu switches the tty to raw mode, Ctrl+D is EOF and aborts it;
after, it arrives as byte 4 (or a CSI-u / modifyOtherKeys Ctrl+d), which
_decode_menu_key mapped to nothing, so the menu just redrew. Whether Ctrl+D
left a menu depended on timing.

The shared-metrics offer (#133571) made that visible: it is drawn before an
interactive chat on any profile that has not answered, and
test_entrypoint_in_a_fresh_process[module-interactive-cli] (which ends the CLI
with EOF) hung for its full 300 s whenever raw mode won the race.

Ctrl+D now decodes to NAV_CANCEL in all three encodings, the same as Esc.

Live (real `python -m hermes_cli.main` on a pty, fresh home, Ctrl+D sent 3 s
after the offer drew): main redrew the offer 20 times and never exited; fixed,
the offer is left at once and the CLI exits rc=0 after 4 presses.
2026-10-06 02:53:00 -07:00
kshitijk4poor
93cbf617c7 revert: back out #133714 (merged accidentally; still under review)
PR #133714 ("test: pin each provider's wire, real error bodies, and the
prompt-cache prefix contract") was merged before review and discussion
finished. This reverts both of its commits so main is back to the state
before it landed:

- 74306c1d1b test: the skill scaffold prefix cell is a plain regression test now that #133715 landed
- 560e53638c test: pin the wire every provider receives, real error bodies, and the prompt-cache prefix contract

Test-only revert: removes the three test files and the wire snapshot
fixture; no production code is touched. Discussion continues on #133714.
2026-10-06 14:45:29 +05:30
kshitijk4poor
74306c1d1b test: the skill scaffold prefix cell is a plain regression test now that #133715 landed
The fix merged as 90194c64ff, so the run-time xfail for #81867 would only ever
pass. Drop the known_failure wrapper and reword the module docstring from
"gap" to "regression". The cell is red with the pre-fix agent/prompt_caching.py
(messages[1] goes from parts to a plain string) and green on main.
2026-10-06 14:22:56 +05:30
kshitijk4poor
560e53638c test: pin the wire every provider receives, real error bodies, and the prompt-cache prefix contract
Three regression nets, each built from production evidence:

- tests/agent/transports/test_provider_wire_snapshot.py + tests/fixtures/provider_wire_snapshot.json:
  the request every registered provider is sent (51 providers x reasoning unset/high/off), built
  through the production assembly path (build_api_request: cache decoration, sanitization,
  transport kwargs, Codex preflight, middleware). A shared-code edit that changes what any
  provider receives fails naming the provider and the first diverging key; a new provider fails
  until its wire is recorded. Network and DNS are blocked so capability probes (Ollama
  /api/show, LM Studio, catalogs) record their offline answer, not a live server's.
  Mutation check: +1 on the shared timeout fails 38 providers.

- tests/agent/test_error_classifier_observed.py: error bodies Hermes actually received (relay
  traces and agent.log, scrubbed), raised as the real openai/httpx exception types. Two known
  gaps are run-time xfails that flip to passing when their fix lands: #49769 (OpenRouter
  "can only afford N" 402 classified as terminal billing) and #85005 ("not a multimodal model"
  not recognised as an image rejection).

- tests/agent/test_prompt_cache_prefix_contract.py: a real AIAgent on a caching route against
  the recording fake provider; every request must re-send the previous request's content and
  only append. Pins a live cache bust as a run-time xfail: the #81867 skill-scaffold split is
  applied only while the skill message is inside the last-N marker window, so request 3 of a
  skill session re-sends the scaffold as a plain string and re-writes it (~20k tokens on call 2
  of skill-invoked sessions in agent.log).
2026-10-06 14:22:56 +05:30
kshitijk4poor
90194c64ff refactor(cache): one scaffold-split builder and one prior-decoration strip pass
Simplify pass over the skill-scaffold split fix:

- _scaffold_parts builds the [scaffold, tail] parts for both the marked split
  (_apply_cache_marker) and the unmarked one (_split_unmarked_scaffolds). The
  stripper only recognises the unmarked split while both stay byte-identical,
  so they now come from one place.
- _strip_prior_decoration owns the "strip every list-content message before
  re-planning" gate. build_prompt_cache_plan no longer strips and then hands
  the result to apply_anthropic_cache_control, which stripped it again.
- strip_anthropic_cache_control checks the registry only for plain-text
  two-part user messages, on the text it already joins for the flatten. The
  old check also ran for non-text parts, where a lookup miss compared
  None == None.
- apply_anthropic_cache_control docstring: drop the duplicated "Idempotent"
  paragraph and the claim that only marked messages pay the copy cost (false
  since the gate widened).
- Tests: cover the direct native tool-cache layout, which plans without
  apply_anthropic_cache_control (red with its _split_unmarked_scaffolds call
  removed). The re-plan test no longer depends on the marker window size.
2026-10-06 14:01:25 +05:30
Teknium
a0e066d614 review-fix(cache): re-planning an unmarked skill split marks the scaffold, not the tail
apply_anthropic_cache_control only flattened rows that still carried a marker. After
#133715 a skill message outside the marker window goes out as an UNMARKED
[scaffold, tail] split, so a second pass over the planner's own output (MoA reference
trim, failover re-decoration) saw a plain two-part list and put the new breakpoint on
the volatile tail. Use the same list-shape gate build_prompt_cache_plan already has,
so the re-plan equals a fresh plan from the canonical string.

Test is red on the PR head, green with the fix.
2026-10-06 14:01:25 +05:30
kshitijk4poor
e56c8d6ddb fix(cache): skill turns no longer re-write their whole scaffold after the first tool round
A skill-invoked turn's user message is sent as [scaffold, tail] text parts so the scaffold can
carry a cache breakpoint (#81867). The split only happened inside _apply_cache_marker, i.e. only
while the message held one of the last-N markers. After the first tool round pushes it out of
the window, the same message went out as one plain string — different content to the provider,
so request 3 re-wrote the entire skill body. agent.log shows it on call 2 of skill-invoked
sessions: 6 of 8 sessions re-wrote ~20k tokens there, versus 1 of 147 plain sessions.

Every registered scaffold message is now sent split, marked or not, so its content is stable for
the whole session. The failover stripper flattens the unmarked split too (registry lookup, since
an unmarked two-part user message is otherwise indistinguishable from an organic one).

Introduced by 214f2b82db (perf(cache): split skill turns at a builder-declared stable/volatile
boundary, joaomarcos, 2026-08-08, landed via #82423).

Tests: one round-trip cell outside the marker window pins identical parts (red on
origin/main) and the stripper flattening the unmarked split (red with the
stripper clause removed).
2026-10-06 14:01:25 +05:30
kshitijk4poor
61844a56f2 fix(tui_gateway): a second session.compress during a manual /compress says to wait
_manual_compress_turn's CompressionBusy is the only busy gate on
session.compress (its pre-check was removed), and it still built the
"Hermes is still replying. Stop..." text. A second session.compress during
a manual compaction (Desktop double-click) told the user to press Stop,
which does not end a compaction. Pass the _manual_compress_active marker to
busy_message like every other refusal site already does.

Finding: [Low BUG-IN-NEW-CODE] C04 quality r4 (fold3 missed this caller).
2026-10-06 13:59:55 +05:30
kshitijk4poor
21d19a42bc fix(tui_gateway): busy refusals during a manual /compress say to wait, not Stop
A manual compaction now holds the session `running`, so every running-gated
4009 refusal (tools.configure via _busy_error, session.undo, full-history
rollback.restore, and the slash mirror guard for /model /personality
/compress) fired mid-compaction with "Hermes is still replying. Stop the
current reply first". Both halves were wrong: nothing is replying, and Stop
deliberately keeps the compaction's busy claim, so following the advice did
nothing.

busy_message() takes compressing=; the session-holding callers pass
bool(session["_manual_compress_active"]) and get "session busy — a /compress
is running; run /<cmd> when it finishes." The "session busy" prefix is kept
because Desktop's isSessionBusyError/withSessionBusyRetry match on it.
2026-10-06 13:59:55 +05:30
kshitijk4poor
8740f3bbcc refactor(tui_gateway): restore the readable /compress and session.compress lines
The previous two moves freed seven lines in each facade, so the squeezes from
e84e82225a and bc58aa1be8 can be undone:
_cmd_compress keeps the separate `output = ...` / `_exec_out` lines and the
wrapped finalize call (back under the line limit), and session.compress binds
`sid = params.get("session_id", "")` on its own line, not in a walrus.
No behaviour change.

Gate finding: C04 r2 quality [Low CLEANUP].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
5081d4665f refactor(tui_gateway): move _compute_host_ack_error into compute_host_bridge
methods_session.py is over 2,000 lines and may only shrink against main.
bc58aa1be8 kept it at 2629 lines by folding `sid` into a walrus inside the
_manual_compress_turn call. Root AGENTS.md says to offset growth by moving a
function into a sibling module instead.

_compute_host_ack_error maps a compute-host control ack to an error reply.
It sits next to the compression code, and its callers (_save_via_compute_host,
_compress_via_compute_host) already depend on compute_host_bridge's
_send_compute_host_control, so it moves there. Pure move: the body is
unchanged. The callers resolve it bare through server.py's namespace. No test
or doc references it, and no re-export is left behind.

Gate finding: C04 r2 quality [Low CLEANUP].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
b010015cea refactor(tui_gateway): move _tui_model_friction into methods_shared_metrics
methods_tools.py is over 2,000 lines, so the code-health ratchet only lets it
shrink against main. e84e82225a kept the file at its cap by squeezing
_cmd_compress onto 118- and 136-column lines. Root AGENTS.md says to offset
growth by moving a function into a sibling module instead. This frees seven lines so
the next commit can restore the readable forms.

_tui_model_friction records the /retry and /undo model-friction metric, so it
belongs with the shared-metrics handlers. Pure move: the body is unchanged.
Its callers (/retry and /undo in methods_tools, session.undo in methods_session)
resolve it bare through server.py's namespace, which bind_module publishes from
the new module. No test or doc references it, and no re-export is left behind.

_cmd_compress itself could not move to methods_slash: the module-level
_SLASH_BUILTINS dict in methods_tools references it by value, so moving it
would need an import from methods_tools into methods_slash.

Gate finding: C04 r2 quality [Low CLEANUP].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
bb3f68f43b refactor(tui_gateway): drop the always-true guard in _manual_compress_turn's release
The finally block cleared running only when it popped _manual_compress_active.
That flag has one setter, the claim at the top of this context manager.
Nothing else pops it, and Stop no longer touches it, so the pop always
returned True. The comment claimed protection for a later turn's claim that
the guard never provided. Pop the flag and clear running unconditionally.
Behaviour is identical: the 2ab mutation run showed the guard was never false.

Gate finding: C04 r2 2ab + quality [Low CLEANUP].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
1369085be4 test(tui_gateway): pin the CompressionBusy race in the /compress core
bc58aa1be8 made _manual_compress_turn raise CompressionBusy instead of a bare
RuntimeError, so /compress and the slash mirror answer a lost race with
their busy reply instead of "compress failed". No test reached the
_compress_live_with_feedback path that /compress and the slash mirror share.
While the compaction holds the claim, the existing test now calls it and
expects CompressionBusy (RuntimeError before bc58aa1be8).

Gate finding: C04 r2 2ab [Low TOOTHLESS].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
352fd68757 test(tui_gateway): pin the tools.configure busy refusal during a compaction
6822acc49c made tools.configure refuse with 4009 while the session is
running, because its agent reset bumps history_version and a live turn would
drop its reply. No test reached that guard: reverting the hunk left the suite
green. The existing manual-compaction test now calls tools.configure while the
compaction holds the busy claim and expects 4009 (4017 without the guard).

Gate finding: C04 r2 2ab [Low TOOTHLESS].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
3fd16a2598 refactor(tui_gateway): keep methods_tools.py at its size cap
The CompressionBusy catch in _cmd_compress grew methods_tools.py from 2017
to 2019 lines, past its code-health ratchet cap (the file is already over
2,000 lines, so it can only shrink against main). Inline the
_exec_out result and the finalize call to bring it back to 2017. No behaviour change.
2026-10-06 13:59:55 +05:30
kshitijk4poor
c2c23f665c fix(tui_gateway): /compress --aggressive and --preview skip the busy claim
_compress_live_with_feedback entered _manual_compress_turn before parsing
the arguments, so the --aggressive refusal and the read-only --preview
report held the session busy and ran _drain_queued_prompt for calls that
never touch history (and refused with "busy" mid-turn instead of answering).

Parse first; answer --aggressive directly, run --preview on an unlocked
snapshot inside the profile scope, and claim the session only for the
committing path.

Gate finding: C04 r1 quality [Low].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
1ece75f5ee refactor(tui_gateway): a manual-compress busy race maps to the busy reply
_manual_compress_turn's locked running check raised a plain RuntimeError.
A turn admitted between a caller's unlocked pre-check and the claim was
reported as a failure (session.compress 5005, /compress 5009 "compress
failed", slash mirror "live session sync failed") and finalized a
context-engine notification for a compaction that never started.

Raise a dedicated CompressionBusy next to CompressionLockHeld; session.compress,
_cmd_compress and the slash mirror map it to their existing busy reply.
session.compress drops its now-redundant unlocked pre-check; _cmd_compress
keeps its pre-check (it also gates the compute-host route) and the slash
mirror keeps its _MUTATES_WHILE_RUNNING check (shared with /model, /personality).

Gate finding: C04 r1 quality [Low].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
7b0d582498 fix(tui_gateway): a manual compaction releases only its own busy claim
_manual_compress_turn's finally cleared session["running"] unconditionally.
If anything released the compaction's claim early and a later turn was
admitted, the finally clobbered that turn's running flag and reopened the
idle-admit path behind #133504. Clear running only while the
_manual_compress_active marker the context manager set is still present.

Gate finding: C04 r1 2c [Medium].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
644b9884c2 fix(tui_gateway): emit the running=false edge when a manual compaction ends
session.compress and /compress emit session.info inside
_manual_compress_turn, i.e. with running=true. Nothing emitted the closing
edge when the context manager released the claim, so Desktop kept the
session latched busy after every /compress.

Emit session.info via the existing _emit_session_info_for_session right
after the flags clear and before the queued prompt drains.

Gate finding: C04 r1 2c [High] regression.
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
8e315d00b3 fix(tui_gateway): Stop during a manual compaction keeps the busy claim
_manual_compress_turn holds session["running"] with no run thread, so
_interrupt_session_turn treated a Stop/Esc mid-compaction as a live turn:
it fired agent_loop_stopped + request_hard_interrupt on the compressing
agent (base never did) and cleared running while _manual_compress_active
stayed set. The next prompt.submit was then admitted on the "idle" session,
snapshotted history_version N and lost its reply when the compaction
committed N+1 -- the #133504 bug reached through Stop.

Skip the hard interrupt and the running/inflight release while the
compaction marker is set; the context manager's finally owns the release.
Queue cancellation and approval cleanup still run.

Gate finding: C04 r1 quality [Medium].
Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
276a0ae61b fix(tui_gateway): refuse tools.configure while a turn is running
tools.configure rebuilds the session agent through _reset_session_agent,
which clears history and bumps history_version with no running check. Mid-turn
that is the same desync as #133504: the live turn's commit sees the bump and
drops its reply. Reuse the _busy_error guard that /compress already uses.

Refs #133504
2026-10-06 13:59:55 +05:30
kshitijk4poor
06b94c946d fix(tui_gateway): hold the session busy for a whole manual compaction
session.compress and /compress were busy-checked only at admission and never
set session["running"]. A prompt.submit sent during the LLM summary was
admitted on the "idle" session and snapshotted history_version N; the
compaction then committed N+1 and _commit_turn_history dropped the turn's
reply ("history_version mismatch (expected=N current=N+1)").

_manual_compress_turn claims running (plus a _manual_compress_active marker
that _session_compression_in_flight honours, so steer/interrupt demote to
queue and never abort the compaction) for snapshot, summary, commit and
session-key re-anchor, then releases in finally and drains the queued prompt
via the existing _drain_queued_prompt so it runs on the compacted history.
The history_version fence stays as the second guard.

Reported-by: sryimnoob (@sryimnoob123)
Fixes #133504
2026-10-06 13:59:55 +05:30
Teknium
86bdb75b2b fix(update): the parked-branch check skips git cherry on a partial clone
Git builds without GIT_NO_LAZY_FETCH (upstream 2.43.0-2.43.3 and other
pre-May-2024 releases; the security backports added it to 2.39.4+) ignore the
override, so a parked branch with its own commits still sent git cherry into
an unbounded lazy fetch on a partial clone (#124767, git 2.43 on WSL). Live
with an unpatched git 2.43.0 build: 20 fetch children and 4 -> 24 packs for a
20-commit upstream, one fetch per commit. Cherry's patch-ids need blobs a
partial clone keeps remote, so it almost never refines the count there:
detect the partial clone from local config (remote.*.promisor, or the older
extensions.partialclone) and settle for the commit count. Same git: 0 fetches.

Co-authored-by: Yuan <xuyuann@icloud.com>
2026-10-06 01:06:29 -07:00
Teknium
f9798bcf9b fix(tts): validate a plugin's stream rate once; keep nous off plugin routing
Independent-review findings on the plugin PCM streaming path:

- stream_sample_rate was checked, then re-read and int()-ed in the
  adapter: NaN/inf raised out of resolve_streaming_provider (the speaker
  then played nothing) and 0.5 became sample_rate=0. Validate once
  (finite, >= 1 Hz) and hand that int to _PluginPCMStreamer.
- `nous` is an alias of the openai path in _get_provider but not a
  reserved name, so a pinned `tts.streaming.provider: nous` could stream
  a plugin registered as `nous` while the sync path speaks OpenAI.
- Document that stream() must be thread-safe: the speaker prefetches up
  to 3 sentences, unlike the serialized synthesize() path.
2026-10-06 00:52:41 -07:00
Teknium
6fda0f3a03 chore: map contributor email for gqwesleywang 2026-10-06 00:52:41 -07:00
Teknium
10f5530967 fix(tts): plugin PCM streamers keep the sync dispatcher's voice and routing
Follow-up to the salvaged plugin-streaming commit. The adapter read
voice/model from the plugin's own tts.<name> section, so a streamed reply
spoke the plugin default while synthesize() got tts.voice: the voice
changed with the surface. It also skipped the sync dispatcher's routing:
a same-named `type: command` provider (which wins for synthesize) lost
to the plugin on the streaming path, and an is_available() == False
plugin was streamed anyway.

- tools/tts_tool_plugins: one routing guard (_plugin_route_key) and one
  voice/model/speed builder (_plugin_voice_kwargs), shared by
  synthesize() dispatch and the new _plugin_pcm_streaming_provider
  (streams_pcm + positive stream_sample_rate + is_available(), read at
  resolve time)
- tools/tts_streaming: _PluginPCMStreamer stays a thin adapter;
  stream_channels/stream_sample_width dropped (the contract is int16
  mono, which every sink assumes)
- agent/tts_provider: streams_pcm / stream_sample_rate declared on the
  ABC with the contract
- tests: two invariant tests next to the plugin dispatch tests (the
  streaming test module skips without numpy); docs for plugin authors
2026-10-06 00:52:41 -07:00
Moninja
65ea2324af feat(tts): let PCM-streaming plugin TTS providers join the streaming path
Plugin-registered TTS providers only ever reached the sync dispatcher, so a
local engine plugin (Piper, KittenTTS, a self-hosted server) could not stream:
every spoken reply waited for whole-utterance synthesis even though the engine
emits audio per sentence. tts_streaming's resolver only ever looked at its own
built-in registry.

A registered TTSProvider that opts in with

    streams_pcm = True            # stream() yields raw int16 mono PCM
    stream_channels = 1
    stream_sample_width = 2
    @property
    def stream_sample_rate(self): ...   # Hz, from the model itself

is now adapted into a StreamingTTSProvider by _plugin_streamer(), so it feeds
the CLI/TUI speaker pipeline, the gateway StreamingTTSConsumer and the desktop
speak-stream WebSocket exactly like a built-in chunked provider
(elevenlabs/gemini/openai/xai).

- a provider that sets streams_pcm without a positive stream_sample_rate is
  refused (raw PCM played at a guessed rate garbles speech) and falls back to
  per-sentence synthesis
- the "never silently swap the user's voice" invariant holds: a pinned provider
  name that cannot stream returns None instead of substituting another provider
- tts.streaming.provider: auto falls back to the configured provider's plugin
  streamer only after the built-in priority list is exhausted

Tests cover adaption, the missing opt-in, the missing sample rate, a pinned
unusable name, a pinned plugin name, and the auto fallback.
2026-10-06 00:52:41 -07:00
Teknium
a02278293e chore: map contributor email for gmail (#133555) 2026-10-06 00:52:06 -07:00