Commit Graph

6637 Commits

Author SHA1 Message Date
ethernet
8ac5c74432 Harder linter fixes (#135281)
* ci: fix B004 linter rule

* ci: prefix unused variables with underscore (RUF059)

* ci: don't cast int to int

* ci: remove unnecessary if/else where both blocks yield same result

* ci: RUF024

* ci: fix RUF017, RUF018, RUF022

* ci: RUF013, RUF015

* ci: RUF007

* ci: RUF009

* ci: address code review
2026-10-08 18:56:50 -04:00
Teknium
a9e2716c9b fix(codex): confirm a pending call whose call_id first appears on its done item
When the announcement carries no call_id and the done item does, the
call_id-exclusive lookup found nothing and left the announcement pending;
response.completed then settled it as a second call. Fall back to the
item/index match only when the matched announcement has no call_id of its
own, so a conflicting call_id still never evicts an unrelated call.
2026-10-08 14:10:45 -07:00
VictorYXL
c3302dd4fb fix(codex): integrate pending call-id aliases with stream correlation
Port PR #94708 to _CodexResponseAssembler, preserving rotating argument IDs and output ordering. Coalesce all matching call_id aliases, inherit earliest announcement metadata, and protect unrelated calls from conflicting item/index identities.

Adapted from commit 70b311ee50a0350b79c6c2b6123754d6aa6505da (PR #94708).

Verification: 130 focused tests passed; independent review passed.

Co-authored-by: DragonLi-Mi <4321063+DragonLi-Mi@users.noreply.github.com>
2026-10-08 14:10:45 -07:00
DragonLi-Mi
5e24de476d fix(codex): preserve calls across rotating stream item IDs
Match pending argument and completion events by output_index when item IDs change. Preserve authoritative arguments and successful output ordering without settling the same call twice.

Verification: independent code review passed; 75 focused tests passed; live Copilot Responses probe passed.
2026-10-08 14:10:45 -07:00
Maciej Lisiewski
9d9a3840d1 ci: address code review 2026-10-08 22:16:26 +02:00
Maciej Lisiewski
4ee5e8e0f3 ci: RUF007 2026-10-08 21:35:58 +02:00
Maciej Lisiewski
4278b6aa24 ci: RUF013, RUF015 2026-10-08 21:29:48 +02:00
Maciej Lisiewski
74f1f17a7e ci: fix RUF017, RUF018, RUF022 2026-10-08 19:54:07 +02:00
Maciej Lisiewski
b4e436c115 ci: don't cast int to int 2026-10-08 19:28:23 +02:00
Maciej Lisiewski
aa2d120cba ci: prefix unused variables with underscore (RUF059) 2026-10-08 19:22:03 +02:00
Maciej Lisiewski
c0fc687685 ci: address code review comments 2026-10-08 18:04:50 +02:00
Maciej Lisiewski
d76cce4f11 ci: apply all automatic linter fixes available 2026-10-08 17:22:54 +02:00
Teknium
b1a39a1b1b refactor(approval): one session /yolo contract for CLI, TUI/Desktop and the messaging gateway
The persist / restore / carry logic for session YOLO had grown three copies
(the CLI mixin, tui_gateway, the gateway runner), each with its own key choice,
ordering and error handling. tools/approval_yolo.py now owns it:

- toggle_session_yolo: flip or set, persist BEFORE the live flip (a racing
  restore never revives an OFF), stored-copy-aware so a toggle after a restart
  turns it OFF; a failed write logs a warning and never blocks the flip.
- restore_session_yolo: re-arm a persisted flag in a fresh process; skipped
  under the frozen process --yolo.
- transfer_session_yolo: move the live flag to a continuation id.
- with_session_yolo: a row being created records a live flag (lazy first row,
  compression child, /branch).

Each surface keeps only its storage: the session row for CLI/TUI/Desktop, the
routing entry for the gateway. Removed: HermesCLI._transfer_session_yolo /
_persist_session_yolo, GatewayRunner._restore_session_yolo, the TUI-only
restore helper, and the TUI's post-publish set_session_yolo carry (the
compression publish now writes the child row with the flag).

Closes a gap the unification exposed: a CLI manual /compress that rotated the
session dropped YOLO (the next dangerous command prompted, and a --resume of
the continuation came back with it off).
2026-10-08 00:49:32 -07:00
Teknium
f3ef9db683 fix(desktop): a voice turn's reasoning-off no longer leaks into new chats
A voice turn runs on auxiliary.voice_chat (default reasoning_effort: none, applied to
the main model when the slot is on auto). While it ran, session.info, config.get
reasoning, the warm-reattach resume and the session-row persist all read the agent's
live voice route as the session's own. Desktop adopts session.info's effort into the
sticky composer effort that seeds every new chat, and the end-of-turn settle repairs
only the chat on screen, so a chat opened mid voice turn was created with reasoning
off. On a model that loops when thinking is disabled (GLM-5.3-Flash on a custom
endpoint) every new Desktop chat died on the repetition guard while the CLI worked.

session_runtime_view answers model/provider/endpoint/effort from the state
begin_voice_turn_route already captured; the voice turn itself still sends its own
route and effort.
2026-10-07 17:42:27 -07:00
ethernet
bc2e4d3773 Merge pull request #134209 from NousResearch/sid/onboarding
feat(onboarding): first-run setup chat
2026-10-07 19:17:15 -04:00
Teknium
19cb1cbfed fix: disclose enabled plugins' registered skills in the system prompt's available_skills index 2026-10-07 16:01:10 -07:00
Teknium
b8788c3cb1 fix(agent): bound lease lock tolerance by the row's committed expiry
Review of #134577 found two ways the lock tolerance could still outlive
the lease row, letting a successor reclaim it under a running turn:

- The local deadline was taken AFTER acquisition/renewal returned, while
  the store stamps expires_at BEFORE its write-lock wait, so a renewal
  that waited 20s gave the turn 20s more than the row. The deadline now
  starts from the admitted row's committed expiry
  (SessionDB.session_turn_lease_expires_at) and from a timestamp taken
  before each successful renewal; neither can exceed the row.
- A retry that started before expiry could block in the write-lock wait
  past it. Each renewal's write patience is now capped at the remaining
  authority minus a 2s margin (refresh_session_turn_lease patience_s),
  and the next-tick check budgets that margin too.

Test: real SessionDB + a second-connection write lock. Red on the PR head,
green here; the lock-clears control renews with no interrupt.
2026-10-07 14:24:22 -07:00
686f6c61
23cd990268 fix(agent): stop a locked lease refresh once its lifetime runs out
A SQLite lock is still a missed tick, but only while the next attempt
lands before the row expiry. Past that, acquisition can hand the session
to someone else while this turn is still active.
2026-10-07 14:24:22 -07:00
686f6c61
bae3a01012 fix(agent): do not interrupt a turn when lease refresh hits a SQLite lock
A locked state.db is contention, not a lost holder. Keep the refresher armed so the next tick retries instead of stopping a healthy turn.
2026-10-07 14:24:22 -07:00
Austin Pickett
3adc457de5 Merge remote-tracking branch 'origin/main' into sid/onboarding
Conflicts, all in apps/desktop/src/i18n:
- de/en/es/fr/zh.ts, types.ts: import blocks only. Main added the
  *_projects section split, this branch the *_onboarding one; kept both.
- en_shared_metrics, zh_shared_metrics, ar_common, zh-hant (usage-stats
  consent copy): kept this branch's rewritten copy. Main's only change on
  its side since the merge base was a prettier reflow of
  collectedReliability (2943ee6f19), which the branch's rewrite replaces;
  types_shared_metrics on HEAD already matches the branch's keys.

Follow-through: main's Undo-hide toast (sidebar.projects.hiddenFromSidebar /
undoHide) arrived without ar or zh-hant strings, which this branch's
overlay-completeness test rejects. Added both to ar_chrome / zh-hant_chrome
and regenerated locales/_keys.desktop.json.
2026-10-07 16:16:23 -04:00
Brooklyn Nicholson
dd6ab89cb8 fix(pricing): price Bedrock Claude Opus 5 and 5.5 from the snapshot
Direct Anthropic Opus 5 is priced on main, but the Bedrock lane still
missed anthropic.claude-opus-5 and anthropic.claude-opus-5-5, so Bedrock
sessions on either model (including the static Bedrock picker's
us.anthropic.claude-opus-5-5) fell through to models.dev or recorded
unknown / $0. Bedrock bills Claude at Anthropic's per-token rates, so
reuse the direct rows' rates.

Refs hermes-agent 100848

Co-authored-by: RelaxJonh <92573950+RelaxJonh@users.noreply.github.com>
2026-10-07 14:18:26 -05:00
kshitijk4poor
b6a7e591b6 fix(free-tier): the 402 sign-in refusal applies on the welcome host only
The new 402 branch in _nous_welcome_tier keyed on the anonymous JWT alone, so a
free-tier credential that reached the paid host (user-set NOUS_INFERENCE_BASE_URL,
the case the route heal and its anon_on_paid_host copy exist for) got the
free-tier "refused" sign-in verdict instead of the ordinary 402 handling. The
module's own test docstring states the rule as "a 402 from the welcome host".

Test: anonymous 402 off the welcome host classifies as billing with no
welcome_refusal. Red on c4ba8b1e8e, green here.
2026-10-08 00:41:21 +05:30
kshitijk4poor
9d33231734 refactor(setup): setup_choose reads the facts module directly; drop the dead fork skip
- The fork card has had no `skip` row since its rows became app-owned (mind,
  machine, figure plus two first tasks), and typed text matching no row comes
  back as `typed`, so `picked == "skip"` could never be true. A skipped fork
  arrives as `cancelled`, which the same branch already handles.
- `collect_setup_cards` and `fork_card` in initiate_setup_prompt only forwarded
  to initiate_setup_facts and setup_choose_tool was their only caller.
- The fork question was spelled twice; read initiate_setup_facts.FORK_QUESTION
  (late import: tool discovery imports this module).
2026-10-08 00:41:21 +05:30
alt-glitch
db02737cf3 Merge remote-tracking branch 'origin/main' into sid/onboarding 2026-10-07 20:37:24 +05:30
alt-glitch
2c8df45ee5 Merge remote-tracking branch 'origin/main' into sid/onboarding
# Conflicts:
#	agent/conversation_loop.py
#	tui_gateway/prompt_turn.py
2026-10-07 19:29:35 +05:30
unsupportedpastels
bf23bccef3 fix(auth): a profile's auth add never copies root's login into the profile
Root's login for nous, openai-codex and xai-oauth can live only in its
auth.json `providers.<id>` block, with no pool rows yet. In a profile
that borrows from root, load_pool() seeds it as a `device_code` entry
through the root fallback. The write-time claim in add_entry() kept
every entry except root's pool rows, so the profile's first `auth add`
wrote root's single-use refresh token into the profile's auth.json: a
forked grant (#100339) that the heal cannot match, because root has no
pool row for it.

- add_entry() writes only the added row when a profile claims a
  single-use provider. Rows the profile seeds from its own sources are
  seeded again on the next load.
- _seed_tokens_singleton() (openai-codex, xai-oauth) no longer seeds
  root's providers block into a profile that owns its own rows, the
  rule _seed_nous_singleton already applies. On current main such a
  profile copied root's refresh token into its own pool on every load.
2026-10-07 09:59:11 -04:00
John Paul Soliva
dc824c5f65 fix(auth): save a profile's first single-use login, and never print Added for an unsaved row
- Decide the profile claim when the row is written (the check from #101827)
  and drop the None sentinel from #103712. On current main load_pool() leaves
  a profile that borrows from an empty root at the default, so the sentinel
  alone never reached the claim path (its test stays red).
- add_entry() re-reads the store after writing. A missing row raises
  CredentialNotSavedError; `hermes auth add` exits with that message instead
  of printing "Added", and the dashboard pool endpoint returns it as a 400.
- Tests: the two contributor tests become one invariant over every
  SINGLE_USE_REFRESH_POOL_PROVIDERS provider (the row is readable, root
  auth.json is byte-identical), plus the CLI success-line contract.
- Docs: profiles page, EN and zh-Hans.

Fixes #103694

(cherry picked from commit 0e250a005a3353564642ce70f100d89174a583f2)
2026-10-07 09:59:11 -04:00
Lluis Faus
c5087bad02 fix(auth): persist first profile OAuth row with empty root
(cherry picked from commit 5293489825af98e3e3f0e6cb485ffffa72d14833)
(cherry picked from commit 4144e14bfa7c021a92db5b0f66a75e2e0ac767d1)
2026-10-07 09:59:11 -04:00
0xAlyDev
f383e66b3e fix(auth): persist profile single-use OAuth credentials when root store is empty
Fixes #103694: When a named profile runs auth add for a single-use
refresh provider (e.g. Anthropic, Codex, xAI) and the root store has
no existing rows for that provider, load_pool() sets _borrowed_root_ids = set().

Previously, add_entry() checked if borrowed_ids:, which evaluated to
False on an empty set. It fell back to _persist() -> persist_pool_entries(),
which routed to the update-only _update_root_pool_rows() on root. Because
root had no rows to update, the credential was dropped with no error and
never written to the profile's auth.json.

Checking if borrowed_ids is not None: ensures that profile-scoped pools
persist their new credential directly to the profile store via write_credential_pool(),
transitioning the profile to owning rows for that provider.

(cherry picked from commit b7467a2999832749f540d86530cf463ca0945b04)
(cherry picked from commit 9c604999e233cf6d74a83e4d7c141785b341b0d6)
2026-10-07 09:59:11 -04:00
alt-glitch
16f16755d5 fix(free-tier): a short free-tier 429 no longer blocks for a whole quota window
welcome_refusal_from_headers took the hourly bucket's reset as the wait
even when that bucket had requests left, so Retry-After: 4 with a
healthy bucket became a 2,000 s rate_limited refusal and tripped the
shared breaker. The wait now comes from an empty bucket's reset, else
Retry-After, else the body's wait.
2026-10-07 19:22:43 +05:30
kshitijk4poor
70dcdd640f refactor(agent): hoist the hidden placeholder row import and say why a timeout carries no message
Gate rQ quality Lows: the placeholders module imports nothing, so conversation_loop can import it at module level like turn_api_call; the terminal-approval comment now says callers re-queue _interrupt_message, so a system stop must not set one.
2026-10-07 18:15:29 +05:30
kshitijk4poor
299a1327b2 docs(agent): say skipped-result content uses str.replace for {name}
The _append_skipped_tool_results docstring still said content "is
formatted with {name}", but the body substitutes with str.replace so a
recorded interrupt reason containing braces renders verbatim. State
that, so nobody "simplifies" it back to str.format.

Gate rP finding 8 (quality).
2026-10-07 18:15:29 +05:30
kshitijk4poor
184a83de33 refactor(agent): import the interrupt placeholder at module level
agent_runtime_helpers (two sites) and message_sanitization imported
_INTERRUPTED_PLACEHOLDER inside the function body. The placeholders
module is a leaf with no imports, so there is no cycle to dodge; import
it once at module level (each module still imports cleanly in a fresh
interpreter). conversation_loop keeps its local import.

Gate rP finding 7 (quality).
2026-10-07 18:15:29 +05:30
kshitijk4poor
714fc8071c refactor(agent): share the hidden interrupt placeholder row
message_sanitization.close_interrupted_tool_sequence and the
turn_api_call repetition-loop exit each spelled the same hidden
assistant row literal (content "", display_kind hidden, api_content
_INTERRUPTED_PLACEHOLDER), and the conversation loop's redirect built it
field by field. Add hidden_interrupt_placeholder_row() to the leaf
placeholders module and use it at all three sites so the shape cannot
drift between them.

Gate rP finding 6 (reuse).
2026-10-07 18:15:29 +05:30
kshitijk4poor
7c493cd2a1 refactor(agent): derive user-stop skip wording from USER_INTERRUPT_REASONS
_USER_STOP_WORDING restated the three USER_INTERRUPT_REASONS keys with
their values capitalised, so a new human-stop reason had to be added in
two places or it would render as "Turn aborted — …". Drop the mirror
and capitalise the recorded reason; output is identical for all three
keys (probe-B02-5.py).

Gate rP finding 5 (reuse).
2026-10-07 18:15:29 +05:30
kshitijk4poor
89143571fa fix(agent): stop terminal batch preparation from overwriting a user stop
terminal_approval_batch caught both _CancelledPreparation and TimeoutError
and called agent.interrupt(str(exc)). _CancelledPreparation is raised
because a user interrupt is already pending, so the user's queued message
and redirect were replaced by "Terminal approval preparation cancelled…",
which the gateway then re-queued as a fake user turn. The TimeoutError
branch booked a system timeout as a user stop with a fake requeue — the
#130207 bug class itself, at the sibling site the batch guard fix missed.

Now: close the batch always; only on timeout publish a message-less
interrupt with tool_reason="terminal batch preparation timeout"; do
nothing on cancellation (the stop is already published).

Gate rP finding 1 (2c).
2026-10-07 18:15:29 +05:30
kshitijk4poor
4136797829 refactor(agent): correct the stale placeholder-consistency comment
The moved comment on _INTERRUPTED_PLACEHOLDER claimed it was "kept
identical to the stub placeholder in chat_completion_helpers"; that
module has no such literal (the claim was already stale on main and was
moved verbatim). State what the constant actually is instead.

Trivial note from the public review on #133271 (@ahrazzle).
2026-10-07 18:15:29 +05:30
kshitijk4poor
15855870b2 fix(agent): only relabel a prepared-batch abort as a batch timeout on timeout
The batch-guard tail after _poll_sequential_future is shared by the
"timeout" and "interrupted" outcomes. On a user stop (Ctrl-C, /stop, a
new message) whose prepared terminal worker had not settled after the 3s
grace, the unconditional agent.interrupt(tool_reason="terminal batch
timeout") republished the interrupt: the turn was booked
interrupted_by_system(terminal_batch_timeout), _interrupt_message (the
user's queued next message) and _pending_redirect were nulled, and the
wrong reason was re-fanned to workers.

Keep batch.close() unconditional, but only call interrupt() on the
timeout branch; on the interrupted branch the stop is already published.

Public review blocker on #133271 (@ahrazzle, executed repro; @Enough1122).
2026-10-07 18:15:29 +05:30
kshitijk4poor
2bbb37b973 refactor(agent): move interrupt placeholders into agent_runtime_helpers_placeholders to keep files under the size cap
Code-health ratchet: agent/agent_runtime_helpers.py was 3787 > 3776 and
tests/agent/test_run_agent.py 7014 > 7012 (both over the 2,000 target, so
they may only shrink vs main).

- _INTERRUPTED_PLACEHOLDER / _LEGACY_INTERRUPTED_PLACEHOLDER (and their
  rationale comment) move byte-identically into the leaf sibling
  agent/agent_runtime_helpers_placeholders.py. Every importer (runtime
  helpers via late import, conversation_loop, message_sanitization,
  turn_api_call, turn_iteration_prep and five test files) now reads the
  sibling; no shim is left in the facade. Nothing patches the old path.
- test_keyboard_interrupt_emits_cancelled_post_tool_hook moves unchanged
  into tests/agent/test_run_agent_interrupt_hook.py, reusing the facade's
  agent fixture.
2026-10-07 18:15:29 +05:30
kshitijk4poor
764938ed74 fix(agent): publish the Ctrl-C reason before the sequential cancel hook
Gate r2 finding 3: in _run_sequential_call's KeyboardInterrupt handler,
ref.emit_cancelled() ran before agent.interrupt(..., tool_reason=user
interrupt), so the post_tool_call hook rendered its wording from an unset
reason and reported "Tool execution cancelled. Turn interrupted" while the
concurrent handler (and 47db3231e2's intent) report "User interrupt".
Publish the interrupt first, matching the concurrent handler.

The existing cancelled-hook test now pins the hook's error_message.
2026-10-07 18:15:29 +05:30
kshitijk4poor
c8634c7b64 refactor(agent): tidy the interrupt-placeholder fold-ups
Fold finding 4 (B02 gate, low): behaviour-neutral cleanups.
- close_interrupted_tool_sequence: the hidden branch only runs when the
  stripped text is empty or already the placeholder, so `stripped or
  _INTERRUPTED_PLACEHOLDER` is always the placeholder; say so.
- tool_executor: compute interrupt_skip_wording once for the cancelled
  outcome instead of twice.
- move _neutralise_replay_echo_ghosts out of prepare_iteration to module
  level (lazy imports kept inside to avoid the conversation_loop cycle).
- the finalizer test asserts the exact placeholder, not just non-empty.
2026-10-07 18:15:29 +05:30
kshitijk4poor
7f0af2e689 fix(agent): neutralise legacy interrupt placeholders instead of dropping them
Fold finding 1 (B02 gate, regression): the replay-echo filter now matches
the legacy "[response interrupted]" placeholder and DROPPED the hidden row
unless its neighbours were tool -> user. The common persisted redirect
shape user -> hidden(placeholder) -> user(correction with checkpoint
api_content) then became user -> user, and _merge_consecutive_users merged
the pair: the correction's checkpoint sidecar was discarded and the first
stored user row was rewritten in place.

Always neutralise a copy ({content: "", api_content: placeholder}) and
never drop: role-safe in every neighbour shape, and the echoable text is
still gone. The kept replay test now also covers the redirect shape
(3 rows survive, checkpoint intact, stored dicts untouched).
2026-10-07 18:15:29 +05:30
kshitijk4poor
7112b1abc2 fix(agent): attribute Ctrl-C tool cancellation to a user interrupt
Fold finding 2 (B02 gate, sibling of #130207): both KeyboardInterrupt
handlers in tool_executor call agent.interrupt("keyboard interrupt").
With a message and no tool_reason, interrupt() records "user sent a new
message", so the skipped-call notices for a Ctrl-C claimed the user had
sent a new message. Pass tool_reason=_REASON_USER_INTERRUPT so the
recorded reason (and the rendered wording) is "User interrupt"; the
issuer stays None, so attribution to the human is unchanged.
2026-10-07 18:15:29 +05:30
kshitijk4poor
17da1d75b7 fix(agent): render skip wording verbatim instead of brace-escaping it
Fold finding 3 (B02 gate): interrupt_skip_wording doubled braces on the
assumption the wording reaches str.format, but every consumer substitutes
{name} with str.replace, so a reason like "a{b}" rendered as "a{{b}}".
Drop the escape and its docstring claim, and look user-stop wording up
directly in _USER_STOP_WORDING.
2026-10-07 18:15:29 +05:30
kshitijk4poor
cd5b83a749 fix(agent): retire replay-echo ghosts without mutating the transcript
_neutralise_replay_echo_ghosts rewrote the caller's list in place
(seq[:] = out) and overwrote content/api_content on durable row dicts.
Those dicts are the live session rows the SessionDB flush cursor and
_DB_PERSISTED_MARKER track, so an in-place edit silently diverges the
in-memory row from what state.db holds. BASE built a new list here; keep
that contract: return a filtered list and neutralise a shallow copy of the
row, which only shapes this request's replay.
2026-10-07 18:15:29 +05:30
kshitijk4poor
c77fc2c06f fix(agent): drop the diagnostic message from the batch-guard interrupt
The terminal batch-timeout guard still passed "terminal batch tool did not
complete" as the interrupt message. That text lands in _interrupt_message,
which the gateway (_run_agent_drain_pending) and the CLI
(_chat_resolve_interrupt) re-queue as the user's next turn, so a guard abort
injected a fake user message. Call interrupt(tool_reason=...) with no
message, and use a slug-safe reason so the issuer reads
interrupted_by_system(terminal_batch_timeout) rather than
terminal_batch_aborted:_call_timed_out.

Message-less guard shape from #130209.

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-10-07 18:15:29 +05:30
alrcatraz
703200b803 fix(agent): stop the interrupt placeholder echoing back as a reply
A hidden assistant row is hidden from the transcript, not from the request:
the projection substitutes its api_content sidecar into content on every call.
The value chosen for that slot in #88955 was "[response interrupted]" - a short
natural-language phrase sitting in the model's own prior-turn position, which
models reproduce verbatim: an ordinary instruction answered with clean
finish_reason=stop and nothing but the placeholder (#132949). The same hazard
was already recognised twice in this codebase - the redirect scaffold is
barred from assistant text because "the model echoes it" (#81841), and a
runaway repetition partial is replaced by a self-describing label rather than
replayed (#112764) - but the placeholder was never given the same treatment.

Change the placeholder to a structural label the model has no reason to say,
and retire rows already persisted under the pre-change spellings: the prep
filter that dropped scaffold ghosts now also drops hidden rows carrying the
legacy placeholder or the scaffold marker.

Dropping is not always safe. A hidden close appended after a tool result is
the only thing separating that tool message from the next user turn; removing
it recreates the tool -> user alternation violation that makes strict providers
ignore prior context (#48879) - the exact failure the close exists to prevent,
and one repair_message_sequence deliberately leaves alone. When the neighbours
would form that pair, rewrite the row's text instead of dropping it. The
rewrite targets the current placeholder, so a neutralised row never re-triggers
the filter.

(cherry picked from commit 227e9a1ed64e0d75594d031423ecd2f5c74cd0b9)
2026-10-07 18:15:29 +05:30
alrcatraz
15015d7cfe fix(agent): hide only the placeholder close, keep caller banners visible
close_interrupted_tool_sequence hid every synthetic closing turn behind
display_kind="hidden", but four of its five callers pass a real user-facing
notice — the truncation terminal ("Response truncated — …"), the context-
overflow partial text, tool-validation partials and abort_turn_on_interrupt's
"Operation interrupted: …" lines. Hiding those removed the turn's only
explanation from every transcript surface.

Narrow the hidden shape to what it was for: an empty final_response or the
bare interrupt placeholder (which previously rendered as the "Operation
interrupted." bubble users never asked for). Any other text is rendered
verbatim as before. Alternation safety is unchanged in both branches.

(cherry picked from commit a637af2ef44c1f0213c3138c458d6f6c48650988)
2026-10-07 18:15:29 +05:30
alrcatraz
e8a478d155 fix(agent): render concurrent skip notices from the recorded reason
The sequential path was converted to interrupt_skip_wording() but the
concurrent batch kept three hardcoded user-blaming strings: the pre-batch
skip (execute_tool_calls_concurrent), the unfilled-slot notice
(_unfinished_tool_result) and emit_cancelled's hook message. A system
producer that stops a parallel batch — watchdog, lease loss, SSE
disconnect, or any segment routed through execute_tool_calls_segmented —
still told the model "skipped due to user interrupt" while
interrupt_issuer() booked the same turn as interrupted_by_system. Render
all three from the recorded reason like the sequential siblings; the
observability enums (error_type/hook_error_type) stay untouched.

Also harden the interpolation the wording rides on: _append_skipped_tool_results
substitutes {name} with str.replace instead of str.format, and
interrupt_skip_wording escapes braces in caller-supplied reasons, so a
free-form tool_reason can never raise KeyError/IndexError mid-turn.

(cherry picked from commit b785bccbca6fd997921411198d277c39d3d93261)
2026-10-07 18:15:29 +05:30
alrcatraz
6bbdc8ad80 fix(agent): hide the synthetic interrupt close from the transcript
close_interrupted_tool_sequence() appends a synthetic assistant turn when an
interrupted tail is a raw tool result, so the next user message does not land as
tool -> user. The row carried a user-visible "Operation interrupted." string and
was persisted as an ordinary assistant message, so background preemption —
session reaping, watchdogs, batch guards — surfaced in the transcript as an
interruption the user was expected to act on.

Store it in the same hidden shape turn_api_call.py already uses for interrupt
placeholders: content="" plus display_kind="hidden" keeps it out of rendered
transcripts, while the api_content sidecar carries the LLM-visible text and is
substituted at API-build time. The alternation guarantee is unchanged.

(cherry picked from commit 22502b1c40c26a08eefc3d35cee067bd1d889bfc)
2026-10-07 18:15:29 +05:30