_has_active_session_for_thread() hardcoded chat_type='group', causing
session key mismatch for DM and MPIM threads. DM sessions key as
agent:main:slack:dm:{chat_id}:{thread_ts} but the lookup built
agent:main:slack:group:{chat_id}:{user_id}:{thread_ts}.
Impact: _has_active_session_for_thread always returned False for DM
threads, causing thread context to be prepended on every message. The
prepended context broke slash command detection (get_command() checks
text.startswith('/')), so /cmd and !cmd never worked in DM threads.
Fix: accept event-derived chat_type parameter instead of hardcoding
'group'. Both call sites pass chat_type='dm' if is_dm else 'group',
where is_dm is already computed from channel_type in {'im', 'mpim'}.
This correctly handles:
- IM channels (D-prefix): chat_type='dm'
- MPIM channels (G-prefix): chat_type='dm' (was missed by D-prefix heuristic)
- Channel messages (C-prefix): chat_type='group' (unchanged)
Added regression tests covering DM thread lookup, MPIM thread lookup,
and negative cases verifying the old hardcoded 'group' behavior fails.
Slack rich_text blocks mirror the original message text. When bang
commands are rewritten from !model to /model, appending block text makes
the command arguments include a duplicate payload, so the model switcher
sees spaces in the model name and rejects valid commands like:
!model qwen3.7-plus --provider opencode-go
Skip block extraction for command messages while preserving it for
normal messages. Also preserve Slack thread_ts (top-level or nested in
message/container payload shapes) on native slash-command payloads so
session-scoped commands like /model apply to the intended thread instead
of a channel+user key the next threaded message never matches.
Surgical reapply of PR #43533 (originally against gateway/platforms/slack.py,
now plugins/platforms/slack/adapter.py). Thread-shape widening credit also
to #66310.
Follow-up to the #43755 salvage:
- Update the strict _Pool doubles in tests/run_agent/test_run_agent.py to
accept api_key_hint and assert it carries the agent's failed key.
- Add a real-CredentialPool regression (no mocks) proving the hint routes
exhaustion to the entry whose key actually failed, not pool.current(),
plus the no-hint baseline (#43747 wrong-entry marking).
recover_with_credential_pool() called mark_exhausted_and_rotate() without
api_key_hint, causing it to fall back to current() or _select_unlocked().
When a prior rotation left current() as None, _select_unlocked() returned
the NEXT (healthy) entry instead of the one that actually failed — marking
the wrong credential as exhausted (#43747).
Extract the current API key from agent.api_key (or pool.current().runtime_api_key
as fallback) and pass it as api_key_hint to all 4 call sites.
Clear the coding rail while a session's workspace changes and discard late
Git status results for the previous cwd. This prevents Ctrl+Shift+B from
opening a worktree dialog against an old branch.
Use the trusted publisher's scoped GitHub token when resolving the pinned
gh-image release. This avoids GitHub-hosted runners exhausting their shared
anonymous API rate limit before the evidence publisher can run.
- Extract resolveSessionColor(): the sessionColorFor fallback no longer
re-implements the override -> project-color precedence that the
$sessionColorById computed already owns.
- tileStoredRow: collapse the nested project-tree walk into a flatMap +
find, matching the local style.
The no-native-title guard (added on main after this PR's first CI run)
bans native title= on <button>. Wrap the shared ChoiceButton in the
themed <Tip>, which renders the child untouched when the label is falsy
so the live card is unaffected.
A tab group rendered only the active pane's content, so every tab switch
unmounted and remounted the whole surface — revisiting a session tab
re-measured and re-scrolled the thread from scratch, visibly shifting
layout each time.
Panes that have been active in a zone now stay mounted in absolutely
positioned layers; the inactive ones hide via visibility (keeping their
layout box, so scroll positions and measurements survive) with
pointer-events disabled and aria-hidden. Mounting stays lazy — a pane
first mounts when first activated — so a boot-restored tab stack still
doesn't resume every session up front, and panes that leave the zone
(closed / moved) unmount as before.
Opening a session in a new tab left the tab titled 'Session' with no
project-color dot until new activity landed the row in the paginated
recents list. Two gaps:
1. tileTitle/tileAccent/tileDragPayload only looked the stored row up in
$sessions (the recents page). Sessions opened from a project group are
often older than that page, so the lookup missed entirely. Resolve
through the project tree as a fallback (tileStoredRow) and re-sync pane
titles/accents when $projectTree loads.
2. Even with the row present, liveSessionProjectId returned null for a
session whose cwd sits outside its recorded git_repo_root (mid-session
relocation / sibling worktree), because the cwd-under-root guard ran
before the explicit-project folder match. The backend tree groups such
rows under the project; the client now agrees — an explicit folder
match is authoritative, only the auto-project (repo root) fallback
still needs cwd-under-root confidence.
sessionColorFor also computes directly (overrides -> project color) when
the row isn't in the $sessionColorById map, which is keyed over $sessions
only.
Keep the live PR review comment polling for a short grace period after
visible jobs complete. Preserve composite-action jobs in timing and live
status collection, and split the timing HTML from its linked review-status
artifact.
Builds on the skipped-clarify card so it holds up beyond the timeout case:
- Extract a shared `ChoiceButton` (letter badge + label + row chrome) used
by both the live pending card and the settled skip card. The two blocks
had drifted into duplicated markup; now they can't diverge.
- Fix the hint copy. An empty `user_response` is emitted for BOTH a
server-side timeout AND a manual Skip (tools/clarify_tool.py) — there is
no field on the result that tells them apart — so asserting "This question
timed out" was wrong half the time. Neutral wording ("This prompt is no
longer waiting…") is correct for either, and the recover-your-answer path
now also helps someone who mis-clicked Skip. Updated en/ja/zh/zh-hant.
No behavior change to the live prompt or the follow-up-message flow.
Co-authored-by: SHL0MS <SHL0MS@users.noreply.github.com>
When a clarify prompt times out, the settled card collapsed to just
'Skipped' — the options were unrecoverable (the args carry them, the
renderer dropped them) and there was no way to answer late.
The skipped card now:
- renders the original choices, letter-badged like the live card
- clicking one drafts a quoted follow-up ('Re: "<question>" — my
answer: <choice>') into the composer via the insert bus. Enter sends
it; if the agent is mid-turn it queues like any other prompt.
- a hint line explains the question timed out and what picking does
No retroactive resolution of the expired request: the tool already
returned empty and the turn moved on — injecting into past context
would break prompt-cache and role-alternation invariants, and
clarify.respond on an expired id hard-errors (#56558). The follow-up
message path needs no backend change and works against old backends.
Answered clarifies and free-text (no-choice) skips are unchanged.
Interim UX for #44845 (durable ID-addressable clarify decisions).
The bundled @vscode/codicons font has no `git-fork` glyph (only
`git-fork-private`), so the Branch menu item rendered blank. Use
`repo-forked`, the actual fork icon, to match the inline GitFork action.
The row/tab context-menu 'Branch from here' item used the git-branch
codicon while the inline message action uses a GitFork icon. Switch the
menu item to the git-fork codicon so branching looks consistent across
surfaces.
* fix(ci): publish inline E2E evidence
Upload bounded screenshot evidence from E2E, then publish validated images
from a trusted workflow_run job to commit-pinned branches in the evidence repo.
Wait briefly for the live CI review comment marker before publishing, so
GitHub's read-after-write delay cannot leave an orphaned evidence branch.
* fix(ci): isolate privileged credentials from PR jobs
Keep App private keys and Docker Hub credentials out of PR-controlled
workflows. Use protected environments for trusted publishing and a public
repository variable for the App client ID.
* fix(ci): attach E2E evidence with restricted bot session
Replace the App-backed evidence repository publisher with gh-image uploads
from a dedicated bot session in the gh-image environment.
* fix(ci): publish validated E2E evidence from forks
Let the trusted default-branch publisher handle bounded, validated evidence
artifacts from fork PR CI without checking out or executing fork code.
Branching/forking a chat replaced the primary chat (setActiveSessionId +
setSelectedStoredSessionId + navigate). Instead, open the branch as its own
session tile tab in the center zone and reveal it, leaving the parent chat
exactly where it is — mirroring openNewSessionTile. All branch entry points
(message GitFork button, /branch and /fork, sidebar Branch, tile Branch) go
through forkBranch, so this covers every surface.
Update the gateway's live turn projection after an accepted correction so a warm session resume does not add the stale original prompt beside the persisted correction.
Cover the inference-time correction path end to end and assert both redirect entry points refresh the live user text.
The base class spawns _keep_typing for every adapter — a 2s refresh loop
that runs for the whole turn, including while the stream consumer sends
the response — but RelayAdapter inherited BasePlatformAdapter's no-op
send_typing. The loop ran every turn and emitted nothing, so relay-fronted
chats (hosted Discord/Telegram/Signal/Slack) never showed 'is typing…'.
The rest of the pipe already existed on both sides: OutboundOp "typing"
is in the wire contract (contract_version 1), and every connector-side
sender implements it (Discord POST /channels/{id}/typing, Telegram
sendChatAction, Signal sendTyping, Slack assistant status).
This bridges the tick onto the existing outbound frame, mirroring send():
- _with_scope re-attaches the tenant discriminator (metadata.scope_id, or
user_id for DMs) — the connector's routedEgressGuard wraps ALL ops, so
an undiscriminated typing frame is declined like a bare send would be.
- the Phase 1.5 per-frame platform tag routes typing through the platform
the chat lives on for multi-platform gateways.
Best-effort by design: transport errors are swallowed (typing is cosmetic;
the next tick retries), no transport is a silent no-op, and stop_typing
stays the base no-op (platform indicators self-expire). Additive within
contract_version 1 — an older connector returns an unsupported-op result
we ignore. Contract doc §4 updated (typing now carries metadata?).
* test(desktop): cover correction resume without duplicate prompts
Exercise a live composer correction, switch away and back before the
response settles, and assert both user turns retain their order and occur
exactly once.
* test(desktop): cover correction warm resume during tool run
Exercise a correction accepted at a foreground-tool boundary, a switch through a persisted session, and the warm resume back. Assert the original prompt and correction remain singular and ordered.
Model settings was the only page that kept its shape while loading; the rest
flashed a centered spinner (LoadingState) or empty placeholders. Standardize on
skeletons that mirror the settings rhythm.
- Add shared SectionHeadingSkeleton / ListRowSkeleton / SettingsSkeleton to
settings/primitives.tsx (mirror SectionHeading + ListRow).
- Convert keys, providers, sessions, gateway, custom-endpoints, and config
(non-model) from LoadingState to SettingsSkeleton; remove now-dead LoadingState.
- billing: BillingSkeleton (summary cards + sections) on first load instead of
"—" placeholder cards.
- pet: skeleton grid on first load instead of a premature "unreachable" message.
- Iterate ROUTINE_COMPRESSION_STATUS_SAMPLES (formatted from the source
constants the emit sites use) through _prepare_gateway_status_message on
every chat platform — emission-wording drift now fails the suite without
re-copying literals.
- Extend the pinned NOISY_STATUS_MESSAGES with the buffered retry chatter
and post-#69332 wordings.
- New VISIBLE_COMPRESSION_MESSAGES negative suite: manual /compress
headlines and abort/failure notices must never be swallowed by the
widened regex.
Post-sweep audit of every compression status emission (conversation_loop,
turn_context, conversation_compression): the buffered overflow/attempt-cap
retry chatter (🗜️ 'Context too large…', 'Compressed X → Y, retrying…',
'Context reduced to…'), the #69332-reworded auto-lower notice
("Auto-lowered this session's threshold…"), the aux-provider-unavailable
notice, and the concurrent-compression skip all leaked past
_TELEGRAM_NOISY_STATUS_RE on chat platforms. Add anchored alternatives for
each; the ', retrying'/'— compressing' anchors keep manual /compress
feedback ('Compressed: 30 → 12 messages') and failure/abort notices
visible per the deliberate carve-outs.
Also extract every routine compression status string into importable
template constants in agent/conversation_compression.py (single source of
truth shared by all emission sites), so tests can iterate the actual
emitted wording instead of hand-copied literals.
Treat HERMES_DESKTOP_HERMES as an authoritative deployment override.
This keeps the Nix desktop package on its matching immutable Hermes CLI
instead of falling through to install.sh when a best-effort version probe
fails or times out.
Surface audit for #64438 consistency: cli.py and acp_adapter had their
gates removed by the salvaged #63630 commit (each with a behavioral
test). The remaining manual-compress surfaces never gated on
compression.enabled — pin that contract so a gate can't regress in:
- gateway/slash_commands.py _handle_compress_command (new behavioral
test: compression_enabled=False still compresses, force=True).
- tui_gateway/server.py — all three manual routes (session.compress
RPC, command.dispatch compress branch, slash.exec mirror) plus the
compute-host slash.compress/session.compress controls converge on
_compress_session_history; new test pins the helper ignores
agent.compression_enabled and passes force=True.
Main renamed the ACP slash command from /compact to /compress after #63630
was written; update the salvaged status line, comment, and behavioral test
to dispatch the command that actually exists.
compression.enabled: false is documented (agent/conversation_loop.py
overflow path) as disabling *automatic* compaction only — the terminal
context-overflow error explicitly tells users to run /compress manually,
and the gateway handler has never gated on the flag. But the classic CLI
(_manual_compress) and the ACP adapter (/compact) refused with
'Compression is disabled', leaving users at a full context with no
manual escape hatch on those surfaces.
Remove the stale gates (they predate the overflow-path design; the CLI
gate came from the original /compress commit's boilerplate) and unify
force=True across all manual-compaction call sites: ACP /compact and the
TUI's _compress_session_history (manual-only helper) now bypass the
summary-failure cooldown exactly like the CLI and gateway already did.
Also reword the ACP /context status line so a disabled-compression agent
no longer implies /compact is unavailable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integration test for the two compaction layers landing this sweep:
#63144's discovery-scope fix (archived rows surface from the current
session) and #69334's bookend bounding (summary exclusion + content
caps). A single compacted session exercises both: the archived FTS hit
must surface while the compaction handoff at the session tail stays out
of bookend_end and long messages stay capped.
Addresses @teknium1's second review round on #63144:
1. _is_compacted_message now checks both active=0 AND compacted=1.
Previously checked active=0 alone, which also matched rewind/undo rows
(active=0, compacted=0) that must stay hidden.
2. Added _is_compression_ended() — checks only the session's own
end_reason, not the lineage-wide has_compression_hop flag. This prevents
delegation children living under a compression continuation from leaking
through the lineage filter.
3. _discover lineage skip now uses is_ended_session (session-level check)
instead of has_compression (lineage-level flag).
New tests:
- TestRewindExclusion: rewind rows stay hidden alongside compacted rows
- TestCompressionEndedHelper: session-level end_reason checks
- TestLegacyContinuationPlusDelegation: delegate child excluded while
compression ancestors surface
78 passed (71 + 7 new).
After context compaction, pre-compaction content was invisible to
session_search — a memory black hole. The _discover() skip logic
filtered both same-session and same-lineage hits unconditionally,
without distinguishing compression-summarised content (gone from live
context) from delegation children (still visible to the parent agent).
Reworked _resolve_to_parent to return (root_id, has_compression_hop),
checking end_reason='compression' on every hop during the same
db.get_session() traversal — zero extra queries.
_discover() now has three compression-aware paths:
- In-place compaction: FTS hits on active=0 (compacted=1) rows pass
through even when raw_sid == current_session_id
- Legacy rotation: lineage hits pass through when has_compression_hop
is true on either side of the chain
- Delegation children: still excluded (no compression edge)
18 new tests covering all three scenarios + unit tests for the helpers.
Addresses Teknium's review feedback on #6256.
Closes#13840, #13841.
With compression.in_place defaulting True the cron session id never
rotates; get_compression_tip returns the input id. Pin that the
salvaged #67188 fix titles/ends the ORIGINAL cron session in that path
(and for falsy tip returns), i.e. zero behavior change when no
compression rotation happened. Also maps colingreig's contributor
email for attribution CI.
Hardening for the #59114 salvage: generate real standalone and merged
handoffs with the current compressor and assert classify_summary_content
agrees with _is_context_summary_content and is_compaction_summary_message
on every emitted shape (behavior contract, not a format snapshot), incl.
the flag-stripped DB-reload copy. Also adds the contributor email mapping
for @israellot.