Commit Graph

49694 Commits

Author SHA1 Message Date
Teknium
e032fdbca1 refactor(telemetry): registry prefixes come from the hub adapters; update reasons use tables
- `skills_hub._registry_prefixes` builds the identifier prefixes from the adapters' own
  SOURCE_IDs and `SkillsShSource._ID_PREFIX_ALIASES`, case-insensitively, so `skills.sh/...`,
  `skils-sh/...` and `OFFICIAL/...` stop reading `unresolved`. A matched source is named by
  `source_id()`, the method the router and the contract test use; two test fakes gain it.
- `update_failure_class`: exception type -> class and fixed stop-reason prefix -> class are
  tables instead of an if-ladder. `Windows gateway recovery failed: ...`, the fixed prefix
  update_completion writes when the post-update gateway resume fails, now reads
  `restart_failed` instead of `other` (prefix only, never the message).
- `mcp.servers.add` records through `mcp_rpc_helpers.record_mcp_add`, dropping the 175-column
  noqa line that kept methods_tools.py at its cap (2011 lines now).
2026-10-06 11:10:43 -07:00
Teknium
d6813a5dd7 fix(telemetry): extension failure classifiers are inert; the contract rejects contradictory install rows
- `_publish_failure_class`, `probe_failure_class` and `exception_failure_class` read a raise
  site's `failure_class` with `inspect.getattr_static` (`tagged_failure_class`), and the MCP
  type walk falls back to `connect_failed`. A third-party exception whose `__getattr__` or
  property raised replaced the user's PluginOperationError with RuntimeError (they run at the
  raise site, collection on or off), and the metric row was dropped; it now reads `exception`.
- `counter_dimensions_are_valid` adds the cross-field rule the per-field enums cannot state:
  failure_class in the kind's own set, `none` exactly on success, registry only on skill rows.
  Pre-split rows (no failure_class/registry) still validate.
- A refused dependency consent returns its reason as `_ConsentRefusal`, a str carrying the
  closed class (`non_interactive`, `deps_declined`, `manifest_invalid`), so publication no
  longer classifies by matching the user-facing copy. The text the user reads is unchanged.
- `extension_registry` imports the hub's `_SOURCE_ID_ALIASES` instead of a hand copy.
- test file: schema read uses utf-8-sig (windows-footguns).
2026-10-06 11:10:43 -07:00
Teknium
d3674513c8 docs(telemetry): the send explainer and Desktop consent window name the fresh-install note
The fresh-install receipt is kept from before the consent answer and counted
after an opt-in, so "data from before you opt in stays on this machine" was
false for it on the CLI send explainer and Desktop's consentWindow line in all
9 locales; only the pre-chat offer named the exception. Both now carry the same
"apart from the fresh-install note (noted on this machine and counted only once
you opt in)" clause. No i18n key changes; the CLI lines stay under 80 columns.
2026-10-06 11:10:43 -07:00
Teknium
ce14639879 fix(telemetry): a partial update with a failed build or restart keeps that failed_stage
update_cmd_fleet._verify_fleet_after_update writes `partial` for a failed build
(`not update_complete`) and for an incomplete restart as well as for a
verification gap. The relabel marked the synthetic verify stage failed for every
`partial`, so failed_stage moved from build/restart to verify and every such run
added a failed hermes.update.stage verify row. Verify is now failed by `partial`
only when no stage mark already failed (a stale/down fleet row still fails it).
The appendix row states the rule.
2026-10-06 11:10:43 -07:00
Teknium
717d8a3e5e fix(install-metrics): count the install note on a consented day; purge receipts on every opt-out
report_pending_installs recorded the fresh-install row at the first start,
usually the opt-in day itself (setup asks inside the install). The sender's
CONSENT_GATE_SQL needs period_start >= opened_at, so that day's package is
never sent: the metric mostly measured re-runs over already-consented homes.

- A receipt is recorded only when collection is on AND (sending is off, or an
  open send consent window opened at or before today 00:00Z); otherwise it is
  kept for the first start on a later day. Tested against the real store,
  real reconcile_send_consent and the real CONSENT_GATE_SQL: kept on the
  opt-in day, recorded the next day, and that package passes the gate.
- Every opt-out answer purges pending_installs AND pending_updates:
  save_consent(enabled=False) (setup, pre-chat offer, dashboard banner,
  `hermes setup telemetry`) and the tui_gateway shared_metrics.set RPC, via
  one shared_metrics_process.purge_pending_receipts used by begin_process too.
- Receipts are claimed by a plain rename, so an unparseable or non-dict
  receipt is deleted instead of sitting forever; a receipt no start counted
  within 7 days is deleted unreported (it would count under a later version).
- With Relay instrumentation off the store "settles" a row without recording
  it; the reporter now leaves the receipts for a start that records, instead
  of latching and deleting them uncounted.
- Docs: the hermes.install.run row states the consented-day rule, the 7-day
  cap, opt-out deletion and the interrupted rules.

Invariant tests red at 6246dd0f69d (5 in test_shared_metrics_install_run,
1 in test_shared_metrics_consent_rpc).
2026-10-06 11:10:43 -07:00
Teknium
5f7f9e143d fix(install-metrics): a Ctrl-C a stage handles no longer aborts the install; user aborts read interrupted
The ladder's INT/TERM traps wrote the receipt and exited as soon as the
foreground stage returned, even when the stage's child caught Ctrl-C and
succeeded (setup's provider picker prints "Provider setup skipped." and
carries on). gateway and complete were skipped, the bootstrap marker was
never written and rc went 0 -> 130: a telemetry write changed installer
control flow.

install.sh:
- The ladder traps (INT, TERM and now HUP, a closed terminal) only note the
  signal while a stage runs. rc==0 clears a Ctrl-C and the ladder carries on,
  as bash's cooperative exit did on main; rc!=0 with a signal noted writes
  `interrupted` and re-raises the signal. Outside a stage (platform check,
  banner, between stages) the signal stops the run at once with a receipt.
  The traps are installed with the EXIT trap, before check_platform.
- Inside a ladder stage, a signal marks STAGE_INTERRUPTED; stage_result
  writes `interrupted` over the stage's own class, so pm.cli /
  source_completion / `hermes setup` catching KeyboardInterrupt and exiting 1
  no longer reads deps_install_failed / setup_failed. A child the signal
  killed still ends the stage at once (status 128+N).
- A TERM/HUP sent to the installer alone takes effect when the running stage
  returns (documented; the stage stays in the foreground because a
  background stage's children ignore Ctrl-C under bash 3.2), and the receipt
  names the stage the run stopped before, not a finished one.
- A stage killed outright (SIGKILL/OOM) now leaves a `failed/<stage>/other`
  receipt; a failed write (full disk) removes its .tmp; the fallback id uses
  $(( RANDOM*32768+RANDOM )) (no octal parse); no clock -> no receipt.

install.ps1:
- A native child that ended on Ctrl-C (130 or STATUS_CONTROL_C_EXIT) reads
  `interrupted` over the stage's class; the receipt write removes its .tmp
  on failure; the dead `other` class on the pre-ladder Initialize-ResolvedPaths
  Fail is dropped (it runs before the ladder's try, so it never wrote one).

Tests (real install.sh main block with stub stage bodies, real SIGINT to the
process group): a handled Ctrl-C finishes the install with a success
receipt; a child that turns Ctrl-C into exit 1 reads interrupted. Both red
at 6246dd0f69d.
2026-10-06 11:10:43 -07:00
Teknium
4e0eae06e6 docs(telemetry): consent copy names install/update failure reasons and the local install note
CLI setup, the pre-chat consent offer and the Desktop collectedReliability line (all 9 locales,
no new key) now say update and install results carry a fixed-list reason and stage, and that a
fresh install is noted on this machine and counted only once the user opts in. The offer's
'data from before you opt in is never sent' sentence names that one exception.
2026-10-06 11:10:43 -07:00
Teknium
faf3a1e5c0 fix(install-metrics): one receipt per install run, interrupted runs counted, latch after save
install.sh: a run id per ladder, first writer wins (a parent-only SIGTERM no longer
leaves two receipts); Ctrl-C / SIGTERM write failure_class=interrupted.
install.ps1: same once-only guard, finally{} records an interrupted run.
Reporter: latch a receipt id only after its row is saved, so a short-lived start
whose daemon reporter is cut off at exit re-reports on the next start.
2026-10-06 11:10:43 -07:00
Teknium
74e0225377 feat(observability): hermes.install.run from a local installer receipt
install.sh / install.ps1 leave one closed-token receipt per full ladder run
(installer, outcome, failed_stage, failure_class, start/finish epoch) under
$HERMES_HOME/telemetry/shared_metrics/pending_installs. Every fail()/Fail call
site passes a closed failure class; the installer never sends anything.

The next Hermes start records each receipt once (latched by id, deleted after
the row is saved) only while collection is on; a start with collection off
purges the receipts unreported. Out-of-contract receipts are dropped.
2026-10-06 11:10:43 -07:00
Teknium
ef55a3f0fa chore: keep methods_tools.py at its line cap 2026-10-06 11:10:43 -07:00
Teknium
eb47ca507c fix(telemetry): a parked update receipt never claims it stopped before apply 2026-10-06 11:10:43 -07:00
Teknium
2cfab5a886 fix(telemetry): an unserved hub skill names the registry its identifier was for 2026-10-06 11:10:43 -07:00
Teknium
ab6f123eb4 docs+smoke(telemetry): failure_class/registry appendix rows; relay smoke asserts a real failed plugin install and a failed update run 2026-10-06 11:10:43 -07:00
Teknium
cbcbd2c8aa test(telemetry): exact failure_class/registry sets and the failed-stage-other invariant 2026-10-06 11:10:43 -07:00
Teknium
1b76735a5f feat(telemetry): extension installs and update runs say why they failed; hub skills name their registry
hermes.extension.install.count gains failure_class (closed per-kind set named at each failure exit
of the skills hub, plugin installer and MCP catalog emitters; raised exceptions classify by their
tagged class or TYPE, exception:<Type> collapses to exception, never the message) and registry
(the skills-hub adapter SOURCE_ID that resolved/served a skill; none/unresolved/other otherwise).

hermes.update.run gains failure_class derived from the FINAL receipt only (admission step,
stop_reason exception type, exit_code, stage marks, fleet states, gateway_restart). failed_stage no
longer reads 'other' for runs whose every mark passed: a 'partial' receipt (written only by the
post-restart verification) synthesizes a failed verify stage, and a failed run ending on a skipped
restart reads restart.

Package schema v4 is extended in place (canary-only): the pre-split field sets stay valid as the
second oneOf shape and in _LEGACY_METRIC_FIELDS so pending rows drain.
2026-10-06 11:10:43 -07:00
Austin Pickett
950ea4d51e test(desktop): shellSshDouble tolerates a child that exits before reading stdin
The spawn fixtures run commands that never read stdin, so on a fast Linux
runner the shell can exit before shellExec's stdin write lands and the
write fails with EPIPE. The stream had no error listener, so vitest
reported it as an unhandled error and failed the electron project even
though every test passed. The close verdict is the result; the EPIPE is
noise.
2026-10-06 14:07:07 -04:00
xxxigm
db6a9c65e6 test(desktop): prove the relaunch gate survives a transport that refuses big commands
A double that rejects any command over 4 KB - the shape of an OpenSSH mux
socket that cannot pass descriptors behind a large session request - must still
get a verdict, with the judge on stdin and out of argv. Red on base, where the
inlined program makes the gate raise "Could not prove that the remote Hermes
install is clear for SSH startup."

shellSshDouble forwards stdinData the way the real mux exec does, so the live
spawn and v2-marker fixtures now run `python3 -` against a real python3 and keep
proving the argument positions, lock behaviour and verdicts unchanged.
2026-10-06 14:07:07 -04:00
xxxigm
0630e548eb fix(desktop): ship the SSH relaunch marker gate on stdin, not in the ssh command
assertRemoteInstallUpdateClear inlined the ~7.5 KB marker gate program with
`python3 -c`, making the ssh command ~8.1 KB. ssh.exec rides the POSIX
ControlMaster mux socket, where ssh writes the session request and only then
hands our stdio descriptors over with sendmsg(SCM_RIGHTS); a request that still
fills the socket buffer - 8 KB for a unix-stream socket on macOS - leaves no
room for that descriptor message, so the pass fails with EMSGSIZE:

    mm_send_fd: sendmsg(0): Message too long
    mux_client_request_session: send fds failed

ssh exits 255 and the fail-closed gate reads it as "Could not prove that the
remote Hermes install is clear for SSH startup." on every attempt, so no remote
host connects while nothing is wrong on the remote. The window depends on the
client's socket buffer and the command's exact length, which is why a shorter
probe to the same host can still answer.

Hand the program to `python3 -` on stdin instead. Python compiles the whole
program before running it and the gate never reads stdin, so argv keeps the same
`<marker> [payload] [hermes...]` positions while the command stays a few dozen
bytes however the judge grows.
2026-10-06 14:07:07 -04:00
Teknium
945ffcd257 plugin-catalog: add tirith (opt-in pre-exec scanning, moved out of core)
Pins NousResearch/hermes-plugin-tirith@77405987: a pre_tool_call hook that runs
`tirith check --offline` on terminal commands and escalates findings to the normal
approval gate. Uses a tirith binary the user installs; nothing is downloaded.
Validated at the pin with `hermes plugins validate --install-deps` (scan: safe).
2026-10-06 11:00:51 -07:00
1052326311
c8ef24f8fe fix(review): preserve effective Git commit identity 2026-10-06 12:59:08 -05:00
kshitijk4poor
4ca6f9e91c fix(free-tier): a failed stdio announce still shows the challenge link
The stdio JSON-RPC transport deliberately re-raises host I/O errors
(ENOSPC, a locale that cannot encode the server's copy). _announce now
catches (OSError, ValueError) and returns False so present() falls back
to the terminal instead of losing the link and failing the run.

Also pins the ui-tui event -> presenter routing with a dispatch-level
test, and names the billing presenter factory like its sibling
(createBillingVerificationPresenter).
2026-10-06 23:14:10 +05:30
kshitijk4poor
69a12d2203 refactor(desktop,tui): free-tier challenge wiring lives in topical modules, not the facades
- desktop main.ts: the challenge IPC handler moves to
  electron/challenge-window-ipc.ts (registerChallengeWindowIpc), and
  resolvePortalBaseUrl moves next to the portal session that already
  consumed it (portal-session.ts), so the facade ends below its main
  line count; imports are sorted.
- desktop renderer: free_tier.challenge gets its own family handler
  (gateway-event/free-tier.ts) in the dispatcher table instead of a
  branch in handleLifecycleEvent.
- ui-tui: the challenge presenter and the existing billing step-up link
  presenter move to app/gatewayBrowserLinks.ts, keeping the event
  switch at or below its main size; the presenter test moves to its own
  file.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
9cb52daec2 refactor(free-tier): keep auth.py and anon_challenge.py inside the code-health ratchet
- _nous_portal_base_url moves from the auth.py facade to auth_nous.py
  (its sibling _nous_portal_env_override already lives there) and is
  re-exported with the rest of the Nous names, offsetting the
  run_with_challenge seam: auth.py ends below its main value.
- _announce no longer wraps the gateway broadcast in a blind
  `except Exception`: _broadcast_global_event already contains a wedged
  peer per transport, so anything else is a bug worth surfacing.
- _present_in_terminal catches what webbrowser.open can raise
  (webbrowser.Error, OSError) instead of Exception.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
8c0568ee73 test(free-tier): drop change-detector challenge tests; keep the invariants
Removed: the desktop/gateway surface-string tests (the gateway's
behaviour is pinned by "never waits", the desktop's by the inherited-flag
invariant), the full-payload-dict snapshot of a status read (the RPC test
covers the field), the server-copy whitespace/length detector, and the
TS "passes optional through as not-required" pass-through check.

Kept: wait outside the locks, background/gateway callers never block,
off-origin URLs refused, no loop on a second 428, the final mint is
authoritative, blips vs verdicts, PoW verdicts unchanged off the token
exchange, control characters never reach a terminal.

Also: FreeTierChallengePayload sorted into events.__all__.
2026-10-06 23:14:10 +05:30
kshitijk4poor
d0461ba6c3 refactor(desktop): free-tier challenge reuses the window-open policy and the contract's outcome type
- challenge-window.ts uses createWindowOpenHandler (deny + sanitised
  origin log, the link-title window's own hardening) instead of an
  inline deny.
- The renderer's outcome type is the generated contract's
  FreeTierChallengeResultParams['outcome']; global.d.ts uses the main
  process's ChallengeOutcome instead of a third hand-copied union, so a
  drift between the window and the contract fails the renderer typecheck.
- runFreeTierChallenge takes the gateway requester itself and builds the
  free_tier.challenge_result call, so lifecycle.ts and free-tier.ts stop
  each hand-rolling a reporter closure.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
d1f587331e fix(desktop): a required challenge ask never joins or inherits an optional run
The main process de-duplicated challenge windows by URL alone, and
remembered settled outcomes by url:attempt. If an optional (measuring,
never-revealed) run of a ticket was in flight, or had just ended
`failed` on #interactive, and the same ticket turned required, the
required ask got the optional run or its remembered `failed`, so no
window was ever shown for the human to clear. Key both the in-flight
run and the settled memory on URL + required.
2026-10-06 23:14:10 +05:30
kshitijk4poor
fa6ef8ba6d refactor(free-tier): one per-profile challenge record behind one lock
_pending (dict-of-dicts keyed "payload"/"deadline"/"outcome"),
_work_locks and _gave_up_until were three per-profile module globals,
two of them mutated outside the lock. They become one _ProfileChallenge
dataclass in a single _profiles map behind _state_lock; the record holds
the BrowserChallenge itself, so lookups are attribute reads and
as_payload() runs only when a status read asks for it. _set_pending
becomes _record(challenge, new_attempt=...). The test-only _presenter
seam goes: tests patch present() itself. _sleep stays, same idiom as
free_tier_bootstrap.

One edge differs, for the better: an EXPIRED same-URL record is now
treated as a new ticket (fresh deadline) instead of re-recorded with
min(new, old-expired) deadline, i.e. born expired.
2026-10-06 23:14:10 +05:30
kshitijk4poor
50b0082ed0 refactor(free-tier): build AnonChallengeRequired once; endpoint overrides table; codes live in anon_auth
- AnonChallengeRequired takes portal/credential/auth_state in its
  constructor. exchange_anon_jwt (given auth_state by its one caller,
  refresh_guest_state) builds it; the two catch-mutate-reraise blocks and
  the empty/None defaults are gone.
- _raise_for_anon_status no longer reads `portal_base_url=""` as an
  "is token exchange" flag. The exchange passes its verdicts as an
  `overrides` table keyed like _NAS_REFUSALS; 428 pow_* routes to an
  explicit proof-of-work verdict ahead of the 428 sign-in catch-all.
- ANON_CHALLENGE_REQUIRED / ANON_SIGNIN_REQUIRED and their copy are
  defined next to their siblings in anon_auth; anon_challenge imports
  them (and _anon_headers/_mint_memo_key) at module level, anon_auth
  imports anon_challenge lazily.
- client_info/user_agent read get_version_info().base_version instead
  of the old-updater __version__ shim.
- test: the stdio-TUI-gateway presentation test patches the real
  tui_gateway.server attributes; a SimpleNamespace stand-in broke the
  conftest server teardown.

No behaviour change.
2026-10-06 23:14:10 +05:30
kshitijk4poor
1eec4cf274 fix(free-tier): put the challenge link in front of ui-tui users
The TUI gateway child reports surface `cli`, so a 428 printed the link
to the child's stderr, which ui-tui keeps as a /logs tail and never
shows (createGatewayEventHandler: gateway.stderr). Over SSH or headless
that was a silent 120 s wait followed by "finish the check in your
browser" with no link. For a required challenge a foreground caller is
waiting on, the stdio TUI gateway now announces it on its JSON-RPC
channel, and ui-tui renders the message + link and opens one tab per
ticket. Background reads (keepalive, status paint) still never pop a
browser, and optional checks stay the desktop's to run hidden.
2026-10-06 23:14:10 +05:30
kshitijk4poor
4a370f1c76 fix(free-tier): only the desktop-spawned backend reports the desktop surface
client_surface() trusted raw HERMES_DESKTOP=1, which every terminal-pane
shell and agent child inherits (#116107 class). A `hermes` run in the
desktop terminal pane told NAS `surface: desktop` and suppressed its own
"finish the check in your browser" line. Use
process_identity.is_desktop_owned_backend(), which also needs the
per-spawn credential.
2026-10-06 23:14:10 +05:30
kshitijk4poor
e955022eb1 fix(proxy): never wait on a free-tier challenge under the Nous adapter lock
NousPortalAdapter._get_credential resolves credentials while holding
self._lock, which every proxied request queues on. With the challenge
gate on, a 428 parked that lock (and a default-executor worker per
queued request) for up to 120 s. Resolve as a background caller: the
challenge is recorded/announced and the retryable error returns at once.
2026-10-06 23:14:10 +05:30
kshitijk4poor
1507c8d0f7 fix(free-tier): a messaging gateway never waits on a browser challenge
The gateway resolves Nous credentials from async code with sync calls
(run_inbound._inbound_model_context_length -> runtime resolve, and
run_startup -> self_provision_relay -> resolve_nous_access_token). A 428
there parked the event loop for up to 120 s, and nobody sits at a
gateway's console to clear the check anyway. Treat the gateway surface
like a background caller: record/announce the challenge, raise the
retryable error at once.
2026-10-06 23:14:10 +05:30
kshitijk4poor
6dae8d059a fix(free-tier): keep the proof-of-work verdict for pow_* off a 428
The challenge commit dropped the `error == "pow_"` fallback in
_raise_for_anon_status. A pow_* error on a status with no wildcard row
(400/403/5xx) then became a retryable anon_server_error instead of the
terminal anon_pow_required, on every endpoint including sign-up and the
promotion paths the PR said were unchanged. Restore the fallback (kept on
purpose in 2a94ca80e7) and pin it on sign-up.
2026-10-06 23:14:10 +05:30
Robin Fernandes
90ff655c81 feat(free-tier): browser challenge primitive for the anonymous token exchange
NAS may answer POST /api/anonymous/token with 428 challenge_required and a
portal URL. Hermes implements the one `browser-v1` primitive: open the URL
(hidden where it can), poll /api/anonymous/challenge/status, exchange again.
Everything the page does (BotID, in-browser proof of work, an interactive
Turnstile fallback, its copy) belongs to the portal and changes without a
Hermes release. The mint stays authoritative throughout.

Python (hermes_cli/anon_challenge.py)
- the exchange reports surface (desktop / cli / gateway), version and the
  browser-v1 capability; a UA of the documented shape goes on every call
- the wait runs outside the auth-store and shared-store locks, under a
  per-profile work lock; only a caller someone is waiting on waits — the
  keepalive tick and a status paint announce the challenge and return
- 403 signin_required / access_denied and any 428 this version has no
  primitive for become a terminal sign-in error, scoped to the token
  exchange; sign-up and the promotion endpoints keep their previous verdicts
- status-poll 429 / 5xx / transport are blips; a 4xx is a verdict (the
  credential is gone); a second 428 starts a fail-fast cooldown so a boot
  burst of token readers does not each re-mint and reopen a window
- the pending challenge is published as the free_tier.challenge event and
  on free_tier.status for a client that connects later; the desktop reports
  how each window ended (free_tier.challenge_result) so the waiter re-mints
  promptly, and an attempt counter lets a fresh foreground attempt reopen a
  window the user closed without a status replay reopening it
- terminal: print the link, open a graphical browser once per ticket; a
  gateway process never opens one; control characters never reach the tty
- challenge URLs are only ever opened on the portal origin the exchange hit

Desktop (apps/desktop/electron/challenge-window.ts)
- the page loads in a hidden, sandboxed window of its own partition, pinned
  to the portal origin (will-navigate, will-redirect, off-origin commits end
  the run); a phase is read only from a main-frame portal URL fragment
- #interactive reveals the window (never for an optional challenge), #done
  and #failed close it; an HTTP error commit, a load failure and a crashed
  renderer are retryable, a closed window is not; timers are bounded
- the renderer relays the event and the status read to one window per
  attempt and reports the outcome once; a backend without the RPC is fine
2026-10-06 23:14:10 +05:30
Austin Pickett
80cf5183f8 chore: map contributor emails for #133991
Add contributors/emails/ mappings so check-attribution resolves the
author emails on this branch:
- GuerrillaMedia702@gmail.com -> AP3X-Dev
- zaclanzon@users.noreply.github.com -> zaclanzon
2026-10-06 13:23:15 -04:00
Austin Pickett
fd431bedbc test(dashboard): per-call model attribution regression for #71778 2026-10-06 13:23:15 -04:00
Zac Lanzon
0cc2bcdcee fix(dashboard): count distinct models over the merged row set
totals.distinct_models was COUNT(DISTINCT model) over sessions alone, so a
model reached only through auxiliary usage drew a card the header never
counted. Derive it from the same merged rows the cards come from (#89631).
2026-10-06 13:23:15 -04:00
AP3X
2c48f5ff6c fix(dashboard): fold aux usage into the model card it belongs to
/api/analytics/models appended one row per (model, task, provider) from
_aux_usage_rows next to the sessions-derived (model, provider) row, so a
model doing approval/compression/title/background_review work rendered as
four or five identical-looking cards, and their session counts summed past
totals.total_sessions.

Fold aux usage into the existing pair's row; only an aux-only pair (no
sessions-derived row) becomes a new row carrying its own session count,
so dedicated aux models stay visible (#23270).

Ported from #89707 (web_server.py -> web_routers/analytics.py after the
router decomposition). Same diagnosis as #77524.

Co-authored-by: Kumar Sarthak <kumarsarthak0712@gmail.com>
Co-authored-by: Zac Lanzon <zaclanzon@users.noreply.github.com>
2026-10-06 13:23:15 -04:00
Austin Pickett
d40794140b fix(dashboard): attribute models usage per API call from session_model_usage 2026-10-06 13:23:15 -04:00
unsupportedpastels
aac7ad880d fix(scratch): escape control characters in prune-log paths and names
Review of #133952 (ehz0ah) showed a legal POSIX name containing a newline was written across two lines of scratch-prune.log, so one record split and the tail of the name could pass for a record of its own. Paths and process names are now logged with %r, so control characters are written escaped and each record stays one line.

Refs #132401.
2026-10-06 13:22:50 -04:00
unsupportedpastels
788976407a fix(scratch): keep prune-log records for non-UTF-8 names and concurrent rollover
Review of #133952 (andrexibiza) reproduced two ways a deletion could leave no line in scratch-prune.log: a legal POSIX name that is not UTF-8 hit the strict utf-8 handler and the whole record was dropped, and when two prunes of one home rolled the log over at once, the second rename found the file gone and the record was dropped with the FileNotFoundError.

The handler now writes with errors=backslashreplace, and its rollover treats a source renamed away by a concurrent prune as already rotated and writes the record to the new file.

Refs #132401.
2026-10-06 13:22:50 -04:00
unsupportedpastels
67784d8a89 fix(scratch): record every pruned entry and reaped process in logs/scratch-prune.log
The idle prune deleted entries and TERM/KILLed processes with one aggregate INFO line, invisible at the default WARNING level and absent entirely at boot, before setup_logging() runs. It also counted an entry as removed even when rmtree (ignore_errors) left it behind.

Each removed entry (path, size), each signal sent (pid, name, cwd) and each entry that could not be removed now gets a line in <home>/logs/scratch-prune.log, through a RotatingFileHandler of its own (fixed 5 MB x 3, as tool_calls.log), so the boot-time prune is recorded too. The handler belongs to one prune and is closed when it ends: no process holds the file open between prunes (an open handle blocks rollover on Windows), and prunes of two homes never share it. Only entries that are really gone are counted. A process that exited but is not yet collected (zombie) is not sent KILL.

Refs #132401. Salvages #132455.

Co-authored-by: Justin Sharpe <2106314+justinsharpe@users.noreply.github.com>
2026-10-06 13:22:50 -04:00
Mark S.
970ba79fb4 Merge pull request #133975 from victor-kyriazakos/fix/own-patch-carries-write-baseline
fix(tools): a task's own patch keeps its full-file write baseline
2026-10-06 13:14:27 -04:00
Brooklyn Nicholson
cd94de7ea4 test(desktop): tool disclosure cache evicts on unmount
Regression test for #131121: after subscribe -> unsubscribe (a row's
mount -> unmount), a second call with the same id must return a fresh
atom — the entry was released instead of being retained for the
window's lifetime — while identity stays stable while subscribed.

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-06 12:09:55 -05:00
Yuan Li
b8c844b99a fix(desktop): evict inactive tool disclosure atoms 2026-10-06 12:09:55 -05:00
Kuxin
0fa1fb600d fix(stt): normalize local command language hints 2026-10-06 12:08:04 -05:00
Kuxin
0d234e714c fix(stt): normalize local language hints 2026-10-06 12:08:04 -05:00
Austin Pickett
35f0b4b569 fix(dashboard): route native text paste through term.paste to stop last-char duplication
A native/DOM paste on the chat terminal (context-menu paste, middle-click,
or any Ctrl+V route that bypasses the keydown interception) fell through to
xterm's own paste listeners. xterm clears the hidden textarea during the
paste dispatch, but the browser's default action — inserting the pasted
text into that textarea — runs after dispatch, so a stale value survives
and the next typed character is duplicated (#52471: "when" → "whenn").

The host capture-phase paste handler now cancels the native paste for
plain text and delivers it via term.paste() exactly once — the same clean
route the Ctrl/Cmd+V keydown interception already uses. Image pastes keep
their upload path and are never routed as text.

Supersedes #52477 (credit: @liuhao1024 — same interception, extended with
the image route preserved and regression coverage).

Fixes #52471
2026-10-06 13:00:43 -04:00
Austin Pickett
7ed284fee0 fix(web): satisfy code-health ratchet and contributor attribution
- PtySession.close and _discard_active_session_file: waive the deliberate
  teardown catch-alls with reviewed '# health: allow' comments (BLE001+S110).
- pty_ws was over its main-pinned CC cap: extract the channel-marker prologue
  into _pty_channel_marker_state and the keep-alive writer loop into
  _pump_keepalive_input.
- Map sinh3543@gmail.com -> sinh2024 (check-attribution, PR #133936).
2026-10-06 12:46:06 -04:00
Austin Pickett
3178ef3e21 test(pty): the channel marker dict is empty after every PTY lifecycle end
Regression coverage for #63553: _active_session_file_for_channel
registered channel→tempfile entries that no code path popped, so
pty_active_session_files grew without bound and churned a forgotten
tempfile+inode per reconnect.

Proves the invariant across every session end/forget path:
- keep-alive close (close_all, same path as TTL reap / child EOF)
- repeated same-channel reconnects keep one mapping
- legacy 1:1 pump (no attach token) pops its marker on disconnect
- ?fresh=1 drops the stale breadcrumb yet keeps the mapping so the
  next same-channel reconnect resumes the fresh child (the regression
  flagged in the #63563 review)

All four are red on 85db7c3a68.
2026-10-06 12:46:06 -04:00