Commit Graph

46548 Commits

Author SHA1 Message Date
Brooklyn Nicholson
6489ef982c fix(desktop): route Ctrl+W to the shell over an interactive terminal
Main claims Cmd/Ctrl+W on every window (window-accelerator's
before-input-event + preventDefault, kept for the #105498 swallow
guard and the Win/Linux close path) and routes it to the renderer via
hermes:close-preview-requested; the renderer's close rung maps any
focused [data-terminal] to closeActiveTerminal(), so the chord closed
the pane and killed the live shell instead of reaching xterm as
readline unix-word-rubout (#65457).

Route it the way #108550 routes Cmd/Ctrl+R: keep main's claim exactly
as-is, and add the missing terminal rung in the renderer.

- TerminalInstance (the user PTY) gains [data-interactive-terminal];
  the read-only agent mirror keeps only [data-terminal], so its tab
  stays deliberately closeable (the #65524 breadth complaint).
- The terminal handle registry (terminal-context-menu) keys by the
  [data-terminal] scope rather than the nested xterm host — the same
  fix #108550 carries — and grows a wordErase verb: the user terminal
  re-delivers the ^W byte to its PTY, the agent mirror's is null.
- The close-preview IPC rung (use-desktop-integrations) and the
  view.closeTab keybind dispatch (use-keybinds) both consume the
  bare platform chord through that verb instead of closing; Shift /
  Alt / rebound variants keep closing.
- The non-mac Window>Close menu item becomes click-only
  (registerAccelerator: false): the 'close' role's default
  CommandOrControl+W accelerator would claim the chord before the
  before-input-event run. Clicking it still closes the window.

Co-authored-by: Kyssta <218078013+kyssta-exe@users.noreply.github.com>
Co-authored-by: percival <ignacio@agenticolabs.io>
2026-09-30 23:04:06 -05:00
Brooklyn Nicholson
dcf3d8981d test(desktop): shard the uploadComposerAttachment tests out of use-prompt-actions
Part of #79915

Shards the two direct-call uploadComposerAttachment describes out of
use-prompt-actions/index.test.tsx (6256 -> 6180 lines) into
upload-composer-attachment.test.ts (105 lines). The moved cluster needs
neither the Harness component nor sessionInfo: it calls
uploadComposerAttachment directly with an injected requestGateway and a
stubbed window.hermesDesktop.readFileDataUrl. The new filename follows the
sibling shards in that directory (single-flight-resume.test.ts,
rewind.test.ts, utils.test.ts).

Rebuilt against current main: the old preview reuse cluster from the
source PR no longer exists on main (behavior change in #93324 rewrites it
as the image cache contract describe with opposite semantics), so the
shard moves the current direct-call cluster byte-for-byte (parent diff is
a pure deletion).

Test parity: 138 -> 136 + 2, all green.

Co-authored-by: finn763 <165816600+finn763@users.noreply.github.com>
2026-09-30 23:03:19 -05:00
Brooklyn Nicholson
9b761903b9 fix(tts): silence identifier-dense tokens in both speech normalizers
Filenames with extensions, hashes, UUIDs, dense model/version IDs and
paths were spoken character by character ('peyton-sample-20260922.wav'
as 'peyton dash sample dash two zero two six...'), making otherwise
clean synthesized audio sound corrupted (#119207).

One identifier-to-speech pass, mirrored token-for-token in
tools/tts_text_normalize.py (prune_identifier_tokens_for_tts, wired
into prepare_spoken_text after the symbols pass) and
apps/desktop/src/lib/speech-text.ts (pruneIdentifierTokens, in
sanitizeTextForSpeech after code fences/links/URLs/MEDIA tokens are
consumed). Identifier-dense tokens become silence — never a hardcoded
English placeholder word, per the #86602 lesson (the reply may be in
any language). Ordinary prose, email addresses, dates ('2026/06/02'),
ratios ('3:2'), prices and short hyphenated words (COVID-19,
well-known) pass verbatim.

Also fixes a pre-existing smooth_whitespace_for_tts bug the corpus
surfaced: 'user@example.com' was read as 'user@example. com' because
the punctuation-space pass split any letter-follows-punctuation pair;
it now only splits when the punctuation is not glued to a word
character on the left.

The two implementations are pinned to the same corpus
(tests/fixtures/identifier_speech_corpus.json), asserted by both
tests/tools/test_tts_identifier_speech.py (pytest) and
apps/desktop/src/lib/speech-text.identifier-corpus.test.ts (vitest).

Builds on the desktop-half approach of PR #119212 (which leaves the
gateway/CLI normalizer in tools/tts_text_normalize.py untouched) and
extends it to both halves.

Co-authored-by: KoNit-K <konit.block@protonmail.com>
2026-09-30 23:02:33 -05:00
Brooklyn Nicholson
44b1464809 fix(desktop): sort imports in gateway-event lifecycle 2026-09-30 23:02:01 -05:00
Brooklyn Nicholson
d0640d0c44 test(desktop): flip the failure off before the context-meter retry fires
The recovery test advanced the 1s retry timer before clearing the
failure flag, so retry 1 re-failed and no successful fetch ever landed;
it passed only by reading the cached pre-turn snapshot. Main's #70871
busy-toggle now evicts that snapshot, exposing the ordering. Flip the
failure off first so the assertion reads a real retry fetch.
2026-09-30 23:02:01 -05:00
gilfoyle
0dbfa8ae9c fix(desktop): refetch the context meter when compression completes without a turn (#94001)
Sandbox verification of the first fix exposed the remaining gap: manual
/compress runs the session.compress RPC OUTSIDE any turn, so busy never
flips and useContextBreakdown's effect deps (attempt/busy/enabled/
requestGateway/sessionId) never change — the meter kept serving the
pre-compression breakdown (441.2k/1M) after the transcript shrank 1220 →
202 messages (~5.7x), until the status-bar item was unmounted and
remounted, which reset useState and forced a fresh fetch.

Add an invalidation seam: invalidateContextBreakdown(sessionId) bumps a
per-session generation and notifies subscribers; the hook subscribes for
its own session and re-runs the fetch effect on a bump. Two call sites:
slash.ts after a successful session.compress (the no-turn path), and
lifecycle.ts on session.reclaimed (belt-and-braces with the retry ladder
— invalidation makes the refetch immediate, retries cover failures).

Also verified live: the pre-compression value was wrong by 5.7x after
compression, and unmount/remount of the footer item (the user's manual
workaround) is exactly the cache-reset the fix now does automatically.
2026-09-30 23:02:01 -05:00
gilfoyle
32f146b07e fix(desktop): recover the context meter after compression instead of freezing stale (#94001)
useContextBreakdown made one refetch attempt per busy toggle, swallowed
failures, and kept serving the cached pre-compression breakdown whenever
fetched.sessionId still matched (it always does). After compression plus
a session.reclaimed resume, that single refetch races the agent rebuild:
a 4001 is swallowed and the meter freezes at the pre-compression
percentage; an in-flight rebuild answers a zeroed breakdown
(context_max: 0, the agent-is-None branch), which blanks the meter and
caches the zeros. No dep changes after the failed attempt, so nothing
ever retries — recovery required manually switching sessions.

Treat an untrustworthy answer as a transient window, not data: zeroed
breakdowns are never cached; failed fetches retry on a bounded backoff
(1s/4s/12s, re-running the fetch effect via an attempt counter); on
ladder exhaustion the cached breakdown is evicted so the meter goes dark
honestly rather than showing numbers known to be stale. Session switches
reset the retry state; the pending backoff timer is cancelled by the
fetch effect's own cleanup.

Fixes #94001 (scenarios 1, 2, 4).
2026-09-30 23:02:01 -05:00
liuhao1024
1118b0ac47 fix(desktop): collect citation anchors only inside the Sources section
Tighten collectSourceListIds() to the bundled grounded-citations
skill's own _split_draft() boundary: find the last recognized Sources
header (## Sources or plain Sources:), then collect only [n] http(s)://
entries in the section it opens, skipping fenced code. A bare [n]
marker now survives the orphan strip exactly when this response lists
a matching source entry — ordinary [7] todo prose lines or fenced
examples no longer anchor anything.
2026-09-30 23:00:20 -05:00
liuhao1024
9ab86e6584 fix(desktop): keep source-anchored citation markers through markdown preprocessing
The orphan-marker strip deleted every bare [n] that follows prose, so
grounded-citations output lost its claim-to-source links in Desktop. A
numbered source-list entry ([n] url at line start) now anchors the same
number in prose: anchored markers survive, orphan markers are still
stripped.
2026-09-30 23:00:20 -05:00
joaomarcos
b797ed9779 fix(agent): cancel background review when session ends or is stopped
Deleting a desktop session (or hitting /stop) did not stop its
background memory/skill review fork. The review is a SEPARATE AIAgent
instance, tracked only on the parent's _background_review_agent /
_active_children (agent/background_review.py) -- never on
session["running"] or session["_run_thread"]. That made it invisible
to every teardown/interrupt path:

- _teardown_popped_session's run-thread join only waits on the
  FOREGROUND turn's _run_thread.
- session.interrupt's _interrupt_session_turn only calls
  request_hard_interrupt when session["running"] is True -- the
  common shape is the foreground turn already finished and only the
  post-turn review is still running, so this branch never fires.

In the reported incident the review degraded into a failed-tool-call
retry loop (malformed arguments from a fallback local model) and kept
calling the model for 70+ minutes after the session was deleted, only
stopping on a full app restart.

Fix: call the existing
agent.background_review.cancel_background_review_for_live_turn
helper -- previously invoked only when a NEW live turn preempts a
running review -- from both:

- _finalize_session (tui_gateway/session_lifecycle.py), the single
  _finalized-guarded chokepoint every teardown path (explicit close,
  idle-timeout reap, ws-orphan reap, shutdown) funnels through, and
- _interrupt_session_turn, so an explicit /stop reaches a live review
  even when the foreground turn has already ended.

cancel_background_review_for_live_turn gained optional
message/tool_reason kwargs (additive, backward compatible) so the new
callers report the real cause ("session ended (idle_timeout)",
"session interrupted") instead of the misleading default "superseded
by a new live turn" wording inherited from the live-turn-preemption
caller.

Two new regression test files cover the finalize path and the
/stop-only-review-running sibling path; both were proven to fail
(RED) with the fix reverted and pass (GREEN) with it applied.

Fixes #102895

Note on related PRs: #75616 and #102911 (duplicate of #75616) fix a
different, adjacent gap -- the Desktop renderer not calling
session.interrupt before session.close. That closes the window where
the FOREGROUND turn itself outlives deletion, but does not touch this
review-fork gap: in the reported log the foreground turn had already
finished (finish_reason=stop) before the review started, so
session["running"] was already False and session.interrupt's
running-gated branch would not have helped even with those PRs
applied. This fix is complementary, not a duplicate.
2026-09-30 22:59:48 -05:00
PRATHAMESH75
9c2c1855aa fix(desktop): drive the pet from any working session, not just the foreground one (#84282)
`toolRunning` / `reasoning` are pushed to `$petActivity` only from the
per-session message-stream hook (`use-message-stream/gateway-event.ts`), which
is mounted for the foreground session alone. A background session running an
agent task therefore never reached `$petActivity`, so the floating pet sat in
`idle` for the whole run — only the visible session drove its pose.

Mirror the pattern the `waiting` pose already uses: `awaitingInput` works across
sessions because it is derived from the store-level `$attentionSessionIds`
subscription in use-pet-bridge, which is always live regardless of which session
is rendered. Add the twin for busy state — subscribe to `$workingSessionIds`
(the computed over every session's authoritative `busy` flag) and mirror it into
`$petActivity.busy`. `derivePetState` maps `busy` to the `run` pose, so any
working session — foreground or background — now animates the pet, while the
foreground stream's finer `toolRunning` / `reasoning` signals still refine
run-vs-review for the visible session.

Extract `syncPetBusyFromSessions()` as a pure function (mirroring
`rehydrateLiveSessionStatuses`) so the cross-session behavior is unit-tested:
a background-only working session shows `run`, a single finish among several
keeps `run`, and full rest returns to `idle`.

Fixes #84282
2026-09-30 22:59:02 -05:00
Brooklyn Nicholson
e9e07a6715 chore: map contributor email 2026-09-30 22:58:27 -05:00
Brooklyn Nicholson
2fd423740f fix(desktop): the tile restore retries a stale durable target too (#107593)
Re-exports isStaleTargetError from use-prompt-actions and adds the same
refresh-transcript + recompute + one retry recovery to the session tile's
restoreToMessage verb that the primary chat's restore gained, so the tile
surface named in the report no longer dead-ends on 4018.
2026-09-30 22:58:27 -05:00
Mahgoub Soliman
7242cb7a7c fix(desktop): retry stale restore targets 2026-09-30 22:58:27 -05:00
Brooklyn Nicholson
e6121060c5 test(desktop): pin the draft-text swap to the pre-paint layout phase
The review on #62586 (carried through #128476) noted the existing
assertion runs after flushSync returns, proving the eventual draft swap
but not that the previous draft could not be painted first. The merged
#59305 attachment test pins the layout-phase guarantee for chips only.

Add the text counterpart: a useLayoutEffect probe observes the
composer's mirrored draft (draftRef) in the layout phase of session B's
switch render — the last moment before the browser may paint — and
asserts it already holds B's text. Deterministic: flipping the swap
effect to a passive useEffect makes this test fail with A's text, green
with useLayoutEffect.
2026-09-30 22:56:49 -05:00
David Metcalfe
ebb1f1e279 fix(desktop): stop web-haptics audio fallback to prevent CoreAudio stutter
Fixes #68894

web-haptics' `debug` option is its desktop audio fallback: with
navigator.vibrate present (Chromium exposes a no-op on desktop), the
library plays synthesized click sounds through an AudioContext whenever
debug is enabled. On macOS that briefly seizes the audio device,
interrupting other playback (Spotify, Apple Music, Bluetooth streams) on
every prompt submit and stream start.

Desktop has no vibration motor, so haptic output is inert here — but keep
the provider and the persisted titlebar mute control wired, and narrow
the fix to omitting `debug` (default false). Also drop the idle
AudioContext warmup, which constructed an audio context of its own.

Adds provider-level regression tests: no AudioContext is constructed on
trigger or mount, a live trigger is registered while unmuted (null while
muted), and haptics are suppressed while muted.
2026-09-30 22:54:19 -05:00
UniversePeak
872b82b71f fix(desktop): route reactions through stored session fallback 2026-09-30 22:54:03 -05:00
Brooklyn Nicholson
29b951d28c test(desktop): cover dual-store planner reads in usePromptActions
The stale-closure suite seeds only the global $messages mirror, so it
exercises the ?? $messages.get() fallback path. Active sessions publish
their transcript into $sessionStates[runtimeId].messages while the
global mirror stays empty/divergent — add focused tests pinning the
primary path for reloadFromMessage, restoreToMessage, and editMessage.

Ported from #73608 (commit 6ff8426) and adapted to the current
prompt.submit wire contract (confirm_truncate +
truncate_before_message_id). Red-first verified on origin/main: all 4
new tests fail without the fix, 141/141 pass with it.

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-09-30 22:49:53 -05:00
Brooklyn Nicholson
7c1f018f40 fix(desktop): read session messages from $sessionStates, not stale global $messages
planReload/planRestore/planEdit in usePromptActions read $messages.get()
(the global draft mirror), but active sessions store their transcript in
$sessionStates[runtimeId].messages. The global mirror is empty when a
session has a runtime ID, so findIndex returns -1 and the functions
silently fail — refresh, restore, and edit buttons do nothing.

Read from $sessionStates[sessionId]?.messages when available, falling
back to $messages for draft (no-runtime) sessions.

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>

Ported from #73608 (the branch itself diverged too far from main to
rebase cleanly).
2026-09-30 22:49:53 -05:00
Brooklyn Nicholson
b415d38c25 fix(desktop): suppress the pet overlay pop-in echo on quit so the popped-out state survives restart (#55920)
On quit, main.ts closes the pet overlay window and its 'closed' handler
unconditionally sent { type: 'pop-in' } to the main renderer, which runs
popInPet() and persists $petOverlayActive=false. restorePetOverlay() on
the next boot therefore skipped restoration and the pet silently reverted
to in-window after every clean quit with the pet popped out.

Latch an appQuitting flag (distinct from isQuittingForHandoff, which is
hand-off-specific and resets) at the top of before-quit, and on the
non-macOS primary-window close path where closing the window IS quitting.
The overlay 'closed' handler now suppresses the pop-in echo while
quitting; a user-initiated overlay close (⌘W) still pops the pet back in.

Supersedes #55934 (liuhao1024) and #58112 (tianma-if), which implemented
the same guard against the since-deleted main.cjs.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: yoma <yingwaizhiying@gmail.com>

Fixes #55920
2026-09-30 22:49:35 -05:00
Brooklyn Nicholson
af92ea8e5b fix(plugins): reuse the bounded git probe for catalog freshness 2026-09-30 22:30:18 -05:00
liuhao1024
8f628e2e81 fix(plugins): date a treeless checkout by its files when history is unreadable
The no-lazy-fetch probe made ``in_tree_catalog_time()`` return ``None`` on
every tree:0 checkout — indistinguishable from "not a git checkout". That
``None`` feeds the frozen-copy rule in ``_prefer_in_tree_entry`` (no time,
no versions ⇒ live wins), so a live cache fetched before ``hermes update``
re-pinned the old sha on exactly the installs that had just bumped it —
the very regression ``load_catalog_live()``'s contract forbids (#125716
review).

On a git checkout whose path history is unreadable, fall back to the
newest checked-out catalog file's mtime: the checkout ``hermes update``
just wrote outranks the pre-bump doc again, while a checkout whose catalog
files predate the doc still defers to it. ``None`` now means only "no
usable in-tree time" (release installs, never-checked-out clones).
2026-09-30 22:30:18 -05:00
liuhao1024
f99518333d fix(plugins): keep the catalog date probe off lazy fetches on treeless clones
in_tree_catalog_time() dates the checkout with a pathspec'd `git log`,
which opens every commit's tree. On a tree:0 partial clone (the layout
`hermes update` produces) those trees are absent, so the probe lazy-fetches
each one against the remote: measured 40-80s per plugins.manage list,
past the desktop's 30s RPC limit (#125683, #123446). Scope
GIT_NO_LAZY_FETCH=1 to the probe so missing trees fail fast into the
documented None fallback, and resolve the pins/versions/titles maps from
ONE load_catalog_live() pass instead of three in _plugin_rows().
2026-09-30 22:30:18 -05:00
Brooklyn Nicholson
9c6fe3aa75 docs(desktop): keep the ARM64 window-context note inside the Troubleshooting section
The new ### heading was inserted between '### Troubleshooting' and its bullet
list, orphaning the dashboard sign-in bullets out of their section.
2026-09-30 22:27:11 -05:00
itsflownium
ff19f80f86 fix(desktop): make ARM64 binding guidance version-independent 2026-09-30 22:27:11 -05:00
itsflownium
9b363a33db fix(desktop): explain unavailable Windows ARM64 window context 2026-09-30 22:27:11 -05:00
Brooklyn Nicholson
067fb583be fix(gateway): keep the slash worker's string contracts; carry the seed out of band
The seed harvest broke three contracts main relies on: _run(cli, cmd)
returned a dict (main's test asserts a plain string) and _SlashWorker.run
returned a (output, seed) tuple (main's fakes return a bare string, so
slash.exec hit 5030 / the mirror busy-warning path never fired / the
concurrent-spawn fake 403'd). Keep both string APIs and carry the seed
separately: _run parks it on cli._harvested_seed, the worker process
replies it on the existing 'seed' field, and _SlashWorker.run stores it
for a pop_seed() the gateway calls after run().

Two tests asserted the live /prompt viewer this PR intentionally removes
(compose-only /prompt); updated minimally: the r7 read-command case drops
its 'prompt' entry and the mutating-mirror test drops its /prompt row.
2026-09-30 22:24:00 -05:00
Brooklyn Nicholson
4b707a0f2b fix(cli): cancel /prompt compose on editor failure instead of shell fallback
_compose_in_editor fell back to shell=True with the raw $EDITOR value
unquoted when the argv invocation raised, so a crafted EDITOR value ran as
shell code. Drop the fallback: an unargv-splittable or unrunnable editor,
or a nonzero editor exit (which may leave abandoned buffer text), now
cancels the compose cleanly.

Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
2026-09-30 22:24:00 -05:00
Brooklyn Nicholson
59ddb98c03 fix(gateway): send the slash worker's queued next-turn prompt instead of dropping it
/prompt and /blueprint park their composed prompt on the one-shot
_pending_agent_seed for the interactive REPL loop — but the persistent slash
worker has no REPL, so Desktop/TUI /prompt silently dropped the composed
text (or worse, the gateway's live 'prompt' entry dumped the raw system
prompt instead of composing). Harvest the seed in the worker's _run, carry
it through _SlashWorker.run as (output, seed), and route slash.exec to
return a {type:'send', message} dispatch — which the Desktop and TUI clients
already handle. The gateway's live system-prompt viewer entry (and its
mirror) is removed: /prompt is compose-only in the CLI, and Desktop /prompt
is the compose affordance, not a viewer.

Co-authored-by: KoNit-K <konit.block@protonmail.com>
Co-authored-by: JPeetz <j.peetz69@gmail.com>
2026-09-30 22:24:00 -05:00
Mark S.
94bba41339 Merge pull request #128346 from unsupportedpastels/fix/128286-legacy-clarify-answers
fix(compression): preserve legacy clarify answers without promoting notices
2026-09-30 23:16:00 -04:00
Evgeny Mettsler
cfdcea4f22 chore: map contributor email for the attribution check
The check-attribution job requires every new author email on the branch to have
a contributors/emails/<email> mapping (or a legacy AUTHOR_MAP entry). This is
the mapping for the PR author's commit email.
2026-09-30 22:30:35 -04:00
Evgeny Mettsler
d50e8ef279 test(kanban): trim turn-recovery suite per review (1,136 -> 728 lines)
Collapse the per-driver duplicates:
- one cli_harness fixture drives both one-shot routes (chat -q / -Q) through a
  single shared fake CLI, so recovers_in_place, the quota-wall release, the
  denial, nothing-settled and the exit-summary smoke pin are one test each
  parametrized over the driver
- the seven goal_mode_does_not_continue_* tests become one table (eight refusal
  rows) over both drivers, plus the positive control per driver
- interrupt / terminal settlement / partial stay as should_recover_turn unit
  rows; the end-to-end denial is one representative row per driver
- exit mapping: drop the overlap with main's test_single_query_exit_contract.py
  and keep only this PR's addition (the stripped kanban_task_id() predicate)

Kept as requested: pinned-carrier fail-closed, the ambient-board negative,
lease expiry across the backoff, the recovered-response positive control.

Verification: 77/77 green; ruff check . plus the blocking CI scripts clean;
a 7-mutant battery re-introducing each damage class was killed 7/7.
2026-09-30 22:30:35 -04:00
Eugene M.
5fd24f47fd fix(kanban): retry a failed worker turn in place; carry the recovered response
Same-session recovery for kanban workers whose turn dies on a retryable
provider failure: the one-shot CLI used to return rc=0 with no terminal
kanban call, the dispatcher booked a protocol violation and cold-restarted
the card, discarding the whole session context. This retries the turn IN
PLACE (same session, bounded budget, 15/45/90s backoff) and exits honestly
when recovery is exhausted or refused.

Retry authority is positive and typed — never inferred from an outcome
shape: only a failed turn whose classifier marked it retryable; interrupts
are never retried (already persisted/cleared by agent/turn_recovery.py);
the #87096 bounded terminal settlement is never retried; partial /
completed=False are not authority at all. Before EVERY attempt the worker
proves it still owns the exact, unexpired run/claim lease: the dispatcher-
pinned carrier (HERMES_KANBAN_DB + HERMES_KANBAN_RUN_ID +
HERMES_KANBAN_CLAIM_LOCK) is required and compared exactly — no ambient
board resolution — and claim_expires must be unexpired on both the task
row and the open run row, mirroring release_stale_claims()'s stale
predicate and heartbeat_claim()'s extend-then-mirror contract. Fail-closed:
no proof, no retry. The loop never extends its own lease (claim-lifetime
policy remains #95318).

Rebased onto current main (28aceb3451): the one-shot machinery moved out of
cli.py into hermes_cli/cli_single_query.py, and the carrier re-derives
there — the shared exit mapper keeps ONE decision point (the worker
predicate inside it is the STRIPPED kanban_task_id(), at both the
credentials and the turn sites, so a whitespace-only value is not a worker
to the exit mapping either), and both one-shot drivers (`-Q` quiet and `-q`
non-quiet) run the recovery before their goal gates, which continue only on
a settled, authorized turn (post-recovery exit == 0).

Re-review P2 repaired: the non-quiet driver's recovery callback now
CAPTURES the recovered response (nonlocal) and goal continuation receives
the latest settled turn's deliverable — never the stale provider error of
the turn recovery replaced. The quiet driver already derived its response
post-recovery; the non-quiet side now matches. The non-quiet fake mirrors
production (chat() returns the settled result's response) and the positive
control asserts the judge receives "done"; the discarded-capture mutation
reddens that test with the exact stale-error string from the review's
reproduction.

Tests: 63 in tests/agent/test_kanban_turn_recovery.py (retry authority,
carrier/lease proof incl. expiry-during-backoff, goal composition negatives
on both drivers, positive controls, whitespace predicate pin), 133 green
across the 12 focused files incl. main's test_single_query_exit_contract.py.
Mutation receipts: worker predicate un-stripped -> 1 red; non-quiet goal
gate removed -> 1 red; response capture discarded -> 1 red (the review's
repro); quiet goal gate removed -> 8 red; restores byte-identical.
2026-09-30 22:30:35 -04:00
unsupportedpastels
7a2ee65bd2 fix(compression): classify complete legacy notice fragments
Preserve genuine selections around comma-normalized historical notices and require closed quoted question/choice fields before recognizing a notice suffix. Cover real historical producer outputs, embedded terminators, prefix lookalikes, and explicit status through public compression dispatch.
2026-10-01 02:28:11 +00:00
unsupportedpastels
3213083b30 Merge remote-tracking branch 'origin/main' into fix/128346-finish-review 2026-10-01 01:40:13 +00:00
unsupportedpastels
eb8d21f482 fix(approval): give global-flag runs one parse in dangerous-command rules
The terminal approval check runs right after the lifecycle guard, on the
tool thread with no deadline. Seven hermes/docker/podman rules used the same
ambiguous `-{1,2}\S+(?:[=\s]\S+)?` flag repetition as the lifecycle pattern,
so the reported rclone command (and any long flag run after hermes, docker,
docker compose or podman) still froze the gateway in
detect_dangerous_command() once the lifecycle scan returned (#129281).

Give each rule a linear global-flag fragment with one parse per flag. The
docker/podman rules keep taking a separate value only after exactly one
whitespace character, as before, so no approval decision changes: old and
new rules agree on every command of up to four tokens with single spaces
and up to three tokens with mixed spaces, double spaces and tabs.

Reported-by: starship-s
2026-09-30 21:33:08 -04:00
unsupportedpastels
dddad05a47 fix(cron): remove profile scan ambiguity without dropping long commands
Preserve the optional-value disambiguation from #129282 while removing its flag-count cutoff. Make token and whitespace runs possessive, and retain standalone dash values. Bound regression probes in subprocesses so a GIL-holding regex cannot wedge the test runner.

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-09-30 21:33:08 -04:00
Yuan Li
c7fe423476 fix(cron): bound profile flag lifecycle scan
(cherry picked from commit 9546b9dd687606d676799b50a0df6e136fc6db8a)
2026-09-30 21:33:08 -04:00
unsupportedpastels
bb873ade25 fix(compression): recognize normalized legacy headless notices
Match the complete single-query notice rather than its prefix, and recognize comma-normalized headless notice envelopes before filtering individual selections. Preserve current explicit status and independent non-notice selections.

Co-authored-by: Andrex Ibiza, MBA <84248988+andrexibiza@users.noreply.github.com>
2026-10-01 01:27:27 +00:00
unsupportedpastels
238967f797 Merge remote-tracking branch 'origin/main' into fix/128286-legacy-clarify-answers 2026-10-01 01:23:11 +00:00
emozilla
f848940560 fix(desktop): skip the WSL GPU-blocklist override on Wayland ozone
Since entry.ts shipped again (c0a0f097a8), local WSLg launches relaunch on
--ozone-platform=wayland. main.ts still forced --ignore-gpu-blocklist on
every /dev/dxg host, and on Wayland ozone that re-enables GPU compositing
Chromium had correctly disabled: WSL has no DRM render node, so the GPU
process segfaults (exit 139, null deref) on every start until Chromium
aborts with "GPU process isn't usable". hermes desktop never shows a
window.

Keep the override on X11 ozone, where it enables full GPU compositing on
D3D12. On Wayland, leave Chromium's defaults: rasterization and WebGL stay
on the GPU, compositing falls back to software, and the #57614 maximize
fix from #113247 stays intact.
2026-09-30 20:01:08 -05:00
unsupportedpastels
96982d6284 fix(cron): retain bounded history after targeted worker recovery 2026-09-30 20:57:21 -04:00
unsupportedpastels
a70056c73a fix(cron): preserve diagnostics when recovery wins a wait timeout 2026-09-30 20:57:21 -04:00
unsupportedpastels
9830058c00 fix(cron): report acknowledged worker failures without losing diagnostics
Extend Finn763's dead-owner recovery with owner-fenced failure delivery based on PR #120391 and retain stderr until the parent consumes an escaping worker traceback. Preserve unknown side-effect state and do not retry automatically. Cover both shared entry paths, an exception through the payload lifecycle, abrupt death, and concurrent recovery.

Co-authored-by: Denis Dresvyanskiy <dresvyanskiydenis@gmail.com>
2026-09-30 20:57:21 -04:00
Finn763
ef64034005 fix(cron): record a lost manual run instead of reporting a restart that never happened (#128509)
A restart-safe cron worker that dies AFTER adopting its attempt was reported as
"Scheduler restarted after this execution's owner exited" and treated as success.

recover_interrupted_executions() only knows that an owner is gone, so that is all it
can write -- but the waiter holding the worker's Popen knows the owner was that
worker and knows its exit status. Worse, the sweep leaves the row terminal, so the
waiter returned True, run_one_job short-circuited, and mark_job_run never ran: the
job's fire claim outlived its lost execution and refused the next manual fire with
"Job is already being fired" for the whole 300s lease, with an empty last_error.

- executions.terminalize_dead_owner(): terminalize ONE attempt with an observed
  cause. Refuses a live owner, this process, and a handoff inside the adoption
  grace, so it can never steal a running worker's attempt; returns False so the
  caller keeps the generic sweep.
- The waiter records that cause and RAISES, routing the existing post-handoff
  bookkeeping: mark_job_run records the run and retires the claim, no dispatch
  incident and no false crash notice. Also folds the worker's captured stderr into
  the error, which the pre-ack path already did (#112729) and this path dropped.

(cherry picked from commit d3f3571f8c6f4d2f188e90151d8f8393f049a573)
2026-09-30 20:57:21 -04:00
unsupportedpastels
663362680b fix(gateway): pass restored managed pins through plugin inputs 2026-09-30 20:10:03 -04:00
unsupportedpastels
6f7cc09b56 fix(gateway): retain unpinned managed-extra siblings at destination 2026-09-30 20:10:03 -04:00
unsupportedpastels
9c2d40d60a fix(gateway): preserve managed values and bridge authored subdicts
Restore managed extra values after cross-shape merges and expose authored subdict conflicts to plugin hooks before global env fallbacks. Consolidate the precedence coverage into two invariant matrices and qualify documentation for managed policy and explicit env precedence.

Co-authored-by: Victor Kyriazakos <victor@rocketfueldev.com>
2026-09-30 20:10:03 -04:00
unsupportedpastels
77bc57df41 fix(gateway): a managed platform-block pin is not overridden by user-authored extra
With authored platforms.<plat>.extra outranking the <plat>: block, a user
could replace a key the administrator pinned in the managed block
(require_mention, allow_from, ...) by writing it under their own
platforms.<plat>.extra. On main the managed block value wins.

Drop managed block-pinned keys from the authored snapshot. A key the
managed layer sets under platforms.<plat>.extra stays authored, so it now
also beats a user's top-level block.
2026-09-30 20:10:03 -04:00
unsupportedpastels
9b553c1267 fix(gateway): legacy gateway.json extra does not outrank the config.yaml platform block
The authored-extra snapshot was taken from the merged platforms_data, which
already holds the legacy gateway.json layer. A gateway.json
platforms.<plat>.extra key was therefore treated as authored and beat the
same key in the config.yaml <plat>: block, in extra and on the env rung,
with a warning naming a platforms.<plat>.extra the user never wrote.
config.yaml keys always win over gateway.json.

Snapshot from a scratch merge of the YAML layer only.
2026-09-30 20:10:03 -04:00