fix(channels): gate /compact on backend capability (#8156) - #8167
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Shared decision helper mirroring the proven dashboard/auto-compact gates, with per-surface voice kept at the call site — right split, real fix, scoped residue disclosed. Suggestions
[DESIGN-REVIEWED] f205ad8 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of I have everything I need. The fix itself is derived from a reported defect (#8156) and mirrors the dashboard's existing gate; the notable issues are a third spelling of the same capability predicate and two ungated First-Principles-Verdict: CONCERNS The gate is real and derived, but it ships a third spelling of an existing predicate, and the author's own grep pattern leaves two ungated What this change shipsIntent: stop
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] f205ad8 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
A backend outside ACP_BACKENDS_COMPACT (KAS) treats the /compact prompt as ordinary text and never answers, so the messaging surfaces' ungated provider.compact() dispatches stranded the status wait for the call-site bound (120s on Slack/Telegram/Discord, _COMPACT_TIMEOUT_S on Webex) or indefinitely (Teams, Feishu, iMessage, WeCom, Weixin, WhatsApp) while holding the turn semaphore, then reported an error for a backend that was never going to answer. Mirror the dashboard's manual-/compact capability gate (#7800) at every transport call site through one shared decision helper, messaging.commands.compact_unsupported_backend (acts only on a non-empty str, the ABC's stated contract, so mocked providers never read as a refusal), plus compact_unsupported_reply for the informational wording. Ten surfaces are gated: Slack, Telegram, Discord, Webex, Teams, Feishu, iMessage, WeCom, Weixin and WhatsApp — the issue's list of eight missed Feishu and iMessage, which carry the same shape with an unbounded wait (caught in pre-push review). Manual /compact commands answer immediately and informationally, with the declined backend id logged; the context-threshold notices decline silently — no forced hard-threshold compaction to run, and no soft nudge whose /compact advice the backend would refuse. The Chinese-language surfaces translate the reply; iMessage and WhatsApp keep their plain-text voice. Supported backends keep the exact current behavior. Red-before-green tests per transport pin that an unsupported backend gets the informational reply with compact() never dispatched (semaphore still released), that a None/absent capability preserves the existing dispatch, and that both threshold notices stay silent; providers.md now records the messaging surfaces as gated. Closes #8156 Co-authored-by: Kiro Crew <noreply@kirocrew.dev>
e07373d to
f205ad8
Compare
|
Disposition: accepted-and-deferred — First Principles concern: third spelling of the compact-capability predicate ("Watch" item 1 + "Subtractions").
The concern is legitimate: this PR does add a third read site for the same Deferred to #8203 ( |
|
Disposition: accepted-and-deferred (with severity rebuttal) — First Principles concern: two ungated
Neither site reproduces the #8156 defect (an unbounded wedge), so neither is a completeness gap in this fix:
Both are also outside the surfaces #8156 and this |
bolichen97
left a comment
There was a problem hiding this comment.
Approving on the strength of a full readiness audit of every open PR against main, not a
line-by-line reading of this diff — recording that plainly so the next reader knows what this
stamp does and does not cover.
Verified against this exact head SHA:
readiness: passedpresent, andPR Readiness— the one required status context onmain
(rulesetprotected-branches) — issuccesson this head.- No check run on this head is
failure,cancelled,timed_outor still in flight. Skipped
jobs are path-filtered conditionals, none of them required. mergeable: true, and the head is not far enough behindmainfor its green CI to describe a
base that no longer exists.- No surviving reviewer
CHANGES_REQUESTED: any such review is on an older commit and therefore
already dismissed bydismiss_stale_reviews_on_push. - Every issue comment, inline review comment and review thread was read and classified. Nothing
left is an unresolved human change request — the remainder is bot review-lane output, resolved
or outdated threads, explicitly non-blocking suggestions, and author status notes.
Auto-merge (squash) is armed, so this lands once every other ruleset requirement is met.
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Problem / Motivation
Typing
/compact(or!compact) in a messaging channel on a KAS-backed session hangs and then errors. A backend outsideACP_BACKENDS_COMPACTtreats the/compactprompt as ordinary text and never emits a compaction status, so the transport'sprovider.compact()+wait_for_compaction()strands: for the full 120 s call-site bound on Slack/Telegram/Discord and_COMPACT_TIMEOUT_Son Webex, and indefinitely on Teams, Feishu, iMessage, WeCom, Weixin and WhatsApp, which have no call-site bound at all — while holding the session's turn semaphore, so every message in that conversation is answered "still working" until the deadline expires. The dashboard already gates this (#7800, PR #7816); the messaging surfaces did not.Why it matters
Any user driving Kiro Crew from a chat channel on a KAS backend gets a multi-minute wedge plus a misleading error for a command the backend was never going to answer — and on the six unbounded surfaces the conversation is blocked for the whole
COMPACT_WAIT_TIMEOUT_SECS. The hard-threshold auto-compaction paths in the channel dispatchers had the same shape, so a long conversation could wedge itself with no user action at all.What changed (motivation → approach → change)
Symptom: stranded compaction waits on chat surfaces. Root cause: the transports dispatch
provider.compact()with no capability check, unlike the dashboard's pre-acquisition gate. Change: mirror that gate at every transport call site through one shared decision helper rather than ten copies.messaging.commands.compact_unsupported_backend(provider)— readsmanual_compact_unsupported_backendoff the live provider (declared on theLLMProviderABC with aNonedefault, harness-parity H14) and acts only on a non-emptystr(the ABC's stated contract), so a mocked or duck-typed provider never reads as a refusal.compact_unsupported_reply(backend)owns the informational wording, matching the dashboard's message./compacthandlers on all ten surfaces (Slack, Telegram, Discord, Webex, Teams, Feishu, iMessage, WeCom, Weixin, WhatsApp) answer immediately and informationally — the backend manages compaction automatically, thecc_managedrelationship, not an error — with the declined backend id logged. The Chinese-language surfaces (Feishu, WeCom, Weixin) translate the reply; iMessage and WhatsApp keep their plain-text no-markdown voice./compactto compress" advice would loop the user into a command that is refused. The gate sits aftercheck_context_usage(its reading arms the backend autocompactor and must keep running) and fires only when a threshold is actually crossed.docs/system-specs/modules/providers.md: the sentence parking the messaging-surface residue on KAS backend: auto-compact dispatches /compact, strands 300s holding the session semaphore, then recycles the session #7812 now records the ten surfaces as gated; KAS backend: auto-compact dispatches /compact, strands 300s holding the session semaphore, then recycles the session #7812 keeps only the session-level auto-compact path.Tests
Red-before-green (all failed on unmodified production code, pass now):
test_messaging_commands.py— the helper's contract: named backend returned; absent/None/empty-string read as supported; a truthyMagicMockattribute never reads as a refusal; the reply names the backend and carries no error markers.compact()is never dispatched, with the acquired semaphore still released; an explicitNonecapability preserves the existing dispatch byte-for-byte./compactnudge. iMessage's reply is additionally pinned markdown-free per that surface's convention.Full runs of the eleven touched test modules pass (1208 tests); repo-wide pytest matches pristine main exactly (the 99 pre-existing failures on this host reproduce identically on an unmodified main worktree — host-environmental, none in messaging).
Manual verification
N/A — unit coverage sufficient: the gate is a pure pre-dispatch decision on the provider capability property, and the per-transport fakes exercise the exact reply paths.
Related Issues
Closes #8156
Pattern harvest
Rule candidate: review-prompt
Pattern: "a capability gate added to N named call sites — grep for the ungated call shape (
provider.compact()here) across the whole tree before trusting the issue's list of surfaces; two of ten were missing from the report"Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)