Skip to content

fix(codex): persist session messages after app-server turn (#38210) - #38254

Closed
Tranquil-Flow wants to merge 1 commit into
NousResearch:mainfrom
Tranquil-Flow:fix/38210-codex-session-persist
Closed

Tranquil-Flow wants to merge 1 commit into
NousResearch:mainfrom
Tranquil-Flow:fix/38210-codex-session-persist

Conversation

@Tranquil-Flow

Copy link
Copy Markdown
Contributor

What

The codex app-server runtime path (run_codex_app_server_turn) bypasses the session persistence that the chat_completions path performs. Completed turns are never written to the messages table in state.db, so session.resume / session.info report message_count: 0 with messages: []. The desktop SPA reconciles its conversation view against that empty server state and discards the just-streamed reply.

Fix

Added agent._persist_session(messages) after successful codex app-server turns, gated on not turn.interrupted and turn.error is None, with exception safety. Mirrors the same pattern already used for external memory sync in the same function.

Files changed

  • agent/codex_runtime.py — +11 lines: persist call + comment block
  • tests/agent/transports/test_codex_app_server_runtime.py — +104 lines: 3 regression tests

Verification

  • Root cause confirmed: run_codex_app_server_turn() on upstream/main does not call _persist_session(). The codex app-server path returns directly, bypassing session persistence.
  • 30/30 tests pass in test_codex_app_server_runtime.py (27 existing + 3 new).
  • Regression tests:
    • test_persist_called_on_success — verifies _persist_session is called with messages including the assistant reply
    • test_persist_not_called_on_interrupt — verifies persistence is skipped on interrupted turns
    • test_persist_not_called_on_error — verifies persistence is skipped on errored turns

Competitor analysis

No competing open PRs found for this issue.


Auto-published by Moonsong via Path B automated pipeline.

…rch#38210)

The codex app-server runtime bypasses the conversation loop entirely
(conversation_loop.py:788 returns early), so _persist_session is never
called. This leaves sessions with message_count=0 and causes the desktop
SPA to reconcile away the just-streamed reply.

Add _persist_session(messages) in run_codex_app_server_turn after a
successful, uninterrupted, error-free turn. Interrupted / errored turns
skip persistence to avoid saving partial transcripts.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists labels Jun 3, 2026
@gitszabolcs

Copy link
Copy Markdown

Tested this patch live against f019a9c4 — applied this exact diff to agent/codex_runtime.py, restarted the dashboard, and drove a real openai-codex/gpt-5.5 turn from the macOS desktop app over a remote connection.

It fixes #38210. Codex turns now persist; the desktop view no longer reconciles away the streamed reply. sessions.message_count is non-zero and the assistant row lands in messages.

⚠️ But it surfaces a pre-existing duplication: every user message is now written (and displayed) twice. Assistant messages are unaffected.

state.db after two turns, with only this patch applied:

rowid | role      | content
405   | user      | 'hi'
406   | user      | 'hi'                   <- duplicate
407   | assistant | 'Hi. What would you like to work on?'
408   | user      | 'Finally you work!'
409   | user      | 'Finally you work!'    <- duplicate
410   | assistant | "I'm here and responding..."

Root cause

run_codex_app_server_turn does messages.extend(turn.projected_messages), and codex's projector (codex_event_projector.py::_project_user_message) re-emits the user turn as a userMessage echo. But run_conversation() has already appended the user message before this path runs (there's even a NOTE in the function about exactly this). So messages ends up as [user, user, assistant].

Pre-patch this was invisible: the codex path never persisted, and the empty server view got reconciled to nothing. Now that _persist_session(messages) flushes the list, the duplicate user row becomes real and visible.

The new regression test passes because its mocked turn.projected_messages contains only an assistant entry — a real codex turn's projection leads with the userMessage echo, which the mock doesn't reproduce.

Suggested fix

Drop the echo at the splice point, since Hermes already owns the user message:

if turn.projected_messages:
    # Codex re-emits the user turn as a userMessage echo, but
    # run_conversation() already appended the user message (see NOTE
    # above). Drop it so the turn isn't stored/shown twice.
    projected = [m for m in turn.projected_messages if m.get("role") != "user"]
    messages.extend(projected)

With both changes applied, a fresh session persists exactly 1 user + 1 assistant per turn and the desktop view is correct. A regression test that includes a userMessage item in the mocked projection (not just an agentMessage) would have caught this. Happy to open a follow-up PR if useful.

@teknium1

Copy link
Copy Markdown
Collaborator

Thanks for the persistence diagnosis and regression coverage.

Automated hermes-sweeper review found that current main already implements the requested Codex app-server persistence guarantee with the duplicate-write safeguard identified in the discussion:

  • agent/turn_context.py:354-356 persists the inbound user turn before the Codex early return.
  • agent/codex_runtime.py:445-470 appends projected Codex messages and flushes only newly unpersisted rows via _flush_messages_to_session_db(messages).
  • agent/codex_runtime.py:535-546 returns agent_persisted=True, preventing the gateway from re-inserting the already-persisted user message.
  • tests/agent/test_codex_app_server_persist.py:79-129 verifies exactly-once user/assistant persistence against a real SessionDB and FTS visibility.

This landed in dc1ea005d9dbf7cbe18380755bfc4d4c08df9553 and shipped in v2026.7.1.

@teknium1 teknium1 closed this Jul 13, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 13, 2026
teknium1 added a commit that referenced this pull request Sep 7, 2026
Port the exact submitted-wire-text ownership boundary from #93546 onto
current topical runtime code. Do not add the candidate's mocked-result
fallback or storage-level content deduplication. Preserve later distinct
and identical user events, separate identical accepted turns, and keyless
inputs. Add two regression invariants and offline subprocess-wire A/B.

Local wire A/B: 4/8 control matrix passing on base, 8/8 after.
Broader tests queued behind campaign lock; not ready for merge.

Refs #104653
Original diagnosis: @gitszabolcs (#38254)
Original implementation: #43127, submitted by @vashkartik
Focused salvage and wire-text correction: @fancyboi999 (#93546)
Current-main carry-forward considered: #104698

Co-authored-by: Xinmin Zeng <135568692+fancyboi999@users.noreply.github.com>
Co-authored-by: VECTOR <vector.hq@outlook.com>
teknium1 added a commit that referenced this pull request Sep 7, 2026
Port the exact submitted-wire-text ownership boundary from #93546 onto
current topical runtime code. Do not add the candidate's mocked-result
fallback or storage-level content deduplication. Preserve later distinct
and identical user events, separate identical accepted turns, and keyless
inputs. Add two regression invariants and offline subprocess-wire A/B.

Local wire A/B: 4/8 control matrix passing on base, 8/8 after.
Broader tests queued behind campaign lock; not ready for merge.

Refs #104653
Original diagnosis: @gitszabolcs (#38254)
Original implementation: #43127, submitted by @vashkartik
Focused salvage and wire-text correction: @fancyboi999 (#93546)
Current-main carry-forward considered: #104698

Co-authored-by: Xinmin Zeng <135568692+fancyboi999@users.noreply.github.com>
Co-authored-by: VECTOR <vector.hq@outlook.com>
mrkillbob added a commit to mrkillbob/hermes-agent that referenced this pull request Sep 8, 2026
* test(agent): cover all rejected video part types

Co-authored-by: crazyief <8566250+crazyief@users.noreply.github.com>

* test: consolidate video rejection variants into one invariant

* test: preserve reusable localhost provider wire A/B probe

* fix(agent): forward the new_text alias in the memory inline executor

The memory_tool schema advertises new_text as an alias for content, and
memory_tool resolves it when content is None. But the table-driven inline
executor's arg_specs (agent/inline_tool_executors.py) did not list new_text,
so _call_tool's allowlist silently dropped it: a replace call using the
documented alias reached memory_tool with both fields None and failed with
"content is required for 'replace' action." — even though the caller
supplied the value. Forward new_text alongside content/old_text so the
documented alias fires and content still wins when both are set, matching
what the batch path (op.get("content") or op.get("new_text")) already
accepts.

* test: pin memory alias persistence without dispatch mocks

* fix: mirror successful memory alias writes to providers

* fix: explain the launchctl registration restriction without inventing KeepAlive

* fix(agent): interpolate the live backups dir into corruption recovery guidance

The corrupt-cause recovery guidance hardcoded `~/.hermes/backups/` while
every other path in the same message follows the active HERMES_HOME
(`{db_path}` is already interpolated). A custom-home or named-profile
deployment was told to restore from a directory that may not exist at all,
mid data-loss incident. Both sites (turn-completion explainer and gateway
startup broadcast) now interpolate `<hermes_root>/backups` via
get_default_hermes_root(), matching hermes_cli/backup.py's real backup
location.

Fixes #104250

* test(recovery): distinguish the profile home from the full backup root

* fix: retain reasoning effort on named custom provider routes

Apply the narrow registry fallback proposed in PR #68458 without unrelated case normalization or dead legacy flags. Preserve dedicated named profiles before using CustomProfile. This corrects existing reasoning loss only; per-model dialect configuration remains a product decision.

Co-authored-by: saotu <160758706+saotu@users.noreply.github.com>

* test: preserve reusable localhost provider wire A/B probe

* test: handle localhost model metadata probe without fixture error

* test: clarify named-route parity wire evidence and held dialect scope

* test: distinguish completion capture from model metadata HTTP

* fix(desktop): show failed project loads separately from empty sessions

Salvage #104301 error presentation and translations. Keep drill-in outcome local to its mounted scope, ignore stale responses, retain existing rows, and show retry even when cached lanes exist.

Co-authored-by: elvindu <dumanxiang@qq.com>

* refactor: colocate fallback handoff with client lifecycle

* fix: retain rotating credentials during fallback handoff

Adapt the callable-source diagnosis from snipecoder (#102244) and fallback slice from BGwill-OUTLOOK (#102721), without unrelated reasoning or override changes.

Co-authored-by: CloudWishOS <99405975+snipecoder@users.noreply.github.com>\nCo-authored-by: BGwill-OUTLOOK <bgwillwork@outlook.com>

* fix: read only installed SDK credential providers at handoff

Independent review found auto-created mock attributes could replace static credentials. Read the SDK instance's stored provider without triggering attribute synthesis.

Co-authored-by: CloudWishOS <99405975+snipecoder@users.noreply.github.com>

Co-authored-by: BGwill-OUTLOOK <bgwillwork@outlook.com>

* docs: clarify fallback credential and cooldown behavior

* chore: credit callable fallback contributors

* fix: distinguish callable Anthropic bearer sources from OAuth text

* test: publish credential-free fallback wire reproduction

* fix(cli): make session-title badge skin-aware

* fix(cli): preserve session-title badge contrast

* test(cli): keep badge regression coverage minimal

* test: preserve themed badge contrast on light terminals

* fix(desktop-update): clean up throwaway browser profile directory

Deletes the temporary --user-data-dir used by the update UI shim when the browser process is shut down, preventing ~100MB leaks per update. Fixes issue #104350.

* fix(desktop-update): clean only the captured shim profile

Track the path actually launched, preserving the no-UI case and unrelated profiles. Adapted the ownership approach from #104362.

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

* test(desktop-update): leave self-test failure flag empty on success

* fix(desktop-update): atomically claim the temporary browser profile

Use mktemp -d before launching the optional UI; skip UI if allocation fails. Native Chrome collision and allocation-failure probes preserve preexisting directories.

* fix(desktop-update): discard failed profile allocation output

* chore: retain updater contributor release attribution

* feat(desktop): full command description hover on slash autocomplete rows (#104729)

* fix(desktop): reveal complete slash help across the window

Preserve producer descriptions without identity wrappers and size the existing themed tooltip to the viewport. Replace skipped and structural tests with two behavioral invariants. Native Electron before/after hover, click and keyboard verification passed; campaign suite validation remains queued.

* fix(desktop): relay MCP OAuth through client-local callbacks

* fix(desktop): preserve explicit local MCP OAuth without callback bridge

* test(desktop): trim MCP relay coverage to ownership and cleanup invariants

* chore: credit MCP relay contributor

* fix(desktop): cancel MCP auth when scoped Skills tabs leave their owner

* fix(mcp): enforce profile ownership throughout OAuth sessions

* test(desktop): make MCP renderer relay regressions executable

* fix: retain pending fleet restarts until supervisors recover

Discover systemd targets before stopping old processes, restart even when
there are no gateway PIDs, and require successful scope listings plus active
verification. Pending launchd recovery also retains failures for inaccessible
listings and installed jobs without supervision. Keep existing PID cleanup
intact but before recovery so it cannot kill freshly verified workers.

Slim redo informed by #104274, #104283, and #104285.

Co-authored-by: fangliquanflq <fangliquan@qq.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>

* fix(gateway): stop time-triggered conversation rotation

* docs(gateway): describe persistent sessions without reset timers

* test(gateway): retire automatic expiry contracts

* fix(sessions): remove remaining timer migration and guidance

* test(sessions): preserve live persistence and eviction controls

* test(sessions): verify gateway status after timer removal

* test(sessions): exercise pressure eviction and recovery boundaries

* test(sessions): preserve profile ownership checks after timer removal

* fix(slack): preserve explicit suspension after timer removal

* test: use real Slack session entries for reset removal

* fix(desktop): keep gateway reconnect available on open transports

* fix(desktop): reconnect only the active gateway route

* fix(desktop): retain manual recovery scope through retries

* fix(execute-code): teach the working helper import contract

Slim adaptation of #83772 to the current schema and failure-hint table.
Generated helpers are module exports on every execution path, not globals.
Correct schema, recovery hints and CLI tip rather than injecting names or
changing the execution boundary. Two registry-driven invariants reproduce
both misleading instructions on main and execute the corrected guidance.

Additional tool fix discovered during campaign #104904.
Original diagnosis and correction: @yuzilongleif-collab (#83772).

Co-authored-by: yuzilongleif-collab <235949691+yuzilongleif-collab@users.noreply.github.com>

* test: isolate updater fixtures from the live runtime fleet

* fix(cli): allow pinned installs to disable passive update checks

Adapt the config-only portion of #104347; omit its environment flag and unrelated docs. Explicit update commands remain independent.

Co-authored-by: Rohith Pariki <rohithpariki@gmail.com>

* fix(cli): preserve explicit dashboard update checks

Opt the two passive consumers into the config gate; retain default explicit checks and the existing dashboard caller contract.

* fix(desktop): preserve per-session chat scroll position

Rework per hermes-sweeper review (keep_open, salvageability=medium):

- Capture moves out of the render body into the session-switch layout-effect
  cleanup, so only committed switches persist state (no uncommitted render
  can write localStorage).
- Live state is fed by both scroll events and a ResizeObserver on the
  content element, so distance-from-bottom stays fresh under async
  relayout that changes height without a scroll event (the staleness gap
  #70478's own review threads flagged).
- State is distance-from-bottom (or sticky-bottom), not raw scrollTop:
  the render-budget backfill prepends older turns and main anchors by
  distance-from-bottom, so an absolute offset no longer identifies the
  same reading location after the height change.
- Restore integrates with main's hasGroups/settle/anchor lifecycle: the
  settle loop re-applies the remembered target, defers on a clamped offset
  (content still arriving), hands back locked only for sticky-bottom, and
  leaves mid-read sessions escaped at their offset.
- anchorBeforePrepend no longer records 0 mid-load, which would clobber a
  restored offset once the backfill lands; the settle loop owns the target
  every frame until settled.
- Storage is scoped per profile with the session.ts .profile.<encoded>
  key pattern (no cross-profile bleed, #67709 pattern) and LRU-capped at
  120 sessions per profile.
- Regression tests: state classification, target math, profile isolation,
  LRU eviction, corrupt/invalid payload handling.

Related to #45562 (partial; no automatic closure)

(cherry picked from commit 1d79bef93f63d7b190b471bf652b5109b4288a8e)

* fix(desktop): defer parked scroll restore until ready

* fix(desktop): scope saved scroll by gateway

* test(desktop): document and probe reading-position restoration

* fix(desktop): retain scroll ownership and honor hydration wheel intent

* fix(desktop): retain restored offsets through deferred layout

Observe post-settle transcript resizes until reader input or a live run takes ownership. Keep parked upward-wheel cancellation and namespace capture intact. Extend the maintained Chromium restoration probe with long reload and streaming controls; retain the two existing component invariants.

* fix: remove dedicated user-facing output cap controls

* test: exercise output-cap removal across native and child surfaces

* docs: record native output-cap save validation

* test: preserve metadata and passthrough assertions after cap removal

* fix(desktop): fold separators in model search so filters and highlights agree

Model ids use hyphens/underscores, display names use spaces, versions use
dots. The pickers' filter haystacks contained both the raw id and the
display name, but the highlight only ever saw the display label — so
'qwen3.8-flash' revealed the row (via the id segment) while lighting up
nothing, and model-picker.tsx had the inverse polarity (spaces matched,
hyphens didn't highlight). This violated HighlightMatches' own documented
contract: the query must mirror the filter's semantics or the emphasis
lies.

One length-preserving searchFold ([-_.] -> space, 1 char in / 1 char out)
now runs on both sides of every model-search filter AND inside
HighlightMatches' range finder. Length preservation keeps mark ranges
valid against the original text, so <mark> rendering is untouched. The
fold is a per-character substitution applied to both sides, so any query
that matched before still matches — only coverage grows.

- lib/text.ts: searchFold + foldIncludes, the one matcher for all pickers
- highlight-matches.tsx: ranges computed on folded text, marks slice original
- model-catalog-menu.tsx: family, MoA, and download haystacks use foldIncludes
- model-visibility-dialog.tsx, model-picker.tsx: same (fixes inverse polarity)
- dropdown-menu.tsx: DropdownMenuSearch sets spellCheck={false} — squiggles
  under model ids are noise; composer/settings inputs already disable it

Tests: fold primitives (equivalence, 1:1 length, superset), highlighter
fold behavior + index fidelity, end-to-end menu behavior (hyphen query
marks the spaced label; space query finds the hyphenated id without
over-matching), and an updated hidden-model-search test whose id-style
query now legitimately highlights.

* fix(desktop): fold the model-picker downloads filter too

Review-response sweep found one straggler: model-picker.tsx's
visibleDownloads filter still used raw toLowerCase().includes(), so a
hyphen-style query wouldn't match a space-separated download target —
the same bug class this PR fixes, in the same picker. The catalog
menu's equivalent filter was already converted.

* fix(desktop): scope separator highlighting to model searches

Keep command-palette literal filter semantics unchanged; opt model surfaces into the shared fold. Preserve original highlighter contracts and trim new regressions to two invariants. Native Electron catalog before/after and visibility dialog verified; campaign suites remain queued.

* fix(desktop): scope tool changes and commit rebuild ownership together

* fix: reject unavailable desktop profile and session targets

* test: enforce fail-closed project profile targeting

* test(desktop): stop the unread-tile test timing out on loaded CI

`session-unread-tile.test.ts` intermittently fails CI with
`Test timed out in 15000ms` at the first case. It has hit at least three
independent branches, `main` included, so it is not tied to any one change.

The cost is module reconstruction. `beforeEach`/`afterEach` both call
`vi.resetModules()`, so each of the three cases re-imports six modules,
including `@/lib/chat-runtime` and the pane-tree store. Locally that is
~3.3s on median but the tail reaches 11.8s (3.6x the median) on a warm
machine; a loaded CI runner pushes that past the 15s budget. One observed
CI run passed at 14614ms, 386ms under the limit, which is the same test
sitting on the wrong side of the same boundary.

Drop `resetModules` and undo the per-test state explicitly instead:

- collect the `registry.register` disposers and run them in `afterEach`,
  matching what `session-states.test.ts` already does;
- reset `$layoutTree` and `$activeTreeGroup` at the top of `setup()`.
  `declareDefaultTree` only seeds the layout when it is empty, so without
  this the second case would adopt the first case's tree.

The test keeps its teeth: reverting the `$focusedStoredSessionId` change
from a5b5043 still fails exactly the same two cases as before this patch.

Slowest of 30 consecutive local runs goes from 11.84s to 3.70s, with the
median roughly unchanged.

* test(desktop): keep cold module transforms outside UI assertion deadlines

* fix(deps): sync LAZY_DEPS platform.discord brotlicffi pin to 1.2.0.2

The previous commit bumped pyproject.toml and uv.lock but missed the
LAZY_DEPS exact pin for platform.discord, so
test_pyproject_pins_match_lazy_deps_pins and
test_every_lazy_deps_exact_pin_matches_uv_lock fail with
{'brotlicffi': {'platform.discord': {'lazy_pin': '1.2.0.1', 'uv_lock': ['1.2.0.2']}}}.
Update the third registration site to keep all three in lockstep.

* fix(deps): bump brotlicffi pin to 1.2.0.2 to fix chunked brotli streaming DecodingError

* fix: keep desktop provider setup within its selected profile

Remount scope-owned credential state on Applies-to changes and bind onboarding requests to their initiating route. Cancel polling and invalidate late results when setup closes or reopens, without undoing writes already sent.

Co-authored-by: By JTT <29462570+jordan-thirkle@users.noreply.github.com>

* fix(delegation): one completion per call by default; queued units no longer stalled; tell the model results land between turns

Three orchestrator failures traced through the Sep 7 gpt-6-astra campaign sessions:

1. delegation.independent_completions (new, default false). #104299 made every
   ungrouped task its own completion message, so a 15-task call woke the
   orchestrator up to 15 times; one chain received 132 notices and answered
   130 of them with "already incorporated". A multi-task call now returns as
   ONE consolidated message unless the flag is on; `group` is inert until then.

2. Queued units were killed before they started. Units of one call share a
   pool slot but the executor was still sized by slots, so with 15 units live
   a new unit queued behind a full pool; the stale monitor's clock ran from
   dispatch, interrupted it at 450 s, and the child exited `interrupted 0.02s`
   when its thread finally came up (13 such lanes in one session). The
   executor now grows to the number of live units and the stall clock arms
   when the runner actually starts.

3. The tool text said "do not wait or poll — just continue" without saying
   that completions are delivered only BETWEEN turns. A model that never ends
   its turn (one 203-minute turn, 717 API calls) never received 40 finished
   results. Tool description, dispatch note and completion header now say to
   finish independent work, give a one-line status, and end the turn.

* fix(delegation): keep the 'wait or poll' contract token in the tool description

* fix(desktop): isolate hidden composer selection

* fmt(js): `npm run fix` on merge (#105106)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(desktop): release Windows app locks at staged promotion

Salvage #101887 after native Actions 34097643131 reproduced WinError 32 using a ready Electron app with its cwd inside the live release. Reuse the existing install-scoped process cleanup before promotion and wait after forced termination, preserving rollback.

Co-authored-by: fangliquan <fangliquan@qq.com>

* fix(desktop): give the composer action pill a transit-safe hide delay

* fix(desktop): keep directive hover timers on owned boundaries

* docs: explain fenced hosted-room authority recovery

Document actual groups.promote/groups.demote parameters and required
old-writer fencing before confirmation. Demotion is a controlled rejoin
step, not an atomic promote-then-demote handover or log reconciliation.
Clarify replica coverage, confirmation meaning, and lineage readback.

Corrected redo of #104342; its nonexistent groups.peer methods and unsafe
handover ordering are not carried forward.

Fixes #104309
Refs #104904
Co-authored-by: Rohith Pariki <rohithpariki@gmail.com>

* fix(mcp): probe the list signature instead of masking its TypeError (#104150)

_paginate_full_list wrapped the paginated list call in try/except TypeError
to detect the mcp 1.x calling convention. The same except also caught
TypeErrors raised INSIDE the modern list call — e.g. a server response
decode failure — and retried with the legacy cursor= keyword, replacing the
real error with a misleading 'unexpected keyword argument cursor' and
making genuine MCP pagination failures undiagnosable.

Probe list_method's signature instead (_list_method_accepts_params): the
legacy cursor= fallback fires only when the method genuinely doesn't accept
the mcp 2.0 params= keyword (or takes **kwargs), so a TypeError from inside
the list call propagates to the caller. Regression tests: the decode
TypeError surfaces and the legacy retry doesn't run; a genuinely 1.x-shaped
method keeps using the cursor fallback.

* fix: keep pagination signature inspection local and preserve exact decoder error

* fix(tui-gateway): clear abandoned orphan interrupt claims

* fix(tui-gateway): start each detachment with a fresh settlement budget

* test(tui-gateway): add isolated websocket orphan reconnect probe

* fix(skills): pass the ClawHub owner hint as ?owner= so ambiguous slugs resolve

ClawHub's detail endpoint now answers a slug claimed by multiple owners
with 409 AMBIGUOUS_SKILL_SLUG; the bare GET in _skill_detail returned
None for every such slug, so 'skills install clawhub/@owner/slug' (and
the owner/skills/slug URL form) failed at fetch time even though the
requester already knew the owner (#104117).

- _skill_detail forwards expected_owner as the ?owner= query param on
  the detail GET (params already flows through _get_json's **kwargs).
- _parse_identifier also accepts the clawhub/@owner/slug combination:
  the @ surfaces only after the clawhub/ prefix is stripped, so the
  had_at check now re-runs on the stripped form. GitHub-style
  owner/repo/skill paths stay rejected.

* fix: retain ClawHub owner through version and bundle requests

* fix: accept the live ClawHub version-list response shape

* refactor: isolate Discord media upload methods before routing repair

* fix: route Discord cron media to its target and report upload failures

Adapt the earliest routing repair in scroasdale PR #44268 to the current media helpers, retaining metadata in URL fallbacks and refusing successful text-only receipts for failed local uploads. Also informed by jasondschoeman-pixel issue #104357 and ericmaddox PR #104760.

Co-authored-by: scroasdale <67333169+scroasdale@users.noreply.github.com>

* fix(agent): isolate custom endpoint billing health

* fix(agent): scope named custom health by endpoint

* fix(agent): quarantine bare custom aliases by endpoint

* fix(agent): quarantine failed fallback destination

* test(agent): update custom runtime mock contract

* fix(agent): preserve route URL path identity

* refactor: isolate custom health identity and trim redundant alias tests

* docs: clarify fallback credential and cooldown behavior

* test: publish credential-free fallback wire reproduction

* fix(desktop): honor explicit worktree sidebar dismissal

Slim salvage of #103992: preserve dismissal provenance as removed ids rather than a second metadata schema; discovery only overrides successful git removals. Explicit hide is durable even before a discovery scan finishes.

Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>

* fix(agent): surface armed rate-limit cooldown in fallback notice (#104120)

_arm_rate_limit_cooldown now returns the armed backoff seconds so
try_activate_fallback can append them to the user-facing notice
(Primary retried in ~N min/h) instead of discarding the one number
that decides whether the user waits or re-plans. Duration, never
wall-clock: the stored deadline is monotonic-based. Non-rate-limit
reasons and chain-switches from an active fallback arm nothing, so
no suffix is printed there.

* fix: describe remaining retry eligibility without promising recovery

* fix: retain exhausted-chain cooldown classification after extraction

* docs: clarify fallback credential and cooldown behavior

* fix: arm primary cooldown once while walking fallback candidates

* test: assert skipped candidates do not compound cooldown

* test: publish credential-free fallback wire reproduction

* fix(terminal): show sudo password prompts for paths and env prefixes

* refactor: extract Kanban graph persistence into topical sibling

* fix: retain Kanban decomposition identity and inherit parent tenants

* fix: keep Kanban worker scope out of descendant processes

Carry the existing write fence across Hermes-owned spawn boundaries without
dropping board routing or changing credential policy. Grant dispatcher and
managed tool runtimes explicit task scope; align CLI task mutations with tools.

Verify real shell/CLI descendants, dispatcher startup, and supervised stdio
transport against isolated SQLite boards. This is cooperative runtime scoping,
not OS confinement.

Refs #103974, #104058, #104904

* test: align delegated-child env tests with retained board routing

The descendant fence now keeps HERMES_KANBAN_DB/BOARD/WORKSPACE so a
fenced child can still read the board it belongs to; only worker identity
(TASK, RUN_ID, CLAIM_LOCK) is scrubbed. Three pre-existing tests still
asserted the DB var was dropped and went red on CI.

* fix(cli): planned systemd restarts no longer trigger failure alerts

Salvaged from #104272. Preserve restart and fatal-exit policy while classifying the planned restart code as success. Earlier analysis in #13604 by Justin Kausel.

* fix(desktop): route relative remote artifacts to gateway download

Salvage the focused renderer hunk from #104131. Preserve raw gateway tilde and relative paths at the authenticated file bridge; do not reinterpret remote paths on the client or widen external protocol handling.

Co-authored-by: Halldrix <12357213+Halldrix@users.noreply.github.com>

* fix(desktop): resolve artifact downloads in their originating session

* fix(gateway): retain original source fields in busy injections

Trim salvage of #104047: preserve the shared gateway injection boundary, but keep lossless JSON entirely per message. Do not change the system prompt, steer ABI, or synthesize a delivery target. Live handler-to-agent A/B verified; serialized directory tests and independent review pending.

Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>

* docs: describe gateway injection origin context

* fix(gateway): retain alternate and parent source identities

Address independent review: include existing alternate and parent routing fields, and distinguish event message ID from source message ID without inferring a reply target. Expanded live probe first failed on parent_chat_id, then passed with the additional fields.

* fix(gateway): honor privacy policy in busy message origins

Reuse the effective gateway config and shared session platform policy before hashing model-facing metadata. Preserve original routing state and cover enabled/disabled redaction across all busy injection routes.

* fix(gateway): resolve busy origin privacy in routed profile

* feat(cli): show entry id and priority in hermes auth list (#104636)

* refactor: isolate credential pool administration methods

* feat: reset one pooled credential without clearing sibling cooldowns

* fix: count credential selections across every pool strategy

* feat: choose pooled credential priority from the CLI

* feat: refresh one pooled OAuth grant from the CLI

* fix: place reauthenticated credentials by their saved identity

* test: exercise credential controls through PTY and local OAuth wire

* test: isolate external auth stores in pool command fixtures

* fix: reject independent Nous account refresh without clearing cooldown

* fix: prefer owned Anthropic grants and bind auxiliary refresh to request credentials

Port owned-before-borrowed resolution from #104624, crediting the root cause in #104622. Include the synchronous and asynchronous auxiliary fallback recovery sites: forward the failed request key so an unrelated borrowed login never owns that refresh. Local-wire probes preserve the borrowed file and exchange only the owned grant. Broader validation remains queued; do not treat this commit as ready.

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

Co-authored-by: d-bow-dev <24577047+dwb1991@users.noreply.github.com>

* test: exercise owned OAuth recovery through the unchanged ladder surface

* test: fix CI red — pool fixtures carry source, refresh assertion binds the failed key

Two test-only corrections for the failures on run 34105317301:

- tests/agent/test_anthropic_adapter.py::TestResolveAnthropicToken: the new
  skip_borrowed branch reads ``entry.source``. ``PooledCredential.source`` is a
  required dataclass field that ``from_dict`` always materializes (defaults to
  SOURCE_MANUAL) and ``_available_entries`` returns only PooledCredential, so a
  production entry can never lack it. The three SimpleNamespace doubles were the
  incomplete side; build them via ``PooledCredential.from_dict`` instead of
  duck-typing production with getattr.

- tests/agent/test_auxiliary_client.py::test_stale_anthropic_fallback_refreshes_and_retries:
  the PR itself now passes ``failed_api_key=<client.api_key>`` into
  ``_refresh_provider_credentials`` so an unrelated borrowed login never owns the
  refresh; the assertion still expected the bare ``("anthropic")`` call. Give the
  stale client an explicit api_key and assert the request-bound call. Main's
  auxiliary changes since the PR base (ebe4e7bb44, b40998bc3c..cb1a42d33b) did not
  move this call.

* fix: capability probes send minted credentials instead of callable representations

Extend #104477 to the native thinking, vision, metadata, and local header paths identified by #87641. Materialize only at probe boundaries; leave the chat callable and cache ownership untouched. Local-wire A/B: thinking and vision show requests change from 403 to 200, while static credentials and callable chat retain success. Target suites queued.

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

* fix: failed probe credentials cannot fall through to configured auth

* fix: remove automatic session JSON snapshots

* style: remove blank line left by snapshot setting removal

* fix(threats): keep unrelated role prose in context files

Salvage the bounded target-slot design from #104617, using mandatory
word separators to avoid ambiguous repeated matches. Preserve long
payload detection and execution-verb boundaries. Replace the three
candidate tests with two context-loader invariants and document the
heuristic's limits.

Fixes #104609
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>

* fix(codex): keep transport echoes out of durable user history

Port the exact submitted-wire-text ownership boundary from #93546 onto
current topical runtime code. Do not add the candidate's mocked-result
fallback or storage-level content deduplication. Preserve later distinct
and identical user events, separate identical accepted turns, and keyless
inputs. Add two regression invariants and offline subprocess-wire A/B.

Local wire A/B: 4/8 control matrix passing on base, 8/8 after.
Broader tests queued behind campaign lock; not ready for merge.

Refs #104653
Original diagnosis: @gitszabolcs (#38254)
Original implementation: #43127, submitted by @vashkartik
Focused salvage and wire-text correction: @fancyboi999 (#93546)
Current-main carry-forward considered: #104698

Co-authored-by: Xinmin Zeng <135568692+fancyboi999@users.noreply.github.com>
Co-authored-by: VECTOR <vector.hq@outlook.com>

* docs: record independently repeated input-ownership wire probe

* fix(gemini): route google-alias fallback providers through GeminiNativeClient

fallback_providers entries using the "google" alias for the gemini
profile were falling through to the generic OpenAI-SDK client because
the native-client gate only matched the literal string "gemini". That
client posts straight to the raw REST endpoint, sending thinking_config
as an unnested top-level field, which Gemini rejects with:

  Invalid JSON payload received. Unknown name "thinking_config": Cannot find field.

Broaden the check to the same alias set the gemini profile itself
registers (mirrors _GEMINI_NATIVE_PROVIDER_NAMES already used for this
in auxiliary_client.py).

Fixes #104583

* test(gemini): verify alias routing and compatible endpoint controls

* chore: map Charles Ji contributor email

* feat(honcho): add bounded opt-in current-query recall

* fix(honcho): isolate recall generations and reject unscoped fallback

* test(honcho): exercise config precedence through local SDK recall

* style(honcho): drop trailing blank lines at EOF in recall test

`git diff --check origin/main..HEAD` reported
`tests/honcho_plugin/test_recall_sync.py:130: new blank line at EOF.`
Whitespace-only; no test logic changed.

* fix(display): distinguish estimated context from provider usage

* test(display): exercise real usage and usage-less provider controls

* fix(display): retain provider provenance for persisted fallback readings

* fix(display): preserve localized estimate markers and headroom labels

* test(display): encode provenance contract in tilde assertions

Two tests still encoded the pre-PR behaviour that the PR removes:

- tests/test_tui_gateway_server.py: the mirrored fixture carries no
  `context_estimated` flag, so under the PR's rule it is provider-reported
  usage and must render without `~`. The old expectation asserted the
  unconditional tilde main used to emit. Assert the flag-less case is
  unmarked and, in the same test, flip `context_estimated` both ways to
  pin that only the estimate carries `~` in the count and the percent.

- ui-tui appChromeStatusRule.test.tsx: `text.includes('~')` matched the
  `~/repo` cwd label, so the "not estimated" arm was always true. Extract
  the rendered context token and assert the tilde on that token only.

* fix(approval): recover legacy list values without character grants

Recover legacy stringified lists with a warning. Reject malformed shapes and nonstring members without admitting approvals or rewriting user config on read.

Fixes #104779
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>

* fix(notifications): report applied skill batch operations

Use successful applied result records rather than requested operations, and keep staged writes silent. Include legacy delete/write messages.

Fixes #104506
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>

* fix(schemas): preserve required intent without invalid boolean flags

Normalize boolean required only at schema positions; lift true property flags into parent arrays. Preserve literal default/const/extension data.

Fixes #104796
Inspired by #104831 and the lifting proposal by @AdJIa.

* fix(bot-mode): preserve refusal reasons across local delivery

Emit the one-shot reason marker outside the CLI facade; parse whole codes before falling back to legacy prose. Explicit coordination and unknown codes cannot be labeled target_busy.

Fixes #104784
Co-authored-by: William Echo <2054936695@qq.com>

* fix(cron): preserve continuity across silent audit ticks

Slim redo of #104546 and #104551: scan newest-first, match suppression only before payload separators, and keep error context. Covers wake gates and empty outputs without reading every historical file twice.

Co-authored-by: PRATHAMESH75 <prathamesh290504@gmail.com>

Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>

* fix(cron): make failed runs diagnosable without verbose delivery errors

Persist a redacted chained traceback in the private run output and expose
redacted last_error in tool and slash listings, including historical errors.
Keep the run_job concise error return unchanged for delivery classification.

Slim redo of liuhao1024's earliest #104545; adds forced redaction and keeps
formatting in a topical sibling. Local SDK/socket A/B verifies diagnosis
visibility plus healthy-script, clearing, and private-file controls.
Canonical tests queued under the campaign lock at commit time.

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

* test(cron): exercise historical error redaction in live probe

* fix(process): stop late forks escaping deadline tree cleanup

* fix(update): let systemd clients outwait legitimate unit transactions

Salvage the unit-budget implementation from #104745, replacing its test
matrix with two invariant tests and covering the sibling graceful start.
Keep unprivileged property reads, finite fallbacks, real manager errors,
and post-restart health verification.

Native disposable user unit: old client timed out after 15.03 seconds;
new client completed the same 16-second stop transaction in 16.13 seconds.
The unit stayed active with a new PID; missing-unit errors stayed errors.

Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>

* chore: credit systemd restart budget contributor

* test: retain native systemd restart budget probe

* fix(update): cover catch-up restart clients with the unit budget

* test: pass snapshot listings to the catch-up restart budget probe

Main now snapshots systemd unit listings before stopping old processes
and passes them into _restart_systemd_gateway_units_best_effort; the
catch-up budget test and eval call the new two-argument shape while
still asserting the unit's stop+start budget reaches the client timeout.

* fix(update): settle stale receipt warnings from matching live gateways

Reconcile receipt-only restart obligations at the shared warning/catch-up
predicate, requiring every historical runtime/profile identity to have a
current live gateway successor. Preserve missing and unknown obligations,
non-gateway identities, and independently authoritative pending markers.

Keep failed receipts unchanged instead of recording an unverified success.
Live isolated two-process A/B reproduces the warning on base and settles
it after the fix; stale, unknown, and missing-profile controls still warn.

Reported-by: duanzhiwei0315
Inspired-by: zengzheqing (#104295), RootZ3n (#100249)

* fix(cli): probe update-check origin URL under the fetch's isolated git env

The startup update check resolved `git remote get-url origin` with the
user's global config in scope while the subsequent fetch runs under
noninteractive_git_env (GIT_CONFIG_GLOBAL=/dev/null). A global
url.<https>.insteadOf rewrite therefore made an SSH origin masquerade as
HTTPS, the SSH-avoiding fast path was skipped, and the fetch dialed the
raw SSH origin — whose host-key prompt opens /dev/tty directly and
steals the CLI's keystrokes (#104591).

Probe the origin URL under the same isolated env so both sides observe
the URL the fetch will actually dial.

* fix(cli): pin core.sshCommand to BatchMode ssh in the noninteractive git env

ssh bypasses stdin=DEVNULL and GIT_TERMINAL_PROMPT: when a git child
dials an SSH remote whose host key is unknown, ssh opens /dev/tty
directly and its yes/no prompt steals the caller's terminal — exactly
what noninteractive_git_env exists to prevent. Pin core.sshCommand to
"ssh -o BatchMode=yes" at the config-injection layer so the ssh child
fails instead of prompting; an agent-authenticated ssh still succeeds,
and an explicit user GIT_SSH_COMMAND env var still takes precedence
(#104591).

* test(cli): pin git identity in the insteadOf regression test setup

The regression test builds its scratch repo under GIT_CONFIG_GLOBAL/
GIT_CONFIG_SYSTEM = /dev/null, so the init commit has no configured
identity. On CI runners the auto-detected ident is rejected
(user@<bare-hostname>.(none)) and 'git commit' exits 128, failing the
test that passes locally. Pin user.email/user.name on the commit, the
same pattern the install-script tests already use.

* test: verify SSH update checks with real PTY authentication controls

* docs(desktop): document directive action hover grace

* feat: authorize MCP servers with device codes from the CLI

Add explicit RFC 8628 device login and oauth.flow selection while keeping
browser PKCE and the SDK runtime refresh path. Reuse issuer/resource
validation, configured client authentication and profile-scoped storage.
Only persist an approved, validated grant; never echo endpoint error bodies.

Slim redo of #104752 by @wjorgensen, replacing duplicate HTTP/storage
wrappers with the existing SDK and two real-wire invariant tests.

Refs #104742
Co-authored-by: Wes Hermes <weshermes@Wess-Mac-mini.localdomain>

* fix: restore prior device OAuth state if persistence fails

* fix: ignore malformed MCP OAuth metadata caches

Guard device metadata subtype selection with a dictionary check so valid
non-object JSON reaches the existing validation-and-ignore path. Preserve
null handling, cache contents, and normal/device metadata cold-load types.
Extend the existing corrupt-cache invariant rather than adding test functions.

Live filesystem A/B reproduces list/string/number/boolean AttributeError on
the prior head and clean ignore after this change. Nine CLI OAuth wire
scenarios, browser S256 and profile-scoped storage controls pass locally.

* test: keep quickstart preflight independent of host acceleration

* fix(cli): hide the console window for banner/update git probes

`hermes_cli/banner.py::_git_run` is the shared spawn path for every banner and
passive update-check git probe (rev-parse, rev-list, remote get-url, ls-remote).
It carried the UTF-8 text contract but not `creationflags=windows_hide_flags()`,
so on Windows each probe run from a GUI-hosted backend (desktop-spawned
`hermes serve`, `tui_gateway` import kicking off `prefetch_update_check`) flashes
a console window. Every other short-lived helper in `hermes_cli/` already passes
the flag; this brings the banner path in line.

Surfaced by CI on this PR: the stray `git ... origin` spawn from the prefetch
daemon landed in `test_env_probe_run_hides_console_window`'s process-wide
`subprocess.run` capture and tripped its call-count assertion. The test now
scopes its assertion through the module's `_spawns` helper like its siblings,
so an unrelated daemon spawn cannot fail it (the production fix is what makes
that stray spawn carry the flag in the first place).

A/B: base `_git_run` -> no `creationflags` kwarg; fixed -> 0x08000000.

* test: keep core review checks independent of optional ACP

* fix: structured reasoning no longer breaks chat consumers

Normalize incoming reasoning at the shared heading boundary and completed
extraction, and flatten auxiliary content and reasoning before accumulation.
Reuse the existing text flattener with no implicit fragment separators.

Combine the earliest related work from zsuroy (#85791), the diagnosis and
patch from 2025hcsmile2010-hue (#104711, #104848), and completed extraction
work from liuhao1024 (#104717) as a slim redo, not a verbatim cherry-pick.

Two invariant tests exercise the real SDK and local HTTP fixture across
main streaming, Relay collection, auxiliary sync/async and completed output.
The standalone matrix improves from 32/84 to 84/84, preserving answers.

Co-authored-by: suroy <suroy@qq.com>
Co-authored-by: 2025hcsmile2010-hue <2025hcsmile2010@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>

* chore: credit reasoning normalization contributors

* refactor(tools): extract process checkpoint persistence

* fix(tools): retain completed background process results across exit

* fix(tools): keep retained result reads off live status scans

* fix(tools): register readers atomically with completion

* fix(tools): keep retained terminal results scoped to their owner

Capture the durable parent session before output readers start, including CLI
and non-notifying spawns. Require that parent or its compression continuation
for retained reads; exact and prefix handles alone do not authorize access.

Live Linux terminal/one-shot linger/fresh-reader A/B: base loses results;
updated owner recovers both streams and exit 7. Unbound, foreign session,
delegated child, and other profile cannot recover the receipt. No notifications
are replayed. Full tools suite is queued behind the campaign test lock.

Follow-up to contributor salvage #104805 for #104511.

* fix(tools): retain producer profile scope in process readers

* fix(tools): always redact durable process receipts

* fix: coalesce interactive completion backlogs without losing identity

Based on the earliest batching proposal by BrunoBza (#104686) and JoaoMarcos44 structured correction (#104703). Slim redo into topical siblings and shared TUI poller/post-turn routing. Reported by Xipong (#104671). Local shell-to-loopback probes demonstrate 12 to 1 turn dispatches; suites await the campaign test lock.

Co-authored-by: joaomarcos <joaomarcosdias444@gmail.com>

* test: verify completion barriers and document local delivery proof

* fix(tui): post-turn completion drain resolves ownership once; barrier test uses a watch

Rebasing onto main surfaced three reds in tests/test_tui_gateway_server.py.

Two were a real double-check: _run_post_turn_followups already drains the
completion queue through _session_owns_notification_event, then handed the
events to _notif_handle_ready, which re-ran the belongs-elsewhere and
requires-owner gates on every event — a second compression-lineage DB
lookup per notification for events the caller had just proven ours.
_notif_handle_ready/_notif_handle_event take an explicit owned=True from
the post-turn path and skip only the ownership gates (consumed/dedup/turn
claiming still run). The poller path is unchanged (owned defaults False).

The third, test_run_prompt_submit_requeues_all_unstarted_notifications_with_real_threading,
asserted that three consecutive completions produce one in-flight turn plus
two requeued events; under this PR's contract (#104671) consecutive
completions share one turn, so the first event is now a watch_match — the
documented turn barrier — and the test keeps proving that the two
completions behind an in-flight notification turn are never lost.

A/B: revert the tui_gateway hunks and the two lineage tests fail with
ownership_checks recorded twice; restore and all 11 test_run_prompt_submit
tests pass.

* feat(cron): create paused jobs without a scheduling race

Persist paused state, timestamp, reason and no first trigger in the original
locked creation write. Forward the same boolean contract across CLI, tool,
gateway API and dashboard API, validating at the store boundary. Preserve
explicit operator force-run behavior and normal enabled creation.

The live CLI probe also caught the command shim dropping failure return codes;
forward them so invalid creation reports exit 1 rather than success.

Credit earlier atomic-creation work in #78935 and #94952 and the focused
implementation in #104578. The broader manifest staging layer is not imported.

Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
Co-authored-by: Chloé DuPont <321112755+misschloedupont@users.noreply.github.com>

* test(cron): preserve rejected creation exit status

* fix: checkpoint Kanban completion before tool access expires

Give dispatcher-owned workers a tool-capable reporting opportunity before the
hard iteration cap, without accepting arbitrary diffs or weakening failure
counting. Add opt-in per-turn iteration checkpoints for ordinary agents.
Persist checkpoint text with the fresh tool result, never rewrite cached rows.

Salvages the opt-in ratio and per-turn reset implementation from #104683;
credits the earlier default-off signpost proposal in #92438.

Local fixture wire A/B: Kanban ready/1 failure -> done/0; deliberately stuck
workers still reach blocked/2 after two runs. Default-off control unchanged.
Targeted and affected-directory suites queued behind campaign test lock.

Co-authored-by: fangliquanflq <fangliquan@qq.com>
Co-authored-by: C. Michael Gibbs <252231331+MikeGibbsOnyx@users.noreply.github.com>

* test: pin checkpoint transcript persistence before the next request

* fix: keep budget checkpoints out of cancelled tool results

Skip checkpoint evaluation when a turn is interrupted so cancellation rows
remain durable without urging continued execution. The existing minimal-agent
interrupt regression also avoids dereferencing an absent iteration budget.

Consolidate the warning coverage into two invariants, including real SQLite
readback and dispatcher/child scope controls. Cold-start tool availability
between construction cases to model independent worker processes. Place ratio
normalization beside the existing iteration budget instead of growing init.

Real cancelled-tool A/B against current main, draft, and fix: three cancelled
rows and zero writes on all arms; persisted checkpoint notices 0 / 1 / 0.
Repeated scripted HTTP/SQLite loop A/B preserves completion opportunity,
ordinary default-off behavior, and blocked/two-failure exhaustion behavior.

Local targeted run initially passed 15 cases with one fixture cache-isolation
failure; corrected target and inherited affected suites remain queued behind
the campaign lock. This commit is not a CI-green or merge-ready claim.

* feat(openai): add GPT-6 Astra baseline support

(cherry picked from commit a8c53d20c6b16cc35745e364e16bb7259166a1d3)

* test(openai): close Astra baseline review gaps

(cherry picked from commit 8c27b7c9316ba675e4d9ebcc7a659754f37f18ef)

* fix(openai): keep Astra rules on eligible routes

(cherry picked from commit f92fb9d9964e6e8ea118035c548aae812054f504)

* fix(openai): require canonical host for Astra cache

(cherry picked from commit 6e73cc5cc66e4003276c4472e6ce2d77e602f130)

* fix(openai): cap Astra Codex OAuth fallback

(cherry picked from commit bb4156c3881cbaf20736f0fcfd6c3cc5c861a6fb)

* feat(models): preserve live-verified Astra 900K opt-in from #103132

Retain the two context-variant metadata additions by Michael Steuer. Keep the dedicated Astra reasoning contract already present in #103057 rather than replacing it with the GPT-5.6 vocabulary.

(cherry picked from commit add3a4fa6f31ea3f5fdad701a840d4f8530eb30e)
(cherry picked from commit 1672f12c260260ea38ed636528c02aee734d62b1)

* fix(openai): keep Astra 900K alias gated and wire-compatible

(cherry picked from commit c7cd27d7f050598b9dc052c73fa1dc78c045d3c6)

* fix(openai): revalidate Astra at cached and saved-model picker boundaries

(cherry picked from commit f46e2f4c0609c1543d5636c780c6da0d5ce09a56)

* fix(openai): include canonical API picker identity in Astra discovery gate

(cherry picked from commit de57e601193a955a69201e1a274d5ccab83a7634)

* fix(openai): drop prompt_cache_options from Astra requests — not an SDK kwarg, 30m is the server default

Every direct-API (api.openai.com) Astra request raised
``TypeError: Responses.create() got an unexpected keyword argument 'prompt_cache_options'``
before reaching the network: openai 2.24.0's Responses.create has no such parameter and no
**kwargs, and neither send path relocates it into extra_body. The PR's tests stopped at
build_kwargs/preflight so the SDK boundary was never crossed.

OpenAI's prompt-caching guide states ``prompt_cache_options.ttl`` accepts only ``30m`` and that
``30m`` is the default, so the field carried no information: sending nothing yields the same
cache lifetime. The sanitizer now only removes what the API rejects (none/minimal effort,
sampling/logprob knobs, the pre-5.6 ``prompt_cache_retention``) and never adds a field, which
also keeps the request body byte-stable for the cache prefix.

Also: none/minimal→low no longer needs a bespoke {"", "none", "disabled", "off"} set —
``clamp_effort`` against CODEX_ASTRA_EFFORTS already resolves to the floor (``low``); and the
auxiliary adapter derives ``is_codex_backend`` from ``classify_responses_route`` (the declared
single owner of that predicate) instead of re-implementing the host test inline.

Tests reshaped to contracts: the two proxy/subdomain cases collapse into one parametrised
"exact host only" test asserting effort and temperature pass through untouched.

* refactor(openai): one is_astra_model predicate; gate Astra at the untrusted Codex inputs

The slug pair {"gpt-6-astra", "gpt-6-astra-900k"} was spelled out five times
(reasoning_effort, transports/codex, models.py x2, codex_models) with five hand-rolled
``.strip().lower().rsplit("/", 1)[-1]`` normalisations. ``is_astra_model`` in
agent/reasoning_effort.py (the module the other four already import) is now the only home, so a
new Astra alias is one edit.

``_finalize_codex_models(..., allow_astra=)`` was a control-coupling flag set True by the two
live callers and left False by the two static ones. The filter now lives where the untrusted
inputs are — ``_drop_undiscovered_astra`` over config.toml default + models_cache.json in
``get_codex_model_ids`` — and ``_finalize_codex_models`` is back to its one-line original.
DEFAULT_CODEX_MODELS never contains Astra, so the static catalog needs no gate.

``_openai_catalog`` appends whatever Astra ids live discovery returned instead of a name-keyed
``if "gpt-6-astra" in live_lower`` branch with a literal fallback that could never fire.

* perf(models): stop revalidating a fresh provider cache just because it lists Astra

``gated_cache`` bypassed both the fresh-hit and the stale-while-revalidate branches of
``cached_provider_model_ids`` whenever the disk entry contained Astra. For an entitled
account that is every entry: live discovery rewrites the entry with Astra → next call is gated
again → a blocking /models round-trip on every picker open, for exactly the providers the user
is most likely on. The parallel prefetch's staleness check didn't know about the gate either, so
it skipped the slug and the blocking call landed in the serial picker loop the prefetch exists to
avoid.

Gate on provenance, not contents: only live discovery ever writes Astra into a same-credential
entry (the static/offline paths filter it out), so a fresh entry IS the entitlement record. The
one filter that matters stays — a stale entry served because the refresh failed drops Astra, and
the entry itself is left intact so the next successful fetch restores it.

The regression test now pins both halves: fresh entry served with Astra and zero live calls;
failed refresh past the fresh window serves the entry minus Astra.

* refactor(inventory): build the gated-Astra warning tail instead of string-replacing a sentence

`warning.replace("Showing the saved model only.", ...)` silently no-ops the day that sentence is
reworded. Pick the tail sentence first, format once.

* test(openai): Astra pricing/capability tests assert contracts, not snapshots

``amount_usd == Decimal("5.450025")`` and ``pricing_version == "openai-gpt-6-astra-2026-09"``
fail on the next price or version bump without catching a bug. The whole-request tier is the
contract: above 272K prompt tokens every component — including cache writes, the field this PR
adds — is billed at its ``*_above`` rate, so derive the expected total from the entry's own rates
and assert below < above. Likewise the builtin-metadata test keeps only what
``_UNKNOWN_MODEL_BASE`` could not have supplied (vision) plus the openai-api == openai parity.

* fix: resolve the 33 F821 undefined names outside tui_gateway / feishu / godmode

Sweep of `ruff check . --select F821 --target-version py311`: 2,234 hits. 2,201 are left
alone on purpose: tui_gateway (2,169; bind_module rebinds bodies onto server.py globals,
all names verified to resolve there), the Feishu adapter (27; globals().update() SDK
binding) and the godmode script (5; dead standalone script). The other 33 were all
genuine defects. No lint config change; no TYPE_CHECKING escape hatches — every
annotation names a real, imported type; ty on the touched files: 0 new diagnostics.

- gateway/slash_commands.py: HISTORY_UNREADABLE never imported after #102117
  → NameError on the /btw error branch (same one-liner as #102952).
- gateway/platforms/whatsapp_common.py: `-> Path` return annotation with no Path
  import (the body uses `_Path`). Never raised at runtime thanks to
  `from __future__ import annotations`, but `typing.get_type_hints()` and ty
  both fail on it.
- gateway/run.py: ActivityProvenance imported at module level
  (agent.session_activity has no gateway deps); stringly annotation and the
  lazy in-function import are gone.
- tools/patch_parser.py: PatchResult imported at module level; real return
  annotation. The "avoid circular import" lazy import guarded a cycle that
  does not exist (file_operations_common never imports patch_parser).
- gateway/platforms/helpers.py: base.py imports helpers at module level, so
  MessageEvent cannot be named here; TextBatchAggregator only reads .text and
  .source, so it is typed by a BatchableEvent Protocol that MessageEvent
  satisfies structurally.
- tools/mcp_tool_sampling.py: mcp_tool imports this module, so MCPServerTask
  cannot be named here; ElicitationHandler only reads
  owner._pending_call_context, typed by an ElicitationOwner Protocol.
- plugins/platforms/sms/adapter.py: aiohttp is an optional dep ([messaging] extra) →
  module-level try/except ImportError binding `aiohttp = web = None`, the pattern the
  homeassistant / webhook / whatsapp_cloud adapters already use. Retires three lazy
  in-function imports and the `_aiohttp_available()` wrapper; `_handle_webhook` typed
  `web.Request -> web.Response`.
- plugins/platforms/teams/summary_writer.py: plain module-level `import httpx` — httpx is a
  hard core dependency (pyproject `httpx[socks]==0.28.1`), so the lazy import and the
  "imported on every CLI start" docstring premise were both wrong (plugin discovery never
  imports this module; it is reached only via the Teams adapter / meeting pipeline).

Tests:
- tests/hermes_cli/test_config.py: a test body orphaned by the wave-1 prune
  (6b81590c55) sat inside the class as dead code with self/tmp_path unbound
  — header restored, so the v11→12 custom_providers migration is covered.
- tests/tools/test_mcp_tool.py: @staticmethod recursing on `self` in the
  win32 branch; call portalocker directly.
- tests/test_background_review_list_shapes.py: main() still ran 3 pruned tests.
- tests/agent/test_cursor_optimizations_parity.py: bench() used names only
  imported inside a sibling test.
- GatewayRunner / FeishuAdapter / Dict / Optional: missing imports.

* refactor: MessageEvent to gateway/platforms/event.py; ElicitationHandler takes a call_context thunk

Breaks the two import cycles that forced Protocol stand-ins in the F821 sweep, so the two
sites now name the real types.

gateway/platforms/event.py (new leaf): MessageType, ProcessingOutcome, MessageEvent moved
out of base.py verbatim. Their only dependency is gateway.session.SessionSource; base.py
imported helpers.py at module level, so helpers could not name MessageEvent. Now
TextBatchAggregator is typed by the real MessageEvent. 249 importers repointed
(`from gateway.platforms.base import` -> `.event`, preserving each import's layout);
gateway.platforms.__init__ re-exports from .event. The three revert-scheduled PLUGIN-COMPAT
pointers that named these symbols (gateway.slash_commands → MessageType, dingtalk → MessageType,
photon → ProcessingOutcome) and their COMPAT_MANIFEST rows now target gateway.platforms.event.
Docs updated: ADDING_A_PLATFORM.md, adding-platform-adapters.md (en + zh-Hans).

tools/mcp_tool_sampling.py: ElicitationHandler no longer holds a back-reference to its
MCPServerTask (mcp_tool imports sampling, so the task type cannot be named there). It only
ever read owner._pending_call_context, so it takes `call_context: Callable[[], Context | None]`
and MCPServerTask passes `lambda: self._pending_call_context`. The consent call is one
`functools.partial`, run directly or inside the captured Context.

ty on the 11 touched production files vs origin/main: 0 new diagnostics, 14 resolved.
(The one `source: SessionSource = None` diagnostic moves with the class; typing it Optional
exposes ~60 unguarded call sites — separate follow-up.)

Tests: tests/gateway + tests/plugins + tests/tools + touched files, 18,235 passed; the 31
failures reproduce identically on origin/main (macOS /private/tmp, systemd socket,
long-path fixtures, live-service tests).

* refactor: sms AIOHTTP_AVAILABLE flag; ElicitationHandler call_context defaults to a no-op thunk; drop stale TYPE_CHECKING/type-ignore in two tests

Self-review follow-ups on the F821 sweep:

- plugins/platforms/sms/adapter.py: the optional-import block now sets AIOHTTP_AVAILABLE like
  the homeassistant / webhook / whatsapp_cloud adapters, and both call sites test the flag.
  Removes the `if not aiohttp is not None:` double negation left by inlining
  `_aiohttp_available()`.
- tools/mcp_tool_sampling.py: `call_context` defaults to `lambda: None` so the use site is a
  single call instead of an Optional guard; the only None caller was a test. The
  `from __future__ import annotations` was noise (`Context` is a runtime import). Comment
  names the actual cycle (mcp_tool_server_run imports this module).
- gateway/platforms/helpers.py: drop the `from __future__ import annotations` — the only
  MessageEvent annotations are attribute-target locals, which are never evaluated.
- tests/gateway/test_telegram_audio_vs_voice.py, test_video_context_note.py: module-level
  `from gateway.run import GatewayRunner` like the ~100 sibling files; the
  TYPE_CHECKING block + `# type: ignore[name-defined]` were contradicting each other.
  (tests/e2e/conftest.py and test_feishu.py keep TYPE_CHECKING deliberately: they stub
  telegram/discord before importing, and FeishuAdapter is gated on optional lark_oapi.)

Mutation check: neutralising the thunk read (`captured = None`) fails
test_captured_context_is_replayed_in_consent_call; restored → 14/14 green. ty on the three
touched production files vs origin/main: 0 new, 6 resolved.

* test: /btw replies HISTORY_UNREADABLE on a failed transcript read (from #102952)

Regression guard for the one-line import fix in commit 1; identical to the test in #102952,
whose /btw half is superseded by this PR. Red on origin/main (NameError), green here.

* test: e2e conftest imports GatewayRunner at module level; elicitation test docstring matches the default thunk

Same cleanup as the previous commit, applied to the third file that carried the
TYPE_CHECKING + in-function import pair. gateway.run imports none of the telegram/discord/
slack modules the conftest stubs, so import order is not a concern. test_feishu.py keeps its
TYPE_CHECKING import on purpose (FeishuAdapter is gated on optional lark_oapi).

* refactor: repoint 10 MessageEvent/MessageType importers that landed on main after the event.py split

Five test files and five evals scripts merged since this branch's base still import the
moved names via gateway.platforms.base. They work (base.py imports the names for its own
use) but the PR's invariant is that in-tree code imports from the defining module.

* fix(desktop): task panel follows the todo_list wire name

The core-tool rename shipped `todo_list` on the wire (legacy alias `todo`
kept for old transcripts), but the Desktop renderer still matched the tool
by the literal `todo` in seven places: the live tool.start/tool.complete
mirror into the composer status stack, the todo-stream router, args
carry-over, the transcript hoist, the silent-tool class, the count noun,
and stored-history hydration. Every live task update therefore went into
the transcript as an ordinary tool row while the task panel stayed empty,
and reopening a chat never restored a finished list.

One predicate (`isTodoToolName`) now owns the wire/legacy name pair and
every site reads it.

* fix(tui-gateway): subagent lifecycle survives display.tool_progress=off

`_on_tool_progress` bailed on the tool-progress gate before dispatching
`subagent.*`, so a Desktop/TUI user who hid tool-call chrome also lost the
subagent rows in the status stack and spawn tree. Subagent lifecycle is
application state (like `todo.updated`, clarify and MCP consent cards,
which already bypass the gate); the gate now applies only to the optional
progress chrome (reasoning previews, MoA rows, tool.generating).

* fix: address PR 59 review findings

* docs: keep provider endpoints in config

* ci: rerun pull request checks

* ci: rerun checks with actions enabled

* fix: address latest PR review findings

* test: cover backend dial routing behavior

* fix: address follow-up review findings

* fix: address final review findings

* ci: repair attribution and scanner workflow

* fix: remove stale web backend configuration

* ci: avoid neutral osv code scanning status

* fix: preserve scheduled osv sarif publication

* ci: disable osv sarif on pr caller

* fix: preserve pooled backend claim scope

* fix: complete acp tool and osv routing

* fix(ci): isolate osv sarif from pull request scans

* fix(ci): preserve pr osv upload opt-out

* fix: harden fast mode and config integration tests

* ci: use standard hosted runners

* test: fix desktop and sessions CI failures

* test: wait for virtual history compensation

* ci: allow full Python suite to finish

* ci: limit Python test parallelism on standard runners

* ci: shard Python tests across standard runners

* test: fix CI-only Python regressions

* ci: shard JavaScript and TypeScript checks

---------

Co-authored-by: fangliquanflq <fangliquan@qq.com>
Co-authored-by: crazyief <8566250+crazyief@users.noreply.github.com>
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: saotu <160758706+saotu@users.noreply.github.com>
Co-authored-by: elvindu <dumanxiang@qq.com>
Co-authored-by: CloudWishOS <99405975+snipecoder@users.noreply.github.com>\nCo-authored-by: BGwill-OUTLOOK <bgwillwork@outlook.com>
Co-authored-by: Pasquale Minervini <p.minervini@gmail.com>
Co-authored-by: Rohith Pariki <rohithpariki@gmail.com>
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
Co-authored-by: Filipe Bezerra <bezerra@live.com>
Co-authored-by: yuzilongleif-collab <235949691+yuzilongleif-collab@users.noreply.github.com>
Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
Co-authored-by: Victor Nogueira <victornogu80@gmail.com>
Co-authored-by: anhtahaylove <everest.kill1@gmail.com>
Co-authored-by: By JTT <29462570+jordan-thirkle@users.noreply.github.com>
Co-au…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants