Repository navigation
fix(workspace): a gated request's ending notice no longer reads "Finished" (#3688) - #3757
Conversation
…shed" (#3688) After #3657 the requester is woken (triggered_by="skill_gate") with how its gated request ended, and the reply is reported into the Workspace thread it came from. _portal_body chose its header, and _portal_source its pill, from the wake-up row's own run status, which completes normally for a rejected, expired or not-run request. The reader saw a "Finished" header and a green "Finished" pill above "Your request was rejected." A skill_gate row that completed now gets the header "Approval update" and the source completion:notice, which shows no outcome pill and is narrated as a background report. One predicate decides both, so the header and the pill cannot disagree. A wake-up that itself failed still reads "Didn't finish"; the approved run keeps its requester's trigger and still reads "Finished". triggered_by is passed to every resolver, per the shared call shape. Fixes #3688 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AndriiPasternak31
left a comment
There was a problem hiding this comment.
Approving. The Workspace fix is right: it keys on triggered_by, not on the body text, so the header, the source marker and the Inbox pill can't disagree. test_3688_gate_notice_header.py is 7/12 red on dev and 12/12 green here. Across 31 related test files the only failures are the 3 in test_ent751_gate_callers.py::TestPaidA2A, and they fail the same way on dev.
One follow-up, plus three nits:
Follow-up: Slack and Telegram still head the notice "✅ Task finished". _summarize (channel_completion_report.py:136-139) doesn't look at triggered_by, and the resolvers take it but ignore it (the comments at :163 and :213 say so). A gate wake-up does reach those channels, because it copies the asking turn's source_channel; test_3653_gate_wake_channel_context.py:110-118 pins a rejected wake-up into a Slack thread. I drove report_completion with a triggered_by="skill_gate" row on each channel, using the ent#457 harness, and both legs sent ✅ Task finished\n\nOK, noted: my request to run pay-invoice was rejected; nothing was run. The fix is small: pass triggered_by into _summarize at :190 and :269, and reuse _is_gate_notice for the head line, so one predicate drives every surface. Here or as a follow-up issue, your call.
Nits:
test_a_notice_shows_no_outcome_pill_in_the_inbox(:101-104) passes ondev._OUTCOME_BY_SOURCE.get()already returnsNonefor an unknown source, so the"completion:notice": Noneentry is documentation, not behaviour. The real guard is thetest_the_pill_source_agrees_with_the_header[...completion:notice]param. Either drop the test, or drive a row throughchat_previews._project._is_gate_notice(:560) hard-codes"skill_gate"instead of usingskill_gate_service.SKILL_GATE_TRIGGER. If the constant is renamed, the header silently goes back to "Finished" and every test stays green. A lazy import, like the one already done fordb.skill_gate_requests, avoids the cycle.- The marker docs still say
completion:done | completion:failed:architecture/workspace.md:872,requirements/core-agent.md:3861,feature-flows/workspace-inbox.md:263,364andfeature-flows/skill-gate.md:33.
Not covered: I didn't drive a rejected request through the UI, and I didn't grep the enterprise submodule for other readers of completion:*.
Summary
triggered_by="skill_gate") with how its gated request ended, and that reply goes into the Workspace chat it came from._portal_bodychose the header, and_portal_sourcethe Inbox pill, from the wake-up run's own status. That run completes normally for a rejected, expired or not-run request, so the reader saw a "Finished" header and a green "Finished" pill above "Your request was rejected."skill_gaterow that completed now gets:completion:notice, which shows no outcome pill and is still narrated as a background report in chat history._is_gate_notice) decides both, so the header and the pill can't disagree.Changes
src/backend/services/channel_completion_report.py:COMPLETION_SOURCE_NOTICE,_is_gate_notice, the header and source choice, andtriggered_bypassed to every resolver (the shared call shape from the ent#457 review).src/backend/client_portal/chat_previews.py: the notice maps to no outcome pill.src/backend/client_portal/service.py: the notice counts among the completion sources for history narration.tests/unit/test_3688_gate_notice_header.py(12), through the realreport_completion→_resolve_portal→add_portal_messagepath:Tested against a local instance
rc4 backend, SQLite. A "rejected" notice was filed through the real
report_completioninto two new test chats onproj-alpha: one on the current code, one with this branch's three files swapped in.**Finished**/Your request to run pay-invoice was rejected…, sourcecompletion:done.**Approval update**/ the same text, sourcecompletion:notice.Both test chats were deleted and the files restored. Screenshots were shared with the reviewer separately (not committed).
Test Plan
test_3688_gate_notice_header.py: 12 passed.TestPaidA2A, which also fail on unchangeddevon that machine and pass in CI.Fixes #3688
🤖 Generated with Claude Code