11c50f05d070b2158afdca73f077d2501ecd77fc
46 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
20fb755609 |
refactor(gateway): single StreamingConfig.enabled_for gate for both streaming sites (#53697)
Both the TurnRunner and proxy paths now call StreamingConfig.enabled_for(), so the master-switch + per-platform override logic lives in one place. The proxy path also returns before re-reading config.yaml when the global switch is off. |
||
|
|
ad5271b217 |
fix: gate per-platform streaming behind global streaming.enabled (#53697)
The effective streaming resolver in gateway/run.py had two sites where per-platform display.platforms.<plat>.streaming values were used directly without checking the global streaming.enabled master switch. When was set globally, the Telegram platform still had streaming enabled because its per-platform default bypassed the global gate. Fix: introduce an intermediate variable that combines and , then AND it with the per-platform override (or default-true when no override exists). This ensures streaming.enabled: false acts as a hard global kill switch regardless of per-platform defaults or overrides. Affects two resolver sites: - Line ~14854: session streaming setup - Line ~16034: stream consumer initialization (cherry picked from commit 2b210eb2a0e0b9421dae9e3c1984ceb59760e3a3) |
||
|
|
4774bb0a1a |
fix(gateway): dedupe non-editable interim finals
(cherry picked from commit 9084be9bd97df053967943d6aa0b5f7b9e7e4b48) |
||
|
|
a8cc840372 | chore: merge origin/main (resolve gateway/run.py) | ||
|
|
4b0dd7e9a7 |
fix: quota-exhausted 429 is no longer reported to chat as a sign-in failure
Rate-limit now wins over the auth pattern in the gateway's provider-error reply table, `401` only matches as a standalone status token (a bare `\b401\b` also hit timestamp fragments like `05:14:15,401`), and the rate-limit reply names the reset window when the envelope carries `resets_in_seconds` or `retry after Ns` instead of "wait a moment" for a weekly quota. The pre-turn credential-resolution failure on chat surfaces and the api_server `_ProviderAuthResolutionError` label follow the same rule via `is_rate_limited_auth_error` on the cause chain. WHY: a quota/429 envelope often also carries an auth-shaped preamble and "Credentials are still valid"; classifying it as auth sent operators to re-login working credentials across profiles (#89401). |
||
|
|
417d0c0f6a |
refactor(gateway): run_turn_runner resolves adapters through the intake/delivery seams
Mechanical migration of the `_adapter_for_source` call sites in this sibling. Intake policy sites take `_intake_adapter_for`; every send/edit/typing/pending-slot site takes `_delivery_adapter_for`. Part of #88715 (phase 4). |
||
|
|
38241942df |
fix(gateway): surface fallback notice when credential resolution falls back to secondary provider
When the primary provider fails with AuthError during gateway credential resolution (before AIAgent is constructed), the fallback provider is silently used with no user-visible notice. The existing _pending_fallback_notice mechanism only covers in-conversation-loop fallback activation, not the pre-agent gateway path. Fix by: 1. Capturing primary provider/model from config in _resolve_runtime_agent_kwargs() before the AuthError try block 2. Adding _fallback_notice metadata to the returned fallback dict 3. Popping it in TurnRunner.run_sync() before forwarding kwargs 4. Setting agent._pending_fallback_notice after agent creation/cache The existing _emit_pending_fallback_notice() mechanism then surfaces the notice on the next turn, deduplicating naturally. Fixes #74349 (cherry picked from commit 7d66877ecb4718dbfebc7729e7a0ea96d8eab174) |
||
|
|
3a8b82362f |
fix(gateway): escalate a persistent transcript-lag streak to ERROR
'Persisted transcript lagged live cached history' repeated at WARNING on every turn for 11 days in #114266. _load_turn_history now keeps a per-session-key streak on the runner (cleared on /new via _CONVERSATION_SCOPED_STATE); after _TRANSCRIPT_LAG_ESCALATION_TURNS consecutive lagging turns the line logs at ERROR with an operator-attention suffix, and a caught-up turn resets it. |
||
|
|
94a029091d |
refactor(notifications): one notice classification, one config read, one diagnostic-metadata helper
- `is_diagnostic_notice()` replaces three drifting copies: the gateway muted every
`credits.*` notice, the TUI and CLI only `warn`/`error`, so `credits.restored` was hidden
on Telegram and shown in the TUI for the same config. Every credit-service notice is an
automatic diagnostic (a "restored" line after a hidden depletion notice is orphan noise).
- `effective_user_config()` is the single fail-open effective-config read; the two extra
`deepcopy`s per foreground turn go (the loader already returns a fresh copy and the
snapshot is read-only).
- `diagnostic_metadata(event)` replaces the repeated
`{"notification_category": "diagnostic"} if event.internal and ... else {}` literal in
gateway/run_turn.py; the gateway-side imports of the resolver are module-level (no cycle:
it imports only gateway.display_config).
- `display.suppress_warning_notifications` is listed with its sibling display keys in
cli-config.yaml.example and the configuration reference; the messaging guide states that a
muted diagnostic wake still runs (and bills) its agent turn.
|
||
|
|
70addd3522 |
fix(gateway): keep default-off byte parity; scope only the policy reads
Four places changed behaviour for users who never touched the setting: - `_interim_send` was stamped on every `warn` status and media-failure notice, and the Slack/relay egress doors learned to skip stream sealing for it. Main's status sends carry no interim mark at all, so the gap is class-wide (every status kind), and fixing it for warnings alone is an undeclared streaming-contract change. Reverted here; the whole-class fix belongs in its own PR against gateway/AGENTS.md rule 3. - The entire post-handler delivery (unwrap, TTS, final text, attachments, delivery-ledger writes) ran inside `_media_delivery_scope`. Under multiplex that binds the routed home, so delivery obligations landed in the routed profile's state.db while boot-time `_claim_pending_obligations` still reads the launch home. Only the policy reads (`diagnostic_wake_muted`, `warning_text`) bind the routed scope now; delivery stays where main ran it. - The turn-crash notice is rebuilt the same way: scope around the policy read, send outside. - The "delivery failed after multiple attempts" notice is unconditional again: the requested result itself was lost and this line is its only signal, so it is not a diagnostic. Tests that asserted the reverted behaviours are removed; the reviewer-round test file is renamed for what it covers. |
||
|
|
cd3de040ab |
feat(notifications): opt-in suppression of user-channel warning notifications
Squash of the 54 commits on victor-kyriazakos:feat/user-channel-warning-suppression (PR #112302, head f45c640e55) so the contributor's authorship survives a rebase-merge; the commits interleave with a cron delivery-ledger rework that the salvage removes in follow-up commits, so per-commit cherry-picks were not practical. Adds display.suppress_warning_notifications (global + per-platform, default false): one resolver (gateway/warning_notifications.py), BasePlatformAdapter.emit_warning / emit_media_warning / warning_text, a notification_category classification carried through wakes, queues and persistence, and render/present boundaries for CLI/TUI. |
||
|
|
98b4efbf48 |
fix: clarify cards that cannot render are re-asked as plain text, never reported as user inactivity
A native clarify card on a messaging platform (Telegram inline keyboard, Slack blocks) could fail without the user ever seeing a question, and the agent then waited out the full clarify_timeout and reported "[user did not respond within Nm]" (#112684): - the platform rejects the card at once -> the wait aborted with a delivery sentinel but the question was never re-asked; - the send outruns the 15 s acknowledgement window and only then fails (pool/connect timeouts, a stale-thread retry) -> the runner never looked at the send future again and blocked for the whole timeout for a card that never posted; - no status adapter at all -> _ask_clarify_question returned ('', False), so a batch reported timed_out=True with an empty notice. gateway/run_turn_runner_clarify_delivery.py (new topical sibling; the send-disposition helpers move out of the gateway/run.py facade) now retries every definitive card failure once through the adapter's plain-text send_clarify (numbered list + text capture) - except a connector egress DECLINE, where re-sending the text is the exfiltration the guard exists to stop - and watches a possibly-delivered send so a late failure releases the waiter with "[clarify prompt could not be delivered]". The no-surface case reports "[clarify prompt could not be delivered: no chat surface]" (same prefix every consumer already treats as a non-answer). Live probe (real TurnRunner + real clarify_gateway, Telegram-shaped adapter, timeout 30 s): card fails after 16 s -> before 45.2 s and "[user did not respond within 0m]", after 16.7 s and the typed answer to the text prompt; card rejected at once -> before sentinel with no text prompt, after the numbered prompt is sent and "2" resolves to the choice. |
||
|
|
b3aef45987 |
fix(clarify): batch result carries the surface's no-answer notice; trim tests
Follow-up to the #112725 salvage. When a gateway clarify batch stops at an unanswered question, the batch payload only said `timed_out: true` with blank answers — so a card the platform REJECTED ("[clarify prompt could not be delivered]") read exactly like a user who walked away, which is the misreport #112684 describes ("a timeout must not be presented as user inactivity when the prompt was never delivered"). - gateway/run_turn_runner.py::_clarify_batch_sync: the unanswered question's own response text rides along as `notice`. - tools/clarify_tool.py::_run_batch/_batch_result: pass `notice` through into the result JSON beside `timed_out` (only when present); schema description mentions it. CLI/TUI batch callbacks send no notice -> byte-identical output. - tests: fold the single-question re-arm pin into the existing bracket-answer test (<=2 new tests), parametrize the end-to-end test over an undeliverable Telegram-shaped adapter so the delivery sentinel is pinned as `notice`. - docs: tools-reference.md describes `notice`. Part of #112684 |
||
|
|
242ff24ff7 |
fix(gateway): answer a clarify batch on the surface, not by sentinel text
clarify_tool loops a callback that cannot take its `questions=` batch form one
question at a time, and that loop can only tell "no answer arrived" from the
sentinel text the callback returns. The gateway callback was such a surface:
_clarify_send_then_wait returns "[user did not respond within Nm]", and
_clarify_send_disposition returns "[clarify prompt could not be delivered]".
Neither is recognized by the loop, so the sentinel was stored as that
question's answer and the next question was asked anyway.
Effect: a gateway batch — one clarify call with several questions, the shape
the tool schema prefers ("several INDEPENDENT questions belong in ONE call") —
blocked for one full clarify_timeout per question (10m default), never set
timed_out, and handed the agent a user response the user never gave.
This surface already knows whether an answer arrived: _clarify_send_then_wait
returns `answered`. So the batch is answered here instead of inferred from
text: _clarify_callback_sync(..., questions=[...]) asks one card per question,
stops at the first unanswered one (retiring that card), and returns
{"answers": {qid: raw}, "timed_out": bool} — the shape clarify_tool's batch
path reads and the tui_gateway bridge (tui_gateway/agent_callbacks.py) already
implements.
The per-question body is the extracted _ask_clarify_question, so the
single-question path keeps its exact behaviour: same sentinel text, same card
retirement, same immediate typing re-arm, same "" when no status adapter is
wired. Within a batch the stream/typing re-arm waits for the last question —
between two cards it only opens a bubble that the next question's boundary
finalizes immediately.
Measured with the real TurnRunner._clarify_callback_sync against a duck-typed
card adapter (tests/gateway/test_clarify_card_retire_on_timeout.py):
before, 3 questions, no answer:
cards asked ['One?', 'Two?', 'Three?'] (each waits one clarify_timeout)
payload the sentinel text as each question's answer, no timed_out
after:
cards asked ['One?']
payload {"answers": {}, "timed_out": true}
|
||
|
|
95d624e903 | fix(gateway): quiet scheduled heartbeat surfaces | ||
|
|
6a22abe5ba |
fix: retire clarify cards on an explicit no-answer signal, not the '[' prefix
`_clarify_callback_sync` decided "no answer arrived" by testing whether the response text starts with '[' (the shape of the timeout / undeliverable sentinels). A real answer can start with '[' too — a "[A] staging" choice label picked by number, or "[urgent] ..." free text after Other — so the clarify resolved and the agent got the answer, yet the Slack card was rewritten to "This prompt expired" and typing was never re-armed. `_clarify_send_then_wait` now returns `(response, answered)` and the runner branches on that flag only. The Slack click handler popped the retire entry as soon as Other was clicked, but Other is not terminal: the clarify stays pending for typed text, so a later timeout or /new reset found nothing to retire and the card stayed stuck on "Awaiting typed answer". The entry is now popped only on a terminal outcome (a choice click, or Other on an already-dead entry). A typed answer to a native card (numeric pick, or text after Other) never reaches the click handler, so the card kept its buttons forever; the TEXT_RESOLVED intercept now retires it with the answer. Review finding: '[' prefix mistaken for the timeout sentinel; Other click dropped the retire entry; typed answers never rewrote the card. |
||
|
|
71cac9426d |
fix(gateway): retire native clarify cards on timeout, reset and prose cancel
One adapter-facing seam replaces the Slack-only callback: an adapter whose clarify prompt is a persistent card (Slack Block Kit) defines `retire_clarify_card(clarify_id, notice)`, and the gateway calls it from every path that ends a clarify without a button click: - TurnRunner._clarify_callback_sync: when the bounded wait returns a sentinel (timeout, /new or run-end clear_session), schedule the retire with the expired notice on the gateway loop (#110821). - run_inbound TEXT_REJECTED_PROSE: retire with the cancelled notice before the prose is routed as a follow-up (#111019). Lookup is on the adapter class so MagicMock doubles cannot fabricate the method; no platform == SLACK special-case. The Slack map is keyed by clarify_id and popped before the first await, so a late timer cannot touch a newer prompt and the button handler's ts-keyed guard makes a racing click a no-op. Gateway-restart-orphaned cards stay out of scope: nothing is waiting on the new process, and the click path already renders them expired. Tests trimmed to invariants: the runner-level timeout probe (card adapter vs no-card adapter), the inbound prose retire, and one Slack test covering buttons-dropped + late-click-noop. Docs updated for the new in-place edit. |
||
|
|
b065e861b3 | fix(gateway): skip duplicate-send diagnostic for interim-only stream consumers (#105341) | ||
|
|
23036e20a6 |
fix(ux): plain-language, actionable user-facing messages (core)
Squashed integration of the user-facing message audit for this surface set. Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts). |
||
|
|
4748caff76 |
fix(gateway): explicit tool_progress new/all keeps text progress in un-cardable Slack chats
The destination preflight / refusal path suppressed the whole progress lane for a flat DM regardless of mode, so an operator who WROTE `tool_progress: all` got nothing there (before #108668 they got text bubbles via the fallback). Silence is right only for Slack's tier default, where no text lane was asked for; explicit new/all now routes through the editable text fallback instead. Also hoists resolve_tool_progress into the existing display_config import in _run_agent_display_settings. Test proven red on the salvaged head (adapter.sent == [] with `all`). |
||
|
|
e9c037f65a | docs(slack): clarify progress resolution and fallback lifetime | ||
|
|
a4e2a82a6d | fix(gateway): preflight task-card destination before transport fallback | ||
|
|
bc125d59d5 |
fix(gateway): null tool_progress inherits; name the task-card suppression latch
Review findings (Salt, adversarial pass on the two preceding commits): - BLOCKING: a `tool_progress: null` (global, platform, or legacy overrides) counted as an explicit mode because the gate tested key presence, while the display resolver skips None and inherits. Null resolved to Slack's tier default `off` and disabled cards, which is the default-off trap the change exists to avoid. Explicit intent is now a non-None value (or the env bridge). Tests cover null at each level plus null-over-global-all; mutation to key-presence turns the three null cases red. - TASTE: `_TaskCardState.egress_declined` now also latched on unsupported destinations, so the name no longer described the field. Renamed to `publication_suppressed` with both causes documented; readers unchanged. - SHOULD-FIX: slack.md still promised an unconditional text fallback and described the opt-in as independent of tool_progress. Rewritten: cards follow an operator-written off (including /verbose), null inherits, an un-threaded chat with the card lane active shows no tool progress, other native failures keep the editable fallback. |
||
|
|
3412490ad1 |
fix(gateway): no text tool progress when a Slack chat cannot host a task card
In flat Slack DMs (reply_in_thread false) the connector refuses task cards
("slack task_card requires a thread anchor"; native Slack: "No Slack thread
target"). The card lane treated that like a transient native failure and
fell back to an editable text message, so every tool event re-rendered
"Hermes is working / - tool - running" in the DM: text tool progress on a
platform whose default is off, for an operator who never enabled it.
Treat unsupported-destination refusals as terminal for the turn (same
latch as an egress decline) and log at info; transient native failures
keep the text fallback.
|
||
|
|
e5ca5207de | fix(agent): unify replay history canonicalization | ||
|
|
ad305bead5 |
refactor(gateway): send_exec_approval is a base template method; 9 adapters only render buttons
Nine surfaces (feishu, teams, slack, telegram, whatsapp_cloud, qqbot, matrix, discord, relay) each re-derived the approval choice set — [Allow Once]; session + always unless smart-denied; [Deny] — and four of them (discord, slack, teams, whatsapp_cloud) never adopted base._format_exec_approval, so header/reason/smart-deny wording and truncation budgets drifted per adapter. Three separate commits had to touch 5–9 adapters for one semantic fix. BasePlatformAdapter.send_exec_approval now builds an ExecApprovalPrompt (shared text via _format_exec_approval, shared `(label, choice, style)` rows via _exec_approval_actions) and hands it to the `_send_exec_approval_prompt` hook. Each adapter keeps only its widget mapping (~10–20 LOC); platform wording stays via the existing `_EA_*` class attrs, and a new `_exec_approval_cmd_budget` hook lets Slack/Discord budget the command against their hard message caps (3000-char section / 2000-char message) instead of computing it inline. `_EA_REASON_BUDGET` covers Slack's 500 / Discord's 300 reason caps. The runner used to detect button support by `hasattr(type(adapter), "send_exec_approval")`; that is now true for every adapter, so `_renders_exec_approval_buttons` asks `supports_exec_approval_buttons()` (hook overridden?) and keeps the duck-typed check for non-BasePlatformAdapter classes. Visible text changes (button semantics unchanged everywhere): - Discord: the smart-deny line now follows the reason (was inside the header before the fence); the truncation marker is "..." not "\n... [truncated]". - Slack: smart-deny line follows the reason instead of the header. - Teams: unchanged (same 2000-char preview, same smart-deny block). - WhatsApp Cloud: identical text; body still capped at 1024. - QQBot/relay: unchanged. |
||
|
|
366f9446c9 |
fix(agent): pass turn_author only to a run_conversation that accepts it
The gateway turn runner and the quiet one-shot passed `turn_author=` unconditionally. Every test double and wrapper with an older `run_conversation` signature raised TypeError, which CI caught across twenty gateway tests. Both call sites now check the callee with the existing `_accepts_keyword` helper. A human `-Q` turn keeps today's call shape with no author keyword. |
||
|
|
70b1ff6930 |
feat(memory): carry the turn's author into the memory-provider contract
`on_turn_start` documents a per-turn kwargs channel — "kwargs may include: remaining_tokens, model, platform, tool_count" — and `MemoryManager` forwards whatever it receives. Its only caller passed nothing, so a memory provider had no way to learn who wrote the turn it was being told about. Providers that key durable state on identity resolve one identity when the session is created. A shared session does not work that way: threads are shared by default (`thread_sessions_per_user` is False), so alice, bob, and another agent all write turns into a session whose peer is whoever spoke first. The gateway's answer today is the `[name]` prefix it prepends to the message text, which the model reads and a provider cannot. `turn_author` now travels from the gateway through `run_conversation` into `build_turn_context`, which forwards `author_id`, `author_name`, and `author_is_bot` to every provider. It stops there — the trio never reaches the model, and providers that ignore the kwargs are unaffected. The bot flag is sent on every transport, not only shared sessions: a provider deciding whether a turn may write to durable memory needs it in a DM too. `SessionSource.is_bot` is only as good as its producers. `build_source` defaults it to False and 3 of 32 adapter call sites pass it, so most platforms still report every author as human. Populating the rest is follow-up work; nothing here depends on the flag being right yet. |
||
|
|
c50bf92f44 | fix(gateway): flush spoken acknowledgments at tool boundaries | ||
|
|
875ada6811 |
fix(gateway): guard display config reads against present-but-null values
A profile config with a bare `display:` key (present-but-null) made
`user_config.get("display", {})` return None — the {} default only
applies when the key is missing — so the chained
`.get("memory_notifications")` in _wire_turn_agent_callbacks raised
AttributeError on every real gateway turn (Discord / cron). Oneshot
turns bypass this wiring, which masked the crash during smoke tests.
Use the same `or {}` guard the other gateway display readers
(display_config.py, runtime_footer.py) already apply, and fall back to
the documented default "on".
Fixes #105674
|
||
|
|
866332bfb5 |
fix(relay): authorize send_message targets and surface egress declines (P5) (#99220)
* fix(relay): authorize send_message targets and surface egress declines
P5 of the relay egress-authorization workstream. The relay path
authenticated the SENDER but never authorized the DESTINATION, and the
gateway compounded it from both ends.
(a) send_message could silently name an arbitrary relay target. Its
`target` parameter is free-form ('platform:chat_id'), so a model could
name ANY chat id and the gateway would emit an outbound frame for it.
gateway/relay/egress.py adds an attestation floor: a relay-routed
destination must have a provenance this gateway can show -- the
operator's home channel, the channel directory, or its own gateway
session origins. Anything else is refused HERE, with a visible tool
error naming the target, before a frame is written. Non-relay platforms
and platforms served by a live native adapter in this process are
untouched (same precedence resolve_delivery_transport applies).
(b) Connector declines were swallowed into apparent successes. The
connector's egress floor answers an unauthorized destination with a
DEFINITE failure whose text is deliberately uniform (F-005). Several
relay lanes degrade a *transport drop* by design and were degrading an
*authorization refusal* the same way:
- _send_media returned None, sending the caller into
BasePlatformAdapter's text fallback -- a DIFFERENT op re-addressed at
the very chat the connector had just refused.
- _send_prompt returned None, so exec-approval / slash-confirm /
clarify reported "relay prompt op unavailable" (a wrong reason) and
ran their numbered-text fallbacks into the refused chat.
- task_card_stop discarded the error entirely.
- typing / delete / react / thread ops degraded silently at debug.
is_egress_decline() classifies THAT a decline happened (never why --
the uniform text is not parsed for reasons) and requires a definite,
non-ambiguous failure, so a lost-ack retry is still a transport
outcome. Lanes with an error-carrying contract now report the decline
verbatim; cosmetic bool/None lanes still degrade but log it at WARNING.
Advisory progress drops that legitimately degrade are unchanged: the
task_card send lane, the draft ambiguous/except branches, and every
transport-exception path keep their existing fail-open behaviour.
Tests: 21 mutations of the production source, all KILLED.
* fix(relay): authorize the RESOLVED target; declines must not fall back
Review round 1 (independently confirmed by a second reviewer) found three
blockers. Two are fixed here; the third (B-2, Telegram @username) is a policy
decision left open deliberately.
B-1 — THE FIX CAUSED THE OUTAGE IT PREVENTED (tools/send_message_tool.py)
The P5(a) guard ran ABOVE Slack user->DM resolution, so it authorized the
internal pseudo-id `_parse_target_ref` emits (`user_name:ben`, `user:U...`).
Provenances only ever hold RESOLVED conversation ids, so a fully attested DM
was compared as a handle against a set of `D...` ids and refused:
base slack:@ben SENT head(before) slack:@ben REFUSED
Every Slack DM by handle was broken. Moved the guard below resolution; it now
authorizes the destination that is actually sent to, and the refusal names the
resolved id. Position is load-bearing, so it is commented as such and pinned:
reverting the move turns exactly the four new cases red.
B-3 — A DECLINE IS NOT A LANE FAILURE (gateway/run.py)
`_approval_send_outcome` had only sent/failed/ambiguous, so a connector
decline collapsed into `failed` — which is the cue to run the plain-text
fallback into the chat the connector had just refused. The adapter fix in the
previous commit improved the error STRING while user-visible behaviour stayed
identical to base; the commit message overstated it. Fixed properly:
- new `declined` verdict, recognised via the shared `is_egress_decline`
contract (not string sniffing at the call site)
- exec-approval returns without the text fallback
- slash-confirm suppresses the text reply AND clears the registration, so a
card that never rendered cannot capture the user's next message
`send_clarify` was already correct (returns early inside the adapter).
MUTATIONS (production source; both directions)
classifier never returns 'declined' -> KILLED (4 cases)
ALL failures classified as 'declined' -> KILLED (2 cases)
guard moved back above Slack resolution -> KILLED (4 cases)
decline CODE changed (review M05) -> KILLED
marker match made case-sensitive (M10) -> KILLED
M05 was a tautology: the test asserted the imported constant against itself,
so changing the constant could not fail it. The wire contract is now pinned as
a literal, because the connector stamps that exact string and a one-sided
change is a silent cross-repo break.
REGRESSION CHECK: the 12 failures + 1 collection error in this test selection
are PRE-EXISTING cross-test contamination — the identical set fails at
|
||
|
|
3114916ee4 |
fix(gateway): carry accepted-input ownership through persistence
Namespace delivery markers and assign fresh keyless turn identities instead of inferring ownership from IDs or process-local row baselines. Query only marker existence on the canonical live compression continuation and ancestors. Preserve raw reply IDs and exclude metadata from provider wire messages. Expand the two existing invariants with resumed cross-chat ID collisions, a real independent SQLite writer, reaped siblings, and archived-history allocation controls. All 20 full-handler checkpoints and 63 targeted tests pass. |
||
|
|
14791b4d4e |
simplify(compat): approval — drop 43 facade re-exports + _command_detection_variants late-bind seam, repoint 30 callers + 46 test files
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/ human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined names directly (module-attribute reads on tools.approval_context so patching the defining module still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied, _denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval. approval_detection calls its own _command_detection_variants instead of late-binding through the facade. |
||
|
|
2031c819fe |
simplify(compat): gateway — re-land 92d0bd0d73 (reverted by stale-index commit b818085298)
Re-applies the gateway compat removal byte-for-byte; see
|
||
|
|
b818085298 | simplify(compat): doctor/status — drop 13 re-exports + the doctor_* globals() facade (97 names), repoint 6 callers / 13 tests | ||
|
|
92d0bd0d73 |
simplify(compat): gateway — drop 30 re-exports/aliases + 2 shim modules, re-remove 3 shim-only names, repoint 24 callers + 34 test files
Per COMPAT_REMOVAL.md (internal import paths are not a stable API): Re-exports removed - gateway/run.py: atomic_json_write, load_dotenv, resolve_delivery_transport, TurnRunner, merge_pending_message_event, _arm_loop_floor_timer, start_loop_liveness_watchdog, DEFAULT_GATEWAY_POST_INTERRUPT_GRACE_TIMEOUT, _UNSET (9) — run_* mixins and tests now import from the defining module (gateway.delivery / gateway.run_turn_runner / gateway.platforms.base / gateway.shutdown_watchdog / gateway.restart / utils). - gateway/session.py: SessionResetPolicy, normalize_whatsapp_identifier, TranscriptReadError, auto_continue_freshness_window (+ "_now & co." noqa facade) — gateway/__init__ takes SessionResetPolicy from .config; callers take TranscriptReadError from gateway.session_transcript. - gateway/kanban_watchers.py: _wake_scope_id + "tests import via origin" noqa facade; tests import from kanban_watchers_common / _notifier. - gateway/slash_commands.py: _model_switch_skew_guard, HISTORY_UNREADABLE. - gateway/stream_consumer.py: escape_code_fences_for_display. - gateway/platforms/api_server.py: "re-exported" RunIdempotencyStore comment; tui_gateway + tests import gateway.platforms.api_server_run_idempotency. - gateway/platforms/__init__.py: PEP 562 __getattr__/__dir__ lazy QQAdapter / YuanbaoAdapter facade (no in-tree importer). - gateway/startup_watchdog.py: whole re-export shim module deleted; the three in-tree callers import hermes_startup_watchdog directly. Aliases removed - gateway/platforms/signal.py: SignalAdapter._markdown_to_signal. - gateway/shutdown_forensics.py: _parse_systemd_duration_to_us. - gateway/platforms/yuanbao.py: OutboundManager.start_slow_notifier / cancel_slow_notifier / get_chat_lock / _chat_locks / CHAT_DICT_MAX_SIZE delegates; module-level get_active_adapter / send_yuanbao_direct; MarkdownProcessor has_unclosed_fence / ends_with_table_row / split_at_paragraph_boundary static pass-throughs (chunk_markdown_text stays — it carries yuanbao's chunking policy). tools/send_message_senders + tools/yuanbao_tools call YuanbaoAdapter.get_active() / sender.send_direct(). Shim-only names re-removed (earlier review-fix round |
||
|
|
4749300508 | review-fix(suppress-audit): run_turn_runner/browser_control_artifacts — restore BASE exception semantics | ||
|
|
81984a7ebd | review-fix(suppress-audit): bedrock_adapter/run_turn_runner/self_repo_guard — restore BASE exception semantics | ||
|
|
e83816a4d1 |
review-fix(comments): restore lost #NNNN rationale comments across non-test source (mechanical sweep, condensed, code unchanged)
For each issue anchor present in BASE
|
||
|
|
eb87cf9a9f | refactor(gateway): TurnRunner reuses runner._spawn_release_thread, folds edit-throttle branch and status/tuple assigns | ||
|
|
70d9addef0 | refactor(gateway): TurnRunner — shared delta-sink tee, single eviction pop, folded result dicts and guard ladders | ||
|
|
7870e3c917 | refactor(gateway): TurnRunner progress rail — unified verbose/short block routing, task-card upsert, suppress-drains, _accepts_keyword | ||
|
|
13949a95e4 | refactor(gateway): keep _final_for_stream anchor pinned by test_stream_final_adoption_gate | ||
|
|
30731e2285 | refactor(gateway): run_turn_runner/run_topics — AST-neutral bracket packing | ||
|
|
8206e42497 | refactor(gateway): TurnRunner — unify thread→loop scheduling, split progress/agent-resolution god methods, dedupe result dicts (2402→1909 LOC) | ||
|
|
66366d3dab |
refactor(gateway): split GatewayRunner into 13 run_* mixins + TurnRunner module (run.py 31332 -> 6957)
AST-driven, body-identical move of 359 GatewayRunner methods into cohesive
mixin modules (gateway/run_{voice,adapters,topics,turn,shutdown,busy,
config_loaders,startup,watchers,notifications,inbound,goals,agent_cache}.py)
plus TurnRunner -> gateway/run_turn_runner.py. run.py-internal symbols are
imported lazily inside method bodies so patch('gateway.run.X') keeps
intercepting; neutral deps are top-level; logger name stays 'gateway.run'.
_UNSET moved to leaf gateway/run_common.py (def-time default-arg sentinel).
Whole-module inspect.getsource(gateway_run) AST-walker tests repointed to
the module that now holds the walked code.
|