Skip to content

fix(channels): gate /compact on backend capability (#8156) - #8167

Merged
bolichen97 merged 1 commit into
mainfrom
fix/messaging-compact-capability-gate-8156
Sep 3, 2026
Merged

fix(channels): gate /compact on backend capability (#8156)#8167
bolichen97 merged 1 commit into
mainfrom
fix/messaging-compact-capability-gate-8156

Conversation

@NicholasRBowers

Copy link
Copy Markdown
Contributor

Problem / Motivation

Typing /compact (or !compact) in a messaging channel on a KAS-backed session hangs and then errors. A backend outside ACP_BACKENDS_COMPACT treats the /compact prompt as ordinary text and never emits a compaction status, so the transport's provider.compact() + wait_for_compaction() strands: for the full 120 s call-site bound on Slack/Telegram/Discord and _COMPACT_TIMEOUT_S on 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) — reads manual_compact_unsupported_backend off the live provider (declared on the LLMProvider ABC with a None default, harness-parity H14) and acts only on a non-empty str (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.
  • Manual /compact handlers on all ten surfaces (Slack, Telegram, Discord, Webex, Teams, Feishu, iMessage, WeCom, Weixin, WhatsApp) answer immediately and informationally — the backend manages compaction automatically, the cc_managed relationship, 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.
  • Context-threshold notices decline silently on such a backend: there is no forced hard-threshold compaction to run, and the soft nudge's "reply /compact to compress" advice would loop the user into a command that is refused. The gate sits after check_context_usage (its reading arms the backend autocompactor and must keep running) and fires only when a threshold is actually crossed.
  • The issue named eight surfaces; pre-push review found Feishu and iMessage carry the identical ungated shape with unbounded waits, so they are gated too (the two reported call sites each were re-verified against main first).
  • 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.
  • Supported backends keep the exact current behavior; KAS auto-summarization frame mapping and existing timeout plumbing are untouched.

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 truthy MagicMock attribute never reads as a refusal; the reply names the backend and carries no error markers.
  • Per transport (all ten): an unsupported backend gets the informational reply and compact() is never dispatched, with the acquired semaphore still released; an explicit None capability preserves the existing dispatch byte-for-byte.
  • Per threshold surface: hard-threshold crossing runs no forced compaction and announces nothing; soft-threshold crossing sends no /compact nudge. 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

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of f205ad8d90cc7236f6fe2d5d493882762018131d — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

  • The PR's own harvest shows the weakness of N-copy gating (the issue missed 2 of 10 sites): add a typed fail-fast refusal inside the provider's compact()/wait_for_compaction() as a backstop, so a future ungated call site errors immediately instead of wedging for COMPACT_WAIT_TIMEOUT_SECS — the call-site gates stay for wording and pre-semaphore placement.

[DESIGN-REVIEWED] f205ad8

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed f205ad8d90cc7236f6fe2d5d493882762018131d — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] f205ad8

Verdict parsed from the review's SHA-scoped output markers for commit f205ad8d90cc7236f6fe2d5d493882762018131d.

False positive or not applicable? A repository writer can comment:
/ai-review override fable f205ad8d90cc7236f6fe2d5d493882762018131d: <one-sentence reason>

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of f205ad8d90cc7236f6fe2d5d493882762018131d — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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 compact() dispatch sites the author's own harvest-pattern grep would have surfaced.

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 compact() siblings.

What this change ships

Intent: stop /compact (and auto-compaction) from wedging chat conversations for minutes on a backend that will never answer it — a FIX.

  1. /compact on ten chat surfaces answers instantly with an informational note instead of wedging — justified
  2. Feishu and iMessage gated beyond the issue's eight — same defect, declared
  3. Soft-threshold "reply /compact" nudge suppressed on such backends — justified (advice loops into a refused command), declared
  4. Hard-threshold forced compaction silently skipped on such backends — justified (removes user-action-free self-wedge), declared
  5. New shared helper compact_unsupported_backend / compact_unsupported_reply — duplicate of session_compaction._compact_unsupported_backend
  6. Slack decline written to the SEL audit log — declared, mirrors dashboard
  7. Per-surface translated / plain-text reply copies — justified by each surface's voice convention
  8. providers.md updated in-commit — mandated by AGENTS.md

Watch

  • messaging.commands.compact_unsupported_backend (commands.py:334) is logic-identical to session_compaction._compact_unsupported_backend (session_compaction.py:148), and dashboard/chat_runner.py:5645-5661 inlines the same read — grep manual_compact_unsupported_backend, 3 read-site spellings after this PR. They will drift.
  • The PR's own harvest rule ("grep the ungated call shape across the whole tree") leaves 2 ungated dispatches: task_executor.py:642 is the exact reported shape (compact() + wait_for_compaction(), no capability check), and cli_chat.py:998 dispatches ungated (softer harm: no wait, provider restarts). Accepted-and-deferred if out of scope, but the description claims the residue is only "the session-level auto-compact path".

Subtractions

  • Delete session_compaction._compact_unsupported_backend (session_compaction.py:132-149) and call the new public helper there; same for chat_runner's inline read.
  • Replace chat_runner.py:5673's inline reply f-string with compact_unsupported_reply — it is the same sentence, hand-copied.

[FIRST-PRINCIPLES-REVIEWED] f205ad8

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of f205ad8d90cc7236f6fe2d5d493882762018131d and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] f205ad8

False positive or not applicable? A repository writer can comment:
/ai-review override gpt f205ad8d90cc7236f6fe2d5d493882762018131d: <one-sentence reason>

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Sep 3, 2026
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>
@NicholasRBowers
NicholasRBowers force-pushed the fix/messaging-compact-capability-gate-8156 branch from e07373d to f205ad8 Compare September 3, 2026 14:16
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention labels Sep 3, 2026
@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

Disposition: accepted-and-deferred — First Principles concern: third spelling of the compact-capability predicate ("Watch" item 1 + "Subtractions").

messaging.commands.compact_unsupported_backend (commands.py:334) is logic-identical to session_compaction._compact_unsupported_backend (session_compaction.py:148), and dashboard/chat_runner.py:5645-5661 inlines the same read — 3 read-site spellings after this PR. They will drift.

The concern is legitimate: this PR does add a third read site for the same manual_compact_unsupported_backend predicate. Consolidating is deliberately out of scope here — deleting the private session_compaction copy and rewiring dashboard/chat_runner.py's inline gate (shipped in #7816) widens a channels-scoped fix into the dashboard surface and re-arms every reviewer on code this PR does not otherwise touch. The new helper was written public precisely so it can become the single spelling.

Deferred to #8203 (deferred-finding, assigned, Due: 2026-09-24): delete session_compaction._compact_unsupported_backend, replace chat_runner's inline read and hand-copied reply f-string with compact_unsupported_backend() / compact_unsupported_reply().

@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

Disposition: accepted-and-deferred (with severity rebuttal) — First Principles concern: two ungated compact() dispatches outside the messaging surfaces ("Watch" item 2).

The PR's own harvest rule ("grep the ungated call shape across the whole tree") leaves 2 ungated dispatches: task_executor.py:642 is the exact reported shape (compact() + wait_for_compaction(), no capability check), and cli_chat.py:998 dispatches ungated (softer harm: no wait, provider restarts).

Neither site reproduces the #8156 defect (an unbounded wedge), so neither is a completeness gap in this fix:

  • task_executor.py:~642wait_for_compaction() defaults to a bounded COMPACT_WAIT_TIMEOUT_SECS (300s, constants.py:72) and returns {"type": "timeout"}, and the handler already resets the session on any non-completed result, with a further except Exception → sessions.reset() fallback. Worst case on an auto-managed backend is one bounded 300s stall per context-overflow followed by automatic recovery — wasteful, not a wedge.
  • cli_chat.py:~998 — no wait_for_compaction() at all: compact() is exception-swallowed and the provider is unconditionally shut down and restarted, which is what clears context regardless of backend capability.

Both are also outside the surfaces #8156 and this fix(channels) PR scope (task runner, CLI chat loop). Gating them for consistency + skipping the wasted 300s stall is tracked in #8203 (deferred-finding, assigned, Due: 2026-09-24) alongside the predicate consolidation; #8203 also records that the total compact() residue is these two sites plus #7812's session-level auto-compact path, sharpening the description's residue sentence.

@bolichen97
bolichen97 enabled auto-merge (squash) September 3, 2026 18:51

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: passed present, and PR Readiness — the one required status context on main
    (ruleset protected-branches) — is success on this head.
  • No check run on this head is failure, cancelled, timed_out or still in flight. Skipped
    jobs are path-filtered conditionals, none of them required.
  • mergeable: true, and the head is not far enough behind main for 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 by dismiss_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.

@bolichen97
bolichen97 merged commit c3668ec into main Sep 3, 2026
100 of 102 checks passed
@bolichen97
bolichen97 deleted the fix/messaging-compact-capability-gate-8156 branch September 3, 2026 18:54
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 3, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This 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

  • PR #7823 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7823: REBASE. The only blocker is a trivial append-at-EOF collision in one test file; both classes belong in the file and no code hunk conflicts. Files: test/test_feishu_dispatch.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Messaging-surface /compact dispatches to a backend that cannot answer it

2 participants