Commit Graph

33269 Commits

Author SHA1 Message Date
Teknium ff76e65e14 fix(cron): a fire_claim whose same-host owner pid has exited is stale immediately
claim_job_for_fire refused any claim younger than FIRE_CLAIM_TTL_SECONDS (300s)
regardless of whether its owner was still alive, so a `hermes cron run` killed
mid-flight (timeout, Ctrl-C, OOM) blocked the next manual run with "already being
fired" for up to five minutes. The executions table already reaps dead owners on
sight; the job-record fire_claim now does the same via gateway.status._pid_exists
when the claim's `by` names a pid on this host. Foreign hosts, explicit
HERMES_MACHINE_ID values, and probe failures keep the TTL (fail safe).
2026-09-09 13:48:27 -07:00
Teknium d9e64e9165 fix(state): record display_identity/display_order in the messages schema history
#106582 (1c6683e8e0) added display_identity and display_order to the messages
table in SCHEMA_SQL without appending the matching events to
SCHEMA_HISTORY["messages"], so test_replayed_history_ends_at_current_schema
has been red on main since 17:06Z. Append the two '+' events; no runtime change.
2026-09-09 13:30:28 -07:00
Teknium 308d12e1fd fix(tests): live-system guard lets a gateway start inside a container exec
#106623 (ca16cafee4) blocks any test subprocess whose argv resolves to
`hermes gateway run|start|restart`, so the harness cannot spawn a runtime that
outlives the worker and restarts the developer's gateway. The matcher strips the
`-p <profile>` selector and reads through `sh -c`, so it also fired on
`docker exec -u hermes <ctr> sh -c 'hermes -p x gateway start'` and broke
tests/docker/test_profile_gateway.py on every Docker build since. A gateway
launched inside a container cannot reach the host unit or webhook port; the
guard now skips commands whose argv[0] is a container runtime (docker, podman,
nerdctl). Host-side spawns stay blocked.
2026-09-09 13:30:28 -07:00
Erosika 267a6b79c8 fix(codex): gate the newest-only reasoning replay on its own Azure predicate
_is_azure_foundry_responses matches the azure-foundry provider or the
project-scoped services.ai.azure.com host. #101243 proposes narrowing it
to that host alone. The multi-item rejection this branch handles happens
on resource-level *.openai.azure.com hosts too, so the pruning gate now
uses _is_azure_responses: the provider, or either Azure host.

Refs #105369
2026-09-09 13:01:48 -07:00
Erosika 2de15babf9 fix(codex): replay only the newest sealed reasoning item on Azure Foundry
The first request of every turn after the first replays the encrypted
reasoning item of every earlier response. Azure Foundry accepts one such
item and rejects two or more with HTTP 400 "Conflicting authenticated
continuation identities". In-turn requests never hit this because
_is_post_tool_replay already suppresses replay on Azure when the request
ends on a tool result.

On Azure Foundry, build_kwargs now keeps codex_reasoning_items only on the
newest assistant row that has any. include still requests
reasoning.encrypted_content, compaction checkpoints stay on every row, and
the canonical messages are not mutated. Other Responses endpoints are
unchanged.

Rebuilding a failed session from state.db through the transport and
sending it live: unpatched, 5 reasoning items, 400; patched, 1 item, 200.

Fixes #105369
2026-09-09 13:01:48 -07:00
Erosika 5db0df28de fix(classifier): treat Azure Foundry's continuation-identity 400 as a rejected reasoning replay
Azure Foundry (gpt-6-astra) answers a request that replays encrypted
reasoning from more than one prior response with HTTP 400 "Conflicting
authenticated continuation identities", code invalid_value. The classifier
mapped that to a non-retryable client error, so the turn aborted and every
later turn in the session failed the same way.

Map the message to invalid_encrypted_content. The existing recovery in
turn_recovery then strips the replay state and retries once.

Refs #105369
2026-09-09 13:01:48 -07:00
Teknium 6f3e630b47 fix(auth): honor Desktop-saved model.key_env for registry providers
The Desktop settings UI persists a registry provider's API key as a
credential pointer -- model.key_env in config.yaml pointing at an env var
in $HERMES_HOME/.env (e.g. HERMES_CUSTOM_LMSTUDIO_API_KEY) -- while
keeping model.provider on the registry id. _resolve_api_key_provider_secret
resolved registry providers exclusively from
PROVIDER_REGISTRY[...].api_key_env_vars, so the UI-saved key was silently
ignored; for lmstudio the runtime then substituted the no-auth placeholder
(dummy-lm-api-key) and auth-enabled LM Studio servers rejected every
request with an opaque, retryable-flagged 401.

Fix: consult model.key_env (only when config.yaml's main model targets the
provider being resolved, so the pointer never leaks across providers)
before the registry env vars, running the value through the same
_usable_declared_secret prefix validation. Precedence for the canonical
env vars and the credential pool is unchanged; a valid canonical key still
wins when no pointer is set.

Complements #75373, which covers providers.<id>.key_env at the provider
level; the Desktop save path writes the pointer at the model level, which
that PR does not consult.

Fixes #106336
2026-09-09 12:33:15 -07:00
teknium1 76c8866ba5 fix(tui-gateway): point the sessionless model rejection at Settings -> Models
Carry the one hunk PR #106424 adds over #106405: the 4001 message names the
door that deliberately changes the profile default, so a stale client's user
sees where to go instead of a bare "requires a live session". The flag
pre-parse from #106424 is not carried: with #106405's guard, sessionless
`--once --global` already surfaces the canonical conflict error through
_apply_model_switch (verified in-process).

Co-authored-by: mkrb84 <192544766+mkrb84@users.noreply.github.com>
2026-09-09 12:26:33 -07:00
KoNit-K 75b1b43ec1 fix(tui-gateway): reject sessionless config.set model with 4001
Sessionless model switches (including legacy --global) could persist
profile defaults before session.create; fail closed unless --once.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 1c4a29757bbde1304404f26b45b6becb841c069f)
2026-09-09 12:26:33 -07:00
Teknium 474143da81 fix(desktop): trim the auto Show-earlier gate and prove it end to end
Drop the injectable `topEdgePx` knob (one caller, one constant) so the
threshold has a single home, and fold the two-branch tail of
shouldAutoShowEarlier into one predicate. Comments keep the WHY: wheel is
the only signal at a clamped scrollTop 0, and the gates name the states
where scrollTop is near 0 without meaning "read earlier".

Tests: collapse the 11-case matrix into one invariant test on the pure
gate, and add a rendered-list test that mounts the real Thread against a
transcript heavier than RENDER_BUDGET, escapes the bottom lock, wheels up
at the clamped top and asserts more turn groups mount — while an upward
wheel mid-transcript mounts none. Red on origin/main (28 groups stay 28),
green with the fix.
2026-09-09 12:23:20 -07:00
KoNit-K 3ec8042483 fix(desktop): page earlier transcript on top-edge scroll
Long conversations and branched sessions hide older turns behind the
render budget, but only the Show earlier button loaded them. Auto-page
through the same showEarlier path when the reader is at the viewport top.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 12:23:20 -07:00
hermes-seaeye[bot] 15f3037647 fmt(js): npm run fix on merge (#106818)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-09 19:21:46 +00:00
Teknium 7d4033c391 test(tui_gateway): fold the watermark check into the oversized-frame test
Salvage bar is at most two invariant tests per fix. The monotonic-watermark
case (small, oversized, small -> is_truncated(s, 1)) is the same contract the
oversized-frame test already pins, so it lives there as a final block instead
of a third test function.
2026-09-09 12:20:38 -07:00
Teknium c8b1c049c7 fix(tui_gateway): size replay frames outside the lock; document the byte ceilings
transport.write serializes outside its lock so one large payload cannot block
other threads' frames; the replay ring is on the same hot path (write_json), so
its sizing json.dumps must follow the same rule instead of holding _replay_lock
for the whole encode. seq is not stamped at that point, which is a few bytes off
a MiB budget and irrelevant.

Also restore the original WHY for the count limit, explain the byte ceilings
(512 x 64 KiB tool results is ~32 MiB per session before the cap; 4 MiB per
session, 64 MiB per process) and make the module docstring state the bound in
bytes and the truncation watermark.
2026-09-09 12:20:38 -07:00
Xipong d31f0a647d fix: preserve streaming and replay safety after parent review 2026-09-09 12:20:38 -07:00
Xipong 721128ac02 fix: bound event replay payload bytes 2026-09-09 12:20:38 -07:00
Teknium f7a973e3d7 ci(lint): diff the PR head ref so the public-surface deepen loop can actually find a merge-base
The advisory step ran `git merge-base origin/main HEAD` where HEAD is the
checked-out refs/pull/N/merge commit. GitHub recomputes that synthetic merge
commit whenever main moves, so by the time the job runs the checked-out SHA is
often no longer reachable from any ref. `git fetch --deepen` on an
unreachable commit can never pull in its parents, so all four fetches in the
loop (200 + 3 x 1000) ran to completion, burned 4m15s, and still ended in
"public-surface: no merge-base" (run 34285107265, job 102259026802). The
step-level timeout keeps that overrun from cancelling the blocking job; this
commit makes the step finish on the first fetch instead.

Fetch refs/pull/N/head explicitly (always reachable server-side) and diff
that ref against the base; the merge commit's own content is irrelevant to a
symbol diff.

Raise the step timeout from 2 to 3 minutes: one --deepen=200 fetch took ~56s
and a --deepen=1000 ~67s on the runner, and main gains ~170 commits a day, so a
branch a day old legitimately needs both. 45s of blocking steps + 3 min still
fits the 5-minute job budget.
2026-09-09 12:19:41 -07:00
PRATHAMESH75 981dc9c367 ci(lint): bound the advisory public-surface step so it can't time out the blocking footguns job (#106103)
The 'Public-surface diff vs base (advisory)' step already carries
continue-on-error: true, so its own failure never fails the Windows-footguns
job. But continue-on-error does not shield the job-level timeout-minutes: 5:
when the deepen/fetch loop runs long (a distant or missing merge-base), the
step eats the whole job budget and the job is cancelled at ~5m even though the
blocking footgun/compat checks passed and the public-surface check is advisory.

Give the advisory step its own timeout-minutes: 2. A step overrun is then
killed and, via continue-on-error, kept off the job outcome the same way a
step failure already is, so the blocking job can finish under its budget.
Fixes #106103.
2026-09-09 12:19:41 -07:00
Teknium b7bef04861 fix(kanban): an explicit scratch workspace never inherits the board's project
Move the "explicit scratch means no project" decision into the one resolver
every surface funnels through, `kanban_db.create_task`: board-project
inheritance now runs only when the caller left `workspace_kind` open
(`None`), and `workspace_kind` defaults to scratch after that check. The
tool handler keeps the #106347 fix for `project=""` (no `or` collapse) and
`board=` scoping but drops its handler-local sentinel logic, since the
resolver now owns the rule; the `self_task` project inheritance for
dispatcher-owned workers is unchanged.

Sibling surfaces had the same bug through the same line and are fixed by
the same change:
- CLI `hermes kanban create --workspace scratch` on a project-scoped board
  produced a project worktree; `--workspace` no longer defaults in
  argparse so the resolver can tell "omitted" from "scratch".
- Dashboard `POST /tasks` with `workspace_kind: "scratch"` did the same;
  `CreateTaskBody.workspace_kind` defaults to `None` for the same reason.
- `kanban_swarm.create_swarm` threads `None` through for consistency.

Tests: one resolver invariant in test_kanban_board_project.py (explicit
scratch stays scratch, omitted still inherits) and the salvaged tool test
folded into a single parametrized matrix over scoped/unscoped target boards.
2026-09-09 12:18:58 -07:00
KoNit-K dc5c87c6c1 fix(kanban): honor explicit scratch/project on MCP kanban_create
Pass the caller's board into create_task and treat explicit workspace_kind=scratch or empty project as no-project so ambient board project_id cannot override MCP args.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 12:18:58 -07:00
Teknium 544169a41a perf(desktop): stop per-event full-timeline allocations on streamed tool events
Every tool.start / tool.progress / tool.complete ran three whole-timeline
passes that each allocated per part: `completeOpenStreamParts` (`map` with a
spread per row), the pending-row scan (`map` → `filter` → `map`, one wrapper
object per part), and `generatedImageEchoSources` (`flatMap` + a throwaway
array per non-image row) — then `dedupeGeneratedImageEchoesInParts` copied
the array again even when it stripped nothing, handing React a fresh
identity per event. Replace them with single in-place loops, and return the
input array from the dedupe when there are no echoes so a no-op stays a
no-op for the store.

Measured on the real `useMessageStream` hook (vitest/jsdom, 2,000 starts +
2,000 completions interleaved with text deltas): 618 ms → 138 ms; the
tool-only 2,000×2 case 252 ms → 67 ms. The remaining O(n) is one
`findIndex` per event over a ~4k array (~8 µs), well under the cost of the
copy React needs anyway, so no per-stream id index is introduced.

Behaviour is unchanged: same first-match `toolCallId` resolution, same
sparse/no-id contextual correlation, same ordering of parts.

Co-authored-by: Xipong <217837358+Xipong@users.noreply.github.com>
2026-09-09 12:13:14 -07:00
Xipong 7b639164cf fix(desktop): parallelize roster source requests 2026-09-09 12:13:14 -07:00
teknium1 a02a3b9808 test(desktop): trim MCP polling tests to two invariants per fix
Health checker: keep the 12-reconnect coalesce test and replace the
stop/disconnect drop rows with the invariant #106135 names — one fresh
follow-up still runs when the active sweep fails through the handled
config-error path (13 config reads on main, 2 after the fix). Log poller:
keep single-flight + resumed cadence, and shrink the replacement-lifecycle
test to "stop suppresses a late publish and leaves no timer". Both files
were run against origin/main source and failed (11 / 13 / 13 requests).
2026-09-09 12:11:25 -07:00
teknium1 8ac2c147a5 refactor(desktop): drop the serial-task layer from the MCP log poller
startCompletionPoll already keeps the pane single-flight and its stop
function already suppresses a late publish, so createSerialTask (a second
queue that never forwarded its AbortSignal to getLogs) and the AbortError
re-throw inside the poll callback were defense-in-depth on top of the fix.
A profile switch now tears the old lifecycle down and starts the new one
immediately; at most one stale read can still be in flight, and its result
is discarded. Replace the AbortController with a plain stopped flag.
2026-09-09 12:11:25 -07:00
Xipong d93988233c fix: serialize MCP log polling 2026-09-09 12:11:25 -07:00
Xipong 3fbc408ff6 fix: preserve event delivery across scheduler lifecycle boundaries 2026-09-09 12:11:25 -07:00
Xipong 42f2067092 fix(desktop): coalesce MCP reconnect sweeps 2026-09-09 12:11:25 -07:00
Teknium 85e423482a fix(tools): kill the browser_exec CLI tree on timeout on Windows too
The salvaged fix only ran the CLI in its own session on POSIX and kept
plain subprocess.run on Windows — the one platform where the wedge in
#106244 is actually reproducible: CPython's run() retries an unbounded
communicate() after kill() there, so a grandchild holding the capture
pipes blocks the worker forever. On POSIX run() wait()s the PID and
returns promptly; the grandchild merely leaks (live-reproduced on Linux).

One code path for both platforms:
- _group_popen_kwargs: start_new_session=True on POSIX,
  CREATE_NEW_PROCESS_GROUP + hide flags on Windows (replaces the
  hide-only _windows_popen_kwargs).
- _kill_cli_process_group: os.killpg SIGKILL on POSIX, taskkill /T /F
  on Windows (same kwarg set as the sibling taskkill sites).
- The Popen decodes with encoding="utf-8", errors="replace" like every
  other subprocess call in this file (windows footgun rule).
- Drain test patches the kill helper instead of os.killpg so it runs on
  every host.

Windows behaviour is not live-verifiable on this Linux host.
2026-09-09 12:11:13 -07:00
Teknium 6788c3224f test(tools): trim the browser_exec group-kill tests to two invariants
Drop test_gone_group_still_surfaces_timeout: the killpg-race branch is a
contextlib.suppress and the test only pinned the exact communicate()
timeout sequence (a change-detector). The real-grandchild test and the
bounded-drain test remain — the two behaviour contracts of the fix.
2026-09-09 12:11:13 -07:00
liuhao1024 7416201baf fix(tools): exempt POSIX-only killpg from the Windows footgun scan
The group kill lives behind browser_exec's os.name != "nt" branch, so
os.killpg/SIGKILL are never reached on Windows; mark the line per the
scan's suppression convention.
2026-09-09 12:11:13 -07:00
liuhao1024 60debff28d fix(tools): kill the whole browser-use CLI process group on browser_exec timeout
subprocess.run only kills the direct CLI child on TimeoutExpired; a
browser_harness daemon / Chrome helper grandchild inherits the stdout/
stderr pipes and keeps them open, so the internal communicate() blocks
on pipe EOF forever. The wedged tool call never returns, its activity
heartbeat keeps stamping last_activity_at every 30s, and the session is
pinned at "now" in the desktop sidebar indefinitely (#106244).

On POSIX, run the CLI in its own session (start_new_session=True) and
SIGKILL the whole process group on timeout, then drain the pipes under
a bounded deadline. Windows keeps subprocess.run.
2026-09-09 12:11:13 -07:00
Teknium d31318745c test(desktop): trim salvaged fixtures to the invariant tests
Drop the two companion tests from #106124 and #106099 that already pass
on main without their fixes (a pinned active transcript is never measured
because prune() skips it before weighing; a rerendered effect already
discards the previous closure). Keep the one red-on-main invariant per
fix: no re-serialisation of unchanged warm transcripts, and change ticks
coalescing behind an in-flight session.active_list request.
2026-09-09 12:09:00 -07:00
Xipong 3d4c537584 fix(desktop): coalesce pending live status refreshes 2026-09-09 12:09:00 -07:00
Xipong 3bfd7b653b fix: cache warm transcript weights 2026-09-09 12:09:00 -07:00
Xipong 5a2ccffd41 fix(desktop): skip distro probe for WSL drive paths 2026-09-09 12:09:00 -07:00
Xipong ef677265d2 fix(desktop): reuse detected SSH platform 2026-09-09 12:09:00 -07:00
Teknium 14a6a75ebe test(agent): trim Codex patch-budget tests to two invariants
Replace the six cap-mapping unit tests (exact 5536 px value, floor, Anthropic
path, two None cases, worst-case tile math) with one behavioral test: the cap
derived from the real Codex rejection makes the production shrink pass rewrite
a real 5444x6200 PNG Responses ``input_image`` part to within the 30000-patch
budget. That is the user-visible contract; the exact pixel value is an
implementation detail. Keeps the classifier test as the second invariant.
2026-09-09 12:06:53 -07:00
liuhao1024 cabe430a25 fix(agent): derive tile-aware shrink cap from Codex patch-budget 400s
The classifier fix routes Codex patch-budget 400s into the shrink
recovery, but _image_error_max_dimension still returned None for the
Codex wording, so the recovery fell back to the 8000 px default cap
and skipped images between ~5542 and 8000 px that already exceed the
30000-tile budget — burning the single shrink retry without shrinking
anything. Parse the reported patch limit and convert it to a per-side
pixel cap of isqrt(limit)*32 (5536 px for 30000 patches), which keeps
a square image under the budget.
2026-09-09 12:06:53 -07:00
liuhao1024 d3515a9d18 fix(agent): classify Codex patch-budget image 400s as image_too_large
OpenAI Codex Responses rejects an image whose tile-patch budget exceeds
its 30000-patch ceiling with wording ("requires N patches after
processing, exceeding the limit") that contains none of the existing
image-size vocabulary, so the 400 fell through to format_error
(non-retryable). The reactive image-shrink recovery in turn_recovery was
therefore bypassed and the session kept failing — or failover re-sent
the identical oversized image to another model.

Route the patch-budget wording to image_too_large (retryable) so the
existing shrink pass re-encodes the image under the ceiling and retries.

Fixes #106337
2026-09-09 12:06:53 -07:00
teknium1 554eb9c217 chore: map contributor email for ten82e (PR #106290 salvage) 2026-09-09 11:51:00 -07:00
teknium1 53b9615ea6 test(hermes_cli): keep one opencode-go delisting invariant; record the delisting beside the keyed-suffix set
Drop the floor-pin test from #106290 (a snapshot of the static table); the
end-to-end merge test through provider_model_ids with the real curated floor
already fails if the slug is re-added. Note beside
_OPENCODE_FREE_KEYED_SUFFIX_MODELS why ox-alpha-free stays excluded from the
keyless catalog even though it left the curated floor.
2026-09-09 11:51:00 -07:00
ten82e b2cae54035 fix(hermes_cli): drop delisted ox-alpha-free from opencode-go curated floor
The Go relay (GET /zen/go/v1/models) delisted ox-alpha-free 2026-09-09, but
_PROVIDER_MODELS["opencode-go"] still carried it. _profile_live_catalog merges
the curated floor into the live list (live-first for opencode-go), so the
model picker kept offering a model that now 401s — the same failure class as
#95914 (opencode-free / x-preview-f-free).

Remove it from the floor and add two regression tests: an end-to-end merge
test through provider_model_ids with the real floor (fails if a stale floor
resurrects it) and a floor-pin test asserting the known-delisted model stays
out of the offline fallback.

(cherry picked from commit 091fd85865caa09e828928f93ee985dae7e9580d)
2026-09-09 11:51:00 -07:00
teknium1 b27c6f7f0d docs(line): document LINE_EXPIRED_TEXT beside the other postback reply knobs 2026-09-09 11:47:55 -07:00
Teknium b21231fef8 test(line): trim #106446 regression suite to the two reporter reproductions
Keep exactly the issue's Repro A (heartbeat must not become the cached answer)
and Repro B (stale READY mapping / expired button must not swallow the next
answer); both fail on origin/main and pass with the fix. The other six tests
from #106470 (config override, PENDING-tap guard, per-layer variants) were
either change-detectors or subsumed by these two.
2026-09-09 11:47:55 -07:00
Tranquil-Flow 77c994bab2 fix(line): keep progress heartbeats and stale mappings out of the postback cache (#106446)
(cherry picked from commit 4b0dc3c58cfe9771745384f20d75ca48d9b25408)
2026-09-09 11:47:55 -07:00
teknium1 3dc3809d07 refactor(fal): render the managed billing message once in fal_common
Image and video callers were each formatting the same four-field dict into
the same sentence. Return the rendered tail from _managed_fal_billing_error
so the wording lives in one place; output is byte-identical.
2026-09-09 11:46:36 -07:00
teknium1 acf9177c70 test(fal): trim the billing-409 salvage to two invariant tests
Keep the two tests that fail on main without the fix:
- test_fal_common: a keyed managed submit makes exactly one POST (plus the
  negative arm: an unkeyed submit still goes through the SDK retry ladder)
- test_image_generation: the 409 BILLING_ERROR body surfaces
  `unsupported_pricing_meter` instead of the generic "not yet enabled" text

Dropped from #106484: the duplicate video-plugin billing test (same helper,
same assertion), the `_fal_client = fake` / `import_fal_client` stub churn
and the `tools.lazy_deps` stub — fal-client is installed in CI (`--extra fal`)
so those fixtures were not needed; the `_load_fal_client` no-op fixture on
TestManagedGatewayErrorTranslation for the same reason.

Also drop the redundant `retry_request is None` re-check in
`_ManagedFalSyncClient.submit` — `__init__` already raises when the helper
is missing.
2026-09-09 11:46:36 -07:00
Matt Earls 0c6b94e499 fix(image-gen): preserve managed FAL billing errors
Avoid retrying idempotent managed FAL submissions because the retry can mask the initial billing failure. Surface structured Nous billing diagnostics consistently for image and video paths, with hermetic regression coverage.

(cherry picked from commit 289ce039e9a522dc8016ae4a512214c05d0a8bc0)
2026-09-09 11:46:36 -07:00
Teknium 68bc4b216e fix(agent): stop length continuation when the prompt filled the context window
finish_reason='length' has two causes: the answer was long (max_tokens reached),
or the prompt itself left no room to generate. _continue_text treated both the
same: append the fragment + a continuation nudge and retry, up to 4 times. In the
second case every retry sends a strictly longer prompt, so each attempt is worse
(Ollama n_ctx=32768: 32,638 -> 32,685 -> 32,732 prompt tokens, all truncated), the
user is told "model hit max output tokens", and max_tokens is not the lever.

The response's usage already carries prompt_tokens and the compressor already
resolves the model's context window; compare them once per truncation. Under
_MIN_CONTINUATION_HEADROOM (512) free tokens the turn ends on the first
truncation, keeps the partial text, names the context window as the cause and
points at /compress or a larger window. Unknown usage or window keeps today's
behaviour. max_tokens semantics untouched.

Co-authored-by: gaoanze888 <214786078+gaoanze888@users.noreply.github.com>
Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
2026-09-09 11:42:43 -07:00
Teknium 95cfcba353 test(pty): trim the reap-idempotence regression to the two interleavings
Drop test_reap_idle_closes_doomed_and_keeps_attached from #106415: it
pins reap_idle's pre-existing selection behaviour (dead/idle reaped,
attached kept), which the fix does not change, and is not red on base.
Fold the duplicated two-idle-sessions + gated-close setup into one
helper shared by the two interleaving tests that do fail without the
fix (reap vs reap, close_all vs reap).
2026-09-09 11:31:10 -07:00