Commit Graph

16739 Commits

Author SHA1 Message Date
fangliquan 6601a004a3 test(telegram): tighten polling liveness assertions 2026-09-15 04:39:16 -07:00
fangliquan e1e943a9df fix(telegram): expose polling transport recovery 2026-09-15 04:39:16 -07:00
teknium1 2298dc8122 fix: propagate caller contextvars into the Feishu adapter-owned executor
Moving the dedup flush and thread lookup from asyncio.to_thread onto the
adapter-owned pool fixed the torn-down default executor, but to_thread also
copies the caller's contextvars and run_in_executor does not. A multiplexed
profile's HERMES_HOME override and secret scope are contextvars, so those
workers silently ran under the launch profile. _run_blocking now runs the call
through contextvars.copy_context().run, matching to_thread semantics.

Review finding: _run_blocking lost the profile HERMES_HOME override / secret scope on the worker.
2026-09-15 04:38:26 -07:00
teknium1 854e02ca6a test(feishu): give the WS scope-hop stub the adapter-owned executor
_connect_websocket now submits the SDK thread to _get_sdk_executor() instead
of the loop default executor; the SimpleNamespace adapter stub in the
profile-scope test predates that contract and lacked the method.
2026-09-15 04:38:26 -07:00
JackJin 5fbef868a9 fix(gateway): keep Feishu websocket off default executor 2026-09-15 04:38:26 -07:00
liuhao1024 957a8a287c test(feishu): restore the loop default executor torn down by the dedup test
The regression test swaps in a torn-down default executor but never
restores the pristine one, leaking the poisoned executor to anything
sharing the loop. Save/restore via the private slot because
set_default_executor() rejects None, which is exactly the pristine
(lazy-creation) state. Also apply ruff format to the file.
2026-09-15 04:38:26 -07:00
liuhao1024 fa2c72746c fix(feishu): run the inbound dedup flush on the adapter-owned pool
A dead background event loop tears the loop's default executor down, and
after that every inbound message was dropped inside the dedup gate with a
RuntimeError out of asyncio.to_thread — the adapter went permanently deaf
while the gateway process, websocket and service all stayed healthy.

#10849 already moved the outbound SDK calls onto an adapter-owned,
self-healing pool; this gives the inbound dedup-state flush the same
treatment, so a default-executor teardown can no longer wedge message
intake.
2026-09-15 04:38:26 -07:00
teknium1 08bb2273e4 fix: make the allow_all_users env bridge own what it writes and re-derive it on restart
The bridge wrote GATEWAY_ALLOW_ALL_USERS into os.environ only when unset and
never cleared it. In-process restart paths (gateway restart watcher, dashboard
profile actions) copy os.environ into the child, so a config.yaml grant became
a sticky env var: flipping allow_all_users to false and restarting left the
gateway OPEN. The bridge now tracks its own write (module flag), overwrites or
clears it on reload, exports only a truthy grant (presence-based readers such
as the Telegram intake prefilter treated "false" as configured auth), and the
two restart env builders drop the bridge-owned value so the child re-derives
the posture from its own config.yaml. Under multiplex_profiles the DEFAULT
profile's events are authorized inside its secret scope, where gate readers
never fall to os.environ; the bridged grant is now seeded into that profile's
scope mapping only (secondaries never inherit it).

Review finding: bridged GATEWAY_ALLOW_ALL_USERS survives restart and overrides a flipped config.yaml; inert for the default profile under multiplex; "false" exported as configured auth.
2026-09-15 04:37:41 -07:00
teknium1 5083d5f78e fix(gateway): honour allow_all_users from config.yaml by bridging it to GATEWAY_ALLOW_ALL_USERS
`gateway.allow_all_users: true` (and the top-level spelling) in config.yaml
was a silent no-op: `_TOPLEVEL_BRIDGE` never forwarded it, GatewayConfig has
no field, and every allow-all reader (authz mixin default-deny branch,
startup access check, own-policy adapters, Discord/Matrix/Email plugin
gates) consults the GATEWAY_ALLOW_ALL_USERS env var only.

Bridge the YAML key into that env var in `bridge_core_env_settings`, the
one seam every reader already shares, instead of threading a new config
attribute through ten readers:
- first-writer-wins: an explicit env var beats YAML (matches every other
  {PLATFORM}_* gate);
- skipped inside a multiplexed secondary profile's scope (#80099 class):
  the secondary's config.yaml must not become the default profile's policy,
  and the isolation test now asserts GATEWAY_ALLOW_ALL_USERS stays unset;
- a startup warning names config.yaml as the grant source, because the key
  was inert until now and a forgotten `true` flips the posture to open.

Tests: both spellings authorize a stranger through `_is_user_authorized`;
`false`, absent key, and env=false over YAML=true all stay denied.
Docs: security guide, env-var reference, gateway internals.

Fixes #110690
2026-09-15 04:37:41 -07:00
fangliquan 584fc99a53 test(photon): cover inbound NDJSON framing 2026-09-15 04:34:13 -07:00
fangliquan 992b517fdd fix(photon): preserve Unicode NDJSON separators 2026-09-15 04:34:13 -07:00
teknium1 676ae4ab36 test(gateway): keep the two invariant tests for the interim-only duplicate-send diagnostic
Drop the change-detector asserting the default attribute value; the two remaining tests
pin the invariants (a delta-fed consumer keeps the WeCom ack-race warning, an interim-only
consumer does not).
2026-09-15 04:31:15 -07:00
webtecnica b065e861b3 fix(gateway): skip duplicate-send diagnostic for interim-only stream consumers (#105341) 2026-09-15 04:31:15 -07:00
teknium1 cc6b6aa8fd fix: de-punctuate autonomous silence candidates like the interactive rule
The cron/webhook lane compared the stripped response and its first/last
line against the marker set without stripping edge punctuation, while
the interactive predicate did. A Chinese lane naturally renders the
sentinel as 【静默】 or 静默。, which chat suppressed but cron still
delivered — the very lane #110935 was filed against. Route the three
autonomous candidates through is_intentional_silence_response so both
lanes share one canonical form; the bracketed-prefix rule and the
embedded-in-prose rejection are unchanged.

Review finding: is_autonomous_silence_response never de-punctuated, so 【静默】 / 静默。 passed cron/webhook while is_intentional_silence_response suppressed them.
2026-09-15 04:30:31 -07:00
teknium1 71d229f5cd fix(cron): the silence instruction names [SILENT] as an untranslatable ASCII token; docs + trim tests
Follow-up to the salvaged #110940 commit:

- `cron/scheduler_prompt.py::_CRON_HINT` tells the model the sentinel is a literal
  ASCII control token that must never be translated or rephrased — the prompt-side
  half of the fix, so a lane answering in any other language is steered to the
  canonical token instead of relying on the filter knowing that language.
- Docs: the supported-token list ('Intentional Silence Tokens') gains the zh-Hans
  forms in the English page and the zh-Hans mirror gets the section it lacked.
- Tests trimmed to two invariants (translated forms match in every shape the English
  ones do; prose that mentions the word is still delivered), proven red on origin/main.
2026-09-15 04:30:31 -07:00
Todd Dailey 623eb48f2f fix(gateway): a translated intentional-silence sentinel is still silence
A model lane that does not think in English answers the cron instruction
("respond with exactly [SILENT]") by translating the sentinel rather than
dropping it. LIVE_GATEWAY_SILENT_MARKERS is English-only, so the whole control
token is delivered to the user as content. Carry the zh-Hans forms (the only
non-English locale this project documents) and derive the autonomous lane's
bracketed-prefix rule from the set so the two rules cannot drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oAAQ4T859Y8fWNXRoiNEB
2026-09-15 04:30:31 -07:00
teknium1 4db4650a01 fix: exempt the hidden canonical Bot Chat from the idle auto-archive sweep
The archived-holder carve-out in _set_session_title lets the next Bot open
claim the "Bot Chat" title from an archived canonical row. That is correct
for a deliberate sidebar archive, but archive_stale_sessions (the opt-in
sessions.auto_archive sweep) could also archive an idle hidden Bot Chat with
end_reason NULL, which unarchive_recoverable_session refuses; the next Bot
click then stripped the old row's title, retiring the bot's whole history
on an idle timer with no way back.

Skip the hidden canonical Bot Chat in the sweep SELECT using the same
hidden + exact-title predicate set_session_pinned already uses to protect
it, so only an explicit archive retires a Bot Chat. Reword the bot-mode doc
so it no longer promises the retired (still hidden) chat is reachable from
the archive view.

Review finding: auto_archive sweep could irreversibly retire the canonical Bot Chat; docs over-promised archive-view reachability.
2026-09-15 04:27:32 -07:00
KoNit-K b6207cb590 fix(state): release archived Bot Chat title 2026-09-15 04:27:32 -07:00
Teknium a654de291a Merge pull request #111844 from NousResearch/fix/cron-script-not-found-hint
Cron 'Script not found' explains per-profile lookup and the fix (#94821, salvage #105775)
2026-09-15 04:26:33 -07:00
teknium1 13f908f10d fix: inline MEDIA data URLs on the API server despite a terminal <|eos|> sentinel
The API server's _resolve_media_to_data_urls ran MEDIA_TAG_CLEANUP_RE over the
raw final_response, bypassing the sentinel seam this branch added in
gateway/platforms/base.py. A provider that leaked its end-of-sequence token
glued to the last MEDIA path (MEDIA:/x.png<|eos|>) therefore still produced no
inline image and returned the raw tag to the HTTP client (non-stream and SSE
chat completions) - the exact symptom fixed for the chat platforms. The scan
now stops at _terminal_sentinel_start and drops the control token only when a
tag actually resolved, so unrelated responses stay byte-identical.

_terminal_sentinel_start also recognises a run of repeated exact tokens
(MEDIA:/x.png<|eos|><|eos|>), so a doubled leak no longer hides the attachment
either; mid-text or non-exact sentinels are still not a media boundary.

Review finding: api_server sibling MEDIA surface bypassed _mask_media_scan_text; doubled terminal sentinel still leaked
2026-09-15 04:26:30 -07:00
teknium1 2bade67c04 fix(gateway): a leaked terminal <|eos|> sentinel no longer hides the MEDIA attachment
A provider that leaks its end-of-sequence control token glued to the last
MEDIA path (MEDIA:/x.png<|eos|>) made BasePlatformAdapter.extract_media
return no media: neither MEDIA_TAG_CLEANUP_RE nor MEDIA_EXTENSIONLESS_TAG_RE
accepts '<' as a path terminator (deliberately - widening the delimiter set
reopens the glue classes #68773/#88038), so the file was silently dropped
and the raw tag leaked into the chat.

Recognise the one exact token at the shared normalization seam instead:
_mask_media_scan_text blanks a terminal <|eos|> (offset-preserving) so the
glued tag ends on whitespace for every scan - extraction, display strip and
stream cleanup - across the known-extension, unknown-extension and
extension-less branches; _deliverable_tag_spans deletes the sentinel with
the tag it terminated. Not the exact token, not terminal, or inside a
protected code/JSON span: byte-identical to before.

Fixes #111046

Co-authored-by: Hukla <129692708+huklaa@users.noreply.github.com>
Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>
2026-09-15 04:26:30 -07:00
kshitijk4poor 569b4242a3 fix(auth): a Portal-returned inference host is accepted only when the operator named it
#111809 accepted any *.nousresearch.com https host once the operator's Portal override
pointed at a non-production Portal. That let a network-provenance value — the Portal's
refresh response — pick any Nous-owned host as the bearer recipient, including hosts that
are not inference gateways. Owning the DNS suffix is not the same as being an authorized
recipient, and the validator's threat model (an injected refresh response) is exactly the
case a suffix rule fails to bound.

The recipient is now the operator's own NOUS_INFERENCE_BASE_URL: a non-production host
returned by the Portal is accepted exactly when it equals that override's host, otherwise
the strict production set stands. The Portal override grants nothing by itself. What the
operator gains over plain use of the override is that the Portal's value is then persisted
and used for the pricing scope and proxy instead of being healed to production, and the
per-turn "refusing inference URL host" warning stops. No environment is named in code.

Raised on #111809 review. Tests: recipient match accepted, unrelated Nous host refused, no
override refused, Portal override alone grants nothing, the match follows the profile scope
under multiplexing; the widening cases are red on main. Docs row for NOUS_INFERENCE_BASE_URL.

Co-authored-by: Ben Barclay <ben@nousresearch.com>
2026-09-15 16:55:43 +05:30
kshitijk4poor aaed2a238f fix(auth): routing overrides fail closed when a multi-profile call has no secret scope
get_secret() already reads os.environ in a single-profile process; it raises
UnscopedSecretError only when multi-profile hosting is active and the call has no profile
scope. _nous_inference_env_override and _nous_portal_env_override caught that and read the
ambient env anyway, which is the launch profile's value — so a routed call that lost its
scope could POST a secondary's refresh token to the launch profile's Portal, or send its
inference bearer to the launch profile's host. Since #111620 the launch profile itself
carries a frozen-env scope while hosting, so the exception never means "single-profile
CLI"; it means the caller has no authority over that value. Both readers now go through
_scoped_operator_override, which returns None in that state — the override is absent, the
stored/default routing applies, nothing raises (pricing, runtime_provider and the guest
path rely on these never raising).

Raised on #111809 review. One parametrized invariant test over both readers: environ when
unscoped single-profile, the scope's value under hosting, None on a scoped miss, None with
no scope; the no-scope leg is red on main.
2026-09-15 16:55:43 +05:30
teknium1 f3c570b663 test: trim CJK opt-in coverage to the shared eligibility helper
Main routes sends, finalize-edits and rich drafts through one _rich_content_ok
predicate, so the draft-specific opt-in test duplicated the send test's
invariant. Keep the send (BMP + astral CJK) and finalize-edit cases.
2026-09-15 04:24:53 -07:00
wang2 fa12d7556c fix(telegram): allow opt-in CJK rich messages 2026-09-15 04:24:53 -07:00
teknium1 0ea0c53e89 fix: record a known exit status when the reader's wait() raises
The early-EOF reaper made _finish_reader return without publishing when
wait() raises, so the session stays tracked for later reconciliation.
That is right for the pipe path (_reconcile_local_exit can still reap via
session.process), but PTY sessions have no session.process: when
ptyprocess.wait raises (waitpid ECHILD after isalive() already reaped the
child) the exitstatus is known, yet poll() reported "running" forever.

Only leave the session tracked when exit_code() is still None; otherwise
record the known status and finish as before.

Review finding: PTY session whose pty.wait raises stays in _running forever (fail-open regression vs main)
2026-09-15 04:23:32 -07:00
teknium1 dc4a2aeb10 test(process): trim early-EOF reaper test to its two invariants
Drop the wait-timeout call-shape assertion; the invariants are that the
session records the real exit code and that a failed reap does not publish a
false completion.
2026-09-15 04:23:32 -07:00
fangliquan 1bfbeff4e5 fix(process): reap children after early stdout EOF 2026-09-15 04:23:32 -07:00
teknium1 8bc5894a3a docs(skills): ip-as-logo follows the modern section order; add skill tests
Authoring standard 5 wants `# <Skill> Skill`, then When to Use,
Prerequisites and Procedure; the port kept the upstream layout with the
trigger sentence in the intro and no prerequisites section. Body text is
unchanged; the docs page is regenerated for this skill only.

Standard 7 asks for tests/skills/test_<skill>_skill.py: two invariants —
frontmatter/section structure, and generation routed through the native
`image_generate` tool with no residue of the upstream harness.
2026-09-15 04:20:15 -07:00
teknium1 b531023622 fix(gateway): drop the redundant whole-block lease; one invariant test per atom; document the watchdog env vars
The per-step leases inside maybe_auto_archive / maybe_auto_prune_and_vacuum
(archive, prune, sweep, vacuum) cover every long step of the construction-time
block, and each renews right before the step it protects, so the extra
report_startup_progress(900) at the top of GatewayRunner._init_session_db
added nothing but a stale phase label ("gateway_startup_state_maintenance"
would outlive the archive step and mask the phase name in the fired record).
Dropped; gateway/run.py is back to origin/main.

Tests: the two contributor tests monkeypatched report_startup_progress in the
module and asserted phase names (change-detectors on the strings). Replaced by
one test that arms a REAL StartupWatchdogHandle and asserts the maintenance
block renews it four times with lease_until in the future — the property the
poller's `lease_until > now` branch actually needs (#111092). Red on
origin/main: lease_count stays at the schema-init lease.

Docs: HERMES_STARTUP_WATCHDOG / HERMES_STARTUP_WATCHDOG_TIMEOUT_S existed only
in the module docstring; add them to website/docs/reference/environment-variables.md
next to the respawn-storm variables (existing env vars only, no new surface).
2026-09-15 04:19:19 -07:00
Ayush Nangia bcab2dd6eb fix(gateway): persist startup watchdog stacks before stderr
A detached or service-managed gateway can have a blocked stderr. The watchdog exit escort then hard-exits after ten seconds before the file-based traceback is reached, leaving only the metadata record. Write the durable dump first and pin the ordering with a regression test.
2026-09-15 04:19:19 -07:00
Kevin Rajan cef69276ae fix(gateway): hold startup-watchdog progress leases across state.db auto-maintenance
Construction-time maybe_auto_archive / maybe_auto_prune_and_vacuum ran
synchronously with no report_startup_progress lease. A multi-minute
VACUUM of a large state.db accrues near-zero CPU, so the startup
watchdog misread it as a parked deadlock and killed the attempt with
exit 75, live-locking gateway restarts. Renew the lease per long step
(prune, orphan sweep, VACUUM, archive) since leases clamp at 900 s,
plus one lease around the gateway maintenance block.

Fixes #111092
2026-09-15 04:19:19 -07:00
teknium1 5358780264 fix: keep the carried pre-admission input across the waited lease reload
carry_unadmitted_user_message appends the interrupted turn's user row to the
early result's in-memory history only; it is never persisted because that turn
never owned the lease. When the follow-up turn also has to wait for the lease
(the common case: the other process that made the first turn wait is usually
still busy), admit_durable_turn_lease reloads conversation_history from the DB
after admission and replaced the caller's list wholesale, so the tagged row was
dropped from both the model input and state.db. Re-append the tagged rows that
have no _row_id after the reload so the follow-up turn sees and flushes them.

Review finding: waited-reload branch of admit_durable_turn_lease discarded the
_persist_after_admission_interrupt row carried from the aborted turn.
2026-09-15 04:18:34 -07:00
teknium1 fae3030fa6 refactor: carry the unadmitted user message from the lease sibling, not the facade
Move the pre-admission carry-forward out of run_conversation (facade) into
agent/turn_facade_lease.py::carry_unadmitted_user_message next to the early
result it repairs, and drop the extra turn-start flush in build_turn_context:
the follow-up turn's normal turn-start persist already writes the marked row
because _db_flush_collect no longer stamps it as durable (live probe: exactly
one A row in state.db after two flushes). Trim to two invariant tests
(carry-forward with metadata; flushed exactly once); the hard-stop negative is
covered by the E2E probe in the PR body.
2026-09-15 04:18:34 -07:00
Ayush Nangia 24ae31f7e7 fix(gateway): retain pre-admission interrupted input 2026-09-15 04:18:34 -07:00
teknium1 9efa50071a fix: match find/read-tool dynamic words only as unquoted command arguments
The dynamic-shell-word rules fired on any `-del*`/`-exec*` substring after a
`find` anywhere in the segment, so quoted predicate arguments the shell never
expands were flagged: `find . -name 'log-del*'`, `find . -name 'pre-exec*.sh'`,
`find src -path '*-exec[0-9]*'`, and `echo find . -{delete,print}`.

Three changes close that:
- `find` must be the command word (_CMDPOS anchored, same as mkfs/rm/dd) and
  the dynamic word must start a whitespace-delimited token (`(?<!\S)`), for
  both the find rule and the rg/sort/ag/man program-option rule.
- Both rules now scan the quote-masked variant (_QUOTE_MASKED_DANGEROUS_
  DESCRIPTIONS, same _mask_quoted_prose used by the positionless hardline
  rules) so glob characters inside quotes are data, not expansion.
- _iter_shell_command_starts no longer treats the `{` inside a brace-expansion
  word (`-{delete,print}`) as a brace-group opener; it split the word across a
  marked start so `echo x; find . -{delete,print}` matched nothing. A brace
  group opener is `{` as its own word (after whitespace or a separator).

The quoted-name cases join the inert parametrized test; the separator case
joins the dangerous one. Still two parametrized functions.
2026-09-15 04:17:42 -07:00
Teknium 06a3a98751 fix: gate dynamic shell words in approval checks
Port from openai/codex#39159: require approval when shell expansion could synthesize destructive find flags or program-executing read-tool options.
2026-09-15 04:17:42 -07:00
teknium1 bc3df8a4d5 fix: NT-namespace guard fires before every sibling resolve (checkpoint, ACP bridge, @file:)
Three paths still resolved the raw model/remote-supplied string before the
guard could refuse it, so on Windows the NTLM-leak trigger (resolving the
path) ran anyway: the file-checkpoint helper stats write_file/patch targets
before the tool executes; the ACP file bridge resolves fs/read_text_file and
fs/write_text_file paths before its read/write denylists; and @file:/@folder:
references resolve their target before the reference allow-check. Each now
checks the raw string first and refuses. The GLOBALROOT form now requires
its path separator so a GLOBALROOT-prefixed local name is not misclassified.

The rationale comment names the vector instead of another product's
changelog, and the security docs say the row is enforced on reads as well
as writes, since it sits under the write-guard table.
2026-09-15 04:17:31 -07:00
teknium1 07e6461dfe fix: guard search_tool's root and trim the NT-namespace tests to two invariants
search_tool resolved its root via _resolve_path_for_task BEFORE the
NT-namespace check saw it (review finding): on Windows the resolve is the
SMB-auth trigger, on POSIX the task-base join hides the prefix from the
resolved-path denylist. The raw-string guard now runs first there too,
and the guard rides the existing top-level agent.file_safety import
instead of two function-local imports.

The tool-layer chokepoint test now covers all four entries and proves
none of them touched Path/_resolve_path_for_task/realpath before the
refusal; the 12 form-matrix tests collapse into one blocked/allowed
invariant over both classifiers.
2026-09-15 04:17:31 -07:00
Teknium faf71eb4c1 Inspired by Claude Code: file tools reject Windows NT-namespace paths (NTLM leak hardening)
Claude Code v2.1.234 (Aug 17, 2026) hardened its pre-approval file
accesses to reject Windows NT-namespace (\??\) paths against the NTLM
credential-leak vector. Port the same guard into Hermes file safety:

- agent/file_safety.py: is_nt_namespace_path() / get_nt_namespace_error()
  raw-string check (never resolves — resolving IS the leak trigger).
  Wired as the first check in get_read_block_error() and the write
  denial classifier.
- tools/file_tools.py: raw-string guard at read_file_tool entry and in
  _check_sensitive_path (covers write_file_tool + patch_tool), before
  the task-base join can anchor the prefix under a POSIX base dir.
- Blocks \??\, \\.\, \\?\UNC\, \\?\GLOBALROOT. Extended-length
  local drive paths (\\?\C:\...) and plain UNC shares stay allowed.
- tests/agent/test_nt_namespace_guard.py: 10 blocked forms, 11 allowed
  forms, no-resolve proof, tool-layer chokepoint coverage.
- docs: protected-paths table in user-guide/security.md
2026-09-15 04:17:31 -07:00
teknium1 248938b4e6 fix(cron): "Script not found" says scripts are per-profile and how to fix it
Cron scripts resolve only against the job's own profile scripts/ dir (by design,
profiles never share files), so a job copied between profiles fails with a path
that looks plausible and no hint about why. The runtime error now names the
profile folder and the two fixes (copy the script, or `hermes cron edit`).

Closes #94821. Builds on #105775 (creation-time existence check, credited).
2026-09-15 04:15:16 -07:00
chelsealong 7ecbe50d98 fix(cron): validate script existence and fix profile-aware path in error messages
_validate_cron_script_path only checked that a no_agent/monitor script path
was relative and contained within scripts_dir, never that the file actually
existed — so a misplaced script registered fine and only failed at every
fire with a generic "Script not found" from scheduler_script.py, dropping
once-only jobs from jobs.json on failure.

The error messages also hardcoded "~/.hermes/scripts/" even though
resolution goes through get_hermes_home(), which is per-profile — actively
misleading users running non-default profiles into placing scripts in the
wrong directory.

Now the validator resolves the file and returns a "Script file not found"
error naming the actual resolved scripts_dir, surfacing the mistake at
creation time instead of at every scheduled fire.
2026-09-15 04:13:47 -07:00
teknium1 aaf38bb2fb test: service stubs carry the profile the pause path now reads
The fix-pass made _pause_windows_gateways_for_update read s.profile from
every discovered service gateway; two quarantine tests build services with
SimpleNamespace stubs that lacked the field the real WindowsGatewayService
always has.
2026-09-15 04:13:44 -07:00
teknium1 27df47af5e fix: count service-supervised gateways as running for per-profile cold-start
The per-profile cold-start probe took its running set from the socket-paused
ordinary gateways only. A profile whose gateway is alive under an SCM service
is skipped by the socket pause, so it looked "not running"; with an empty
current-PID list its live start attestation read as dead and the profile
landed in cold_start_profiles. On resume the service was restarted AND a
second, unsupervised gateway was spawned for the same profile.

Build the running set from every profile that had ANY live gateway at
discovery time: paused profiles, profile-mapped processes, and service
gateways' profiles.

Review finding: service-supervised running profile was cold-started beside
its restarted SCM service (double gateway).
2026-09-15 04:13:44 -07:00
teknium1 b19e5786e2 test(update-windows): spawn stubs accept the per-profile home argument
_spawn_detached now forwards home= to _build_gateway_argv so the post-update
cold-start can launch another profile's gateway; the two windows_only spawn
tests stubbed _build_gateway_argv with a zero-arg lambda and raised TypeError
on the Windows runner.
2026-09-15 04:13:44 -07:00
Hermes Agent 8fbae2813b fix(update-windows): per-profile cold-start runs after the relaunch and fails loud
- Run `_cold_start_attested_profiles` AFTER the paused profiles are relaunched,
  so a sibling that fails to cold-start can never keep the profiles that were
  running from coming back.
- A profile that stays down raises like the active-profile cold-start does;
  the merged outcome then marks the update incomplete instead of printing
  success with a gateway still offline.
- Trim the salvaged tests to the two invariants (dead-attested default beside
  a live beta is cold-started under its own home; every dead-attested profile
  is cold-started when nothing runs, active first). The "no attestation →
  token unchanged" case is the pre-existing behaviour already covered by
  test_pause_skips_cold_start_plan_when_desktop_owns_lifecycle.
2026-09-15 04:13:44 -07:00
kshitijk4poor c728e6583e fix(update-windows): per-profile cold-start probes its own home and runs after the active spawn
Review follow-ups on the per-profile obligation:
- Order: the active profile's cold-start guard is fleet-wide (any live
  gateway ⇒ done), so a sibling spawned first would have left the active
  profile down. Per-profile spawns now run after it.
- A token is built even when the active plan owes nothing (clean exit,
  autostart not installed), so a dead-attested sibling still rides on it.
- Readiness for a per-profile spawn is probed in THAT home's identity files
  (`_live_gateway_pids(home=)` → `get_running_pid(home/"gateway.pid")`), not
  the fleet: a still-running sibling no longer vouches for a dead spawn. The
  wait/confirm helpers share one probe function.
- The new PID is attested in the profile home (`_write_start_attestation(...,
  home=)`) so a death after the CLI exits reaches the next update, and an
  already-live profile is not spawned twice.
2026-09-15 04:13:44 -07:00
kshitijk4poor c8686224d3 fix(update-windows): cold-start every dead-but-attested profile, not only when nothing runs
`_pause_windows_gateways_for_update` built the attested cold-start plan only when the
ALL-profile running PID list was empty, so a default gateway that died after a ✓ beside a
still-running `beta` never received a cold-start obligation and stayed down after the
update (#110959, fifth review thread on #110020).

- gateway_windows: thread `home` through `_start_attestation_path` → `_read_start_attestation`
  → `_attested_pid_exited_cleanly`/`_attested_dead` → `attested_death_generation(pids, home=)`
  and `_consume_start_attestation(gen, home=)`; `_spawn_detached(home=)` builds the argv/env
  for that profile home (`--profile` derived by `_launcher_settings`). Defaults unchanged.
- update_cmd_windows: `_record_attested_cold_start_profiles` evaluates every
  `profiles_to_serve(multiplex=True)` profile that is not running (Desktop-owned installs only,
  active profile left to the existing plan so nothing is spawned twice) and records
  `token["cold_start_profiles"] = {name: generation}` — a sibling key, so `profiles[name]`
  stays an int for relaunch/verify. `_cold_start_attested_profiles` runs on resume before the
  ordinary relaunch, spawns under the profile home, waits for readiness, consumes exactly that
  generation; one profile's failure never aborts the others.
- tests: default dead-attested + beta running → token carries the obligation, resume spawns
  under the default home and consumes its marker while beta is relaunched; no attested
  profile → token and resume unchanged.
2026-09-15 04:13:44 -07:00
teknium1 ef44c1b73f fix: restore TestBridgeDispatch, trim #39797 tests, drop dead execution_guidance_text param
Why: the rebase conflict resolution in tests/tools/test_model_tools.py
deleted the unrelated TestBridgeDispatch class (3 tests from 73163e3);
it is restored verbatim from main with TestBrowserRetrievalHints after it.

The fix had five tests for one invariant: the OPENAI_MODEL_EXECUTION_GUIDANCE
check duplicated test_phantom_tool_references, and the two static-schema
"toolset-neutral" checks are now folded into test_silent_without_web_tools,
which runs _apply_dynamic_schemas over the real browser_navigate/browser_cdp
schemas so the rendered descriptions are what is asserted.

execution_guidance_text() no longer takes valid_tool_names: the guidance is
toolset-neutral, so the parameter was ignored; the single caller in
agent/system_prompt.py and its test are updated.
2026-09-15 04:13:13 -07:00
teknium1 ac63d0eea5 fix(agent): execution guidance and browser hints drop the web_search stripper; tests assert the invariant
The rebased guidance text no longer names web_search anywhere, so
execution_guidance_text()'s replace() calls (3733e4aff5) matched
nothing and were dead; the function now returns the neutral text for
every toolset and its phantom-tool test asserts "no web tool named"
instead of the removed sentence. model_tools ports the PR's hint layer
into main's _DYNAMIC_SCHEMA_REWRITERS table (browser_navigate +
browser_cdp) rather than a second pass after it.

Tests: the two browser_cdp registry tests were re-added by the PR but
main pruned them in 39975613b13b4; replaced with one schema-neutrality
invariant. Exact-wording assertions ("lightweight retrieval tool",
"appropriate permitted retrieval/search tool") were change detectors and
are dropped. tools-reference.md row updated to the new schema text.
2026-09-15 04:13:13 -07:00