feat(platform): copilot followups UI - #13192
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR implements copilot follow-up scheduling, enabling users to defer work into future chat turns via a new ChangesCopilot Follow-up Scheduling
Sequence Diagram(s)sequenceDiagram
participant User
participant Frontend as Frontend Page
participant API
participant Scheduler
participant ChatSession
participant APScheduler as Job Runner
User->>Frontend: Navigate to follow-ups
Frontend->>API: GET /schedules/followups
API->>Scheduler: get_execution_schedules(user_id, kind=copilot_turn)
Scheduler->>API: [CopilotTurnJobInfo]
API->>Frontend: 200 JSON
Frontend->>Frontend: Render list with cancel buttons
User->>Frontend: Click cancel on follow-up
Frontend->>Frontend: Show confirmation dialog
User->>Frontend: Confirm deletion
Frontend->>API: DELETE /schedules/{schedule_id}
API->>Scheduler: delete_graph_execution_schedule(schedule_id, user_id)
Scheduler->>APScheduler: Remove job
API->>Frontend: 200
Frontend->>Frontend: Invalidate query, refetch list
Frontend->>Frontend: Show success toast
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔍 PR Overlap DetectionThis check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early. 🔴 Merge Conflicts DetectedThe following PRs have been tested and will have merge conflicts if merged after this PR. Consider coordinating with the authors.
🟡 Medium Risk — Some Line OverlapThese PRs have some overlapping changes:
🟢 Low Risk — File Overlap OnlyThese PRs touch the same files but different sections (click to expand)
Summary: 6 conflict(s), 1 medium risk, 8 low risk (out of 15 PRs with file overlap) Auto-generated on push. Ignores: |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
autogpt_platform/backend/backend/executor/scheduler.py (1)
580-583: ⚡ Quick winUse discriminator-based branching instead of duck-typing/type checks.
These branches currently rely on
hasattr/isinstancefor dispatch. Since schedule info is already discriminated bykind, switch tokind-based flow to satisfy typed dispatch rules and keep filtering logic simpler.♻️ Suggested change
def _timezone_from_job(job_obj: JobObj) -> str: - if hasattr(job_obj.trigger, "timezone"): - return str(job_obj.trigger.timezone) + if job_obj.next_run_time and job_obj.next_run_time.tzinfo: + return str(job_obj.next_run_time.tzinfo) return "UTC" @@ - if graph_id is not None and ( - not isinstance(info, GraphExecutionJobInfo) or info.graph_id != graph_id - ): - continue - if session_id is not None and ( - not isinstance(info, CopilotTurnJobInfo) - or info.session_id != session_id - ): - continue + if graph_id is not None and ( + info.kind != "graph" or info.graph_id != graph_id + ): + continue + if session_id is not None and ( + info.kind != "copilot_turn" or info.session_id != session_id + ): + continueAs per coding guidelines: "Do not use duck typing — avoid
hasattr/getattr/isinstancefor type dispatch; use typed interfaces/unions/protocols instead".Also applies to: 1138-1145
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@autogpt_platform/backend/backend/executor/scheduler.py` around lines 580 - 583, The _timezone_from_job function uses duck-typing via hasattr on job_obj.trigger; change it to discriminate on job_obj.kind instead and handle the cases that carry timezone info (e.g., the scheduled/cron trigger kinds) by reading job_obj.trigger.timezone only for those kinds, otherwise return "UTC"; apply the same kind-based branching refactor to the similar logic referenced around the other function/section that inspects triggers (the block at lines handling job kind -> trigger attributes) so all dispatch uses job_obj.kind rather than hasattr/isinstance.autogpt_platform/backend/backend/executor/scheduler_test.py (1)
101-110: ⚡ Quick winTighten the failure assertion for invalid trigger input.
Catching bare
Exceptionhere is too permissive and may hide unrelated failures. Assert on the expected error message so this test only passes for the intended rejection path.♻️ Suggested change
- with pytest.raises(Exception) as exc: + with pytest.raises(Exception, match="Exactly one of `cron` or `run_at` must be provided"): await scheduler.add_copilot_turn_schedule( user_id=test_user.id, session_id=session_id, message="x", user_timezone="UTC", ) - # ValueError from _build_trigger propagates as a RemoteError - # through the AppService transport; just verify the call rejected. - assert exc.value is not None🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@autogpt_platform/backend/backend/executor/scheduler_test.py` around lines 101 - 110, The test currently catches a broad Exception when calling scheduler.add_copilot_turn_schedule; narrow this to assert the expected failure by checking the raised error and its message from the _build_trigger path: replace the pytest.raises(Exception) with pytest.raises(RemoteError) or the concrete transport-wrapped error type used in your test harness, capture it as exc, and add an assertion that exc.value (or exc.value.args[0]) contains the expected invalid-trigger message (e.g. text from _build_trigger like "invalid trigger" or the exact ValueError message you expect) so the test only passes for the intended rejection path originating from _build_trigger called by scheduler.add_copilot_turn_schedule with test_user.id and session_id.autogpt_platform/backend/backend/api/features/v1_test.py (1)
1052-1052: ⚡ Quick winMove scheduler model imports to module scope.
Line 1052 introduces a local import in a backend test; this should be a top-level import.
♻️ Proposed refactor
from backend.data.credit import AutoTopUpConfig from backend.data.graph import GraphModel +from backend.executor.scheduler import CopilotTurnJobInfo, GraphExecutionJobInfo from backend.util.exceptions import InsufficientBalanceError @@ - from backend.executor.scheduler import CopilotTurnJobInfo, GraphExecutionJobInfo - copilot_info = CopilotTurnJobInfo(As per coding guidelines: "Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies like
openpyxl".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@autogpt_platform/backend/backend/api/features/v1_test.py` at line 1052, The test contains a local import of CopilotTurnJobInfo and GraphExecutionJobInfo from backend.executor.scheduler; move that import statement out of the local scope and add it to the module-level imports near the other top-level imports so CopilotTurnJobInfo and GraphExecutionJobInfo are imported at file import time rather than inside the test body.autogpt_platform/backend/backend/api/features/v1.py (1)
2336-2339: ⚡ Quick winAvoid
isinstancefiltering after already requestingkind="copilot_turn".Line 2339 introduces class-based filtering. Prefer discriminator-driven handling here and return the scheduler result directly (or validate
kind) instead ofisinstancedispatch.♻️ Proposed simplification
async def list_copilot_turn_schedules( user_id: Annotated[str, Security(get_user_id)], ) -> list[scheduler.CopilotTurnJobInfo]: @@ - schedules = await get_scheduler_client().get_execution_schedules( - user_id=user_id, kind="copilot_turn" - ) - return [s for s in schedules if isinstance(s, scheduler.CopilotTurnJobInfo)] + schedules = await get_scheduler_client().get_execution_schedules( + user_id=user_id, kind="copilot_turn" + ) + return cast(list[scheduler.CopilotTurnJobInfo], schedules)As per coding guidelines: "Do not use duck typing — avoid
hasattr/getattr/isinstancefor type dispatch; use typed interfaces/unions/protocols instead".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@autogpt_platform/backend/backend/api/features/v1.py` around lines 2336 - 2339, The code filters schedules with isinstance(s, scheduler.CopilotTurnJobInfo) after calling get_scheduler_client().get_execution_schedules(user_id=user_id, kind="copilot_turn"); instead, remove the class-based filtering and return the scheduler result directly (or assert/validate that each schedule.kind == "copilot_turn") so dispatch is driven by the kind discriminator rather than isinstance checks; update the function that currently references schedules and scheduler.CopilotTurnJobInfo to either return schedules as-is or perform a lightweight kind validation on each item before returning.autogpt_platform/backend/backend/copilot/tools/manage_schedules.py (1)
46-71: ⚡ Quick winReplace
isinstance-based dispatch with discriminator-based branching.Line 49 currently dispatches by concrete class with
isinstance(...). In this backend, type dispatch should use typed discriminators (e.g.,job.kind) instead.♻️ Proposed refactor
-from typing import Any, Literal +from typing import Any, Literal, cast @@ def _to_summary( job: GraphExecutionJobInfo | CopilotTurnJobInfo, ) -> ScheduleSummary: - if isinstance(job, GraphExecutionJobInfo): + if job.kind == "graph": + graph_job = cast(GraphExecutionJobInfo, job) return ScheduleSummary( - schedule_id=job.id, + schedule_id=graph_job.id, kind="graph", - name=job.name, - timezone=job.timezone, - next_run_time=job.next_run_time, - cron=job.cron, - graph_id=job.graph_id, - graph_version=job.graph_version, + name=graph_job.name, + timezone=graph_job.timezone, + next_run_time=graph_job.next_run_time, + cron=graph_job.cron, + graph_id=graph_job.graph_id, + graph_version=graph_job.graph_version, ) - run_at_str = job.run_at.isoformat() if job.run_at else None + copilot_job = cast(CopilotTurnJobInfo, job) + run_at_str = copilot_job.run_at.isoformat() if copilot_job.run_at else None return ScheduleSummary( - schedule_id=job.id, + schedule_id=copilot_job.id, kind="copilot_turn", - name=job.name, - timezone=job.timezone, - next_run_time=job.next_run_time, - cron=job.cron, + name=copilot_job.name, + timezone=copilot_job.timezone, + next_run_time=copilot_job.next_run_time, + cron=copilot_job.cron, run_at=run_at_str, - session_id=job.session_id, - message=job.message, + session_id=copilot_job.session_id, + message=copilot_job.message, )As per coding guidelines: "Do not use duck typing — avoid
hasattr/getattr/isinstancefor type dispatch; use typed interfaces/unions/protocols instead".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@autogpt_platform/backend/backend/copilot/tools/manage_schedules.py` around lines 46 - 71, The _to_summary function currently uses isinstance() to branch on GraphExecutionJobInfo vs CopilotTurnJobInfo; change it to inspect the job.kind discriminator instead (e.g., if job.kind == "graph": ... elif job.kind == "copilot_turn": ...). Keep the same ScheduleSummary field mappings: for kind "graph" populate graph_id and graph_version from GraphExecutionJobInfo fields; for "copilot_turn" convert job.run_at to ISO string when present and populate session_id and message. Preserve job.id/name/timezone/next_run_time/cron in both branches and raise or handle an unexpected job.kind if neither matches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@autogpt_platform/backend/backend/copilot/tools/schedule_followup.py`:
- Around line 127-247: The _execute method is doing too much; split it into
focused helpers to reduce complexity: extract authentication/session check into
authenticate_and_get_current_session(user_id, session) which returns
current_session_id or ErrorResponse, extract message validation into
validate_message(kwargs) returning message or ErrorResponse, extract target
session resolution into resolve_target_session(override_session_id,
current_session_id, user_id) which uses get_chat_session and returns
target_session_id or ErrorResponse, extract trigger parsing into
parse_trigger(kwargs) that returns (run_at, cron, is_recurring) and enforces the
mutually exclusive rule and delay >= 60, extract cron validation into
validate_cron(cron, user_timezone) which calls CronTrigger.from_crontab, extract
user/timezone lookup via user_db().get_user_by_id and get_user_timezone_or_utc
into get_user_timezone(user_id), extract scheduler call into
schedule_followup(...) which calls
get_scheduler_client().add_copilot_turn_schedule and handles ValueError, and
extract response/analytics into finalize_schedule_response(info,
current_session_id, target_session_id, is_recurring) which calls
track_followup_scheduled and returns ScheduleCreatedResponse; then make _execute
a short orchestrator that calls these helpers in sequence and returns their
ErrorResponse or the ScheduleCreatedResponse.
---
Nitpick comments:
In `@autogpt_platform/backend/backend/api/features/v1_test.py`:
- Line 1052: The test contains a local import of CopilotTurnJobInfo and
GraphExecutionJobInfo from backend.executor.scheduler; move that import
statement out of the local scope and add it to the module-level imports near the
other top-level imports so CopilotTurnJobInfo and GraphExecutionJobInfo are
imported at file import time rather than inside the test body.
In `@autogpt_platform/backend/backend/api/features/v1.py`:
- Around line 2336-2339: The code filters schedules with isinstance(s,
scheduler.CopilotTurnJobInfo) after calling
get_scheduler_client().get_execution_schedules(user_id=user_id,
kind="copilot_turn"); instead, remove the class-based filtering and return the
scheduler result directly (or assert/validate that each schedule.kind ==
"copilot_turn") so dispatch is driven by the kind discriminator rather than
isinstance checks; update the function that currently references schedules and
scheduler.CopilotTurnJobInfo to either return schedules as-is or perform a
lightweight kind validation on each item before returning.
In `@autogpt_platform/backend/backend/copilot/tools/manage_schedules.py`:
- Around line 46-71: The _to_summary function currently uses isinstance() to
branch on GraphExecutionJobInfo vs CopilotTurnJobInfo; change it to inspect the
job.kind discriminator instead (e.g., if job.kind == "graph": ... elif job.kind
== "copilot_turn": ...). Keep the same ScheduleSummary field mappings: for kind
"graph" populate graph_id and graph_version from GraphExecutionJobInfo fields;
for "copilot_turn" convert job.run_at to ISO string when present and populate
session_id and message. Preserve job.id/name/timezone/next_run_time/cron in both
branches and raise or handle an unexpected job.kind if neither matches.
In `@autogpt_platform/backend/backend/executor/scheduler_test.py`:
- Around line 101-110: The test currently catches a broad Exception when calling
scheduler.add_copilot_turn_schedule; narrow this to assert the expected failure
by checking the raised error and its message from the _build_trigger path:
replace the pytest.raises(Exception) with pytest.raises(RemoteError) or the
concrete transport-wrapped error type used in your test harness, capture it as
exc, and add an assertion that exc.value (or exc.value.args[0]) contains the
expected invalid-trigger message (e.g. text from _build_trigger like "invalid
trigger" or the exact ValueError message you expect) so the test only passes for
the intended rejection path originating from _build_trigger called by
scheduler.add_copilot_turn_schedule with test_user.id and session_id.
In `@autogpt_platform/backend/backend/executor/scheduler.py`:
- Around line 580-583: The _timezone_from_job function uses duck-typing via
hasattr on job_obj.trigger; change it to discriminate on job_obj.kind instead
and handle the cases that carry timezone info (e.g., the scheduled/cron trigger
kinds) by reading job_obj.trigger.timezone only for those kinds, otherwise
return "UTC"; apply the same kind-based branching refactor to the similar logic
referenced around the other function/section that inspects triggers (the block
at lines handling job kind -> trigger attributes) so all dispatch uses
job_obj.kind rather than hasattr/isinstance.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 842ea27a-a4cb-47d2-820c-7bd2bb429a4f
📒 Files selected for processing (28)
autogpt_platform/backend/backend/api/features/library/db.pyautogpt_platform/backend/backend/api/features/v1.pyautogpt_platform/backend/backend/api/features/v1_test.pyautogpt_platform/backend/backend/blocks/orchestrator.pyautogpt_platform/backend/backend/blocks/test/test_orchestrator_execution_mode.pyautogpt_platform/backend/backend/copilot/permissions.pyautogpt_platform/backend/backend/copilot/sdk/tool_adapter.pyautogpt_platform/backend/backend/copilot/sdk/tool_adapter_test.pyautogpt_platform/backend/backend/copilot/tools/__init__.pyautogpt_platform/backend/backend/copilot/tools/manage_schedules.pyautogpt_platform/backend/backend/copilot/tools/manage_schedules_test.pyautogpt_platform/backend/backend/copilot/tools/models.pyautogpt_platform/backend/backend/copilot/tools/schedule_followup.pyautogpt_platform/backend/backend/copilot/tools/schedule_followup_test.pyautogpt_platform/backend/backend/copilot/tools/tool_schema_test.pyautogpt_platform/backend/backend/copilot/tracking.pyautogpt_platform/backend/backend/data/diagnostics.pyautogpt_platform/backend/backend/executor/scheduler.pyautogpt_platform/backend/backend/executor/scheduler_test.pyautogpt_platform/backend/backend/executor/scheduler_unit_test.pyautogpt_platform/frontend/src/app/(platform)/library/followups/__tests__/main.test.tsxautogpt_platform/frontend/src/app/(platform)/library/followups/components/EmptyFollowups/EmptyFollowups.tsxautogpt_platform/frontend/src/app/(platform)/library/followups/components/FollowupListItem/FollowupListItem.tsxautogpt_platform/frontend/src/app/(platform)/library/followups/components/FollowupListItem/helpers.tsautogpt_platform/frontend/src/app/(platform)/library/followups/components/FollowupListItem/useFollowupListItem.tsautogpt_platform/frontend/src/app/(platform)/library/followups/page.tsxautogpt_platform/frontend/src/app/(platform)/library/followups/useFollowupsPage.tsautogpt_platform/frontend/src/app/api/openapi.json
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## feat/copilot-schedule-followup #13192 +/- ##
==================================================================
+ Coverage 71.64% 71.65% +0.01%
==================================================================
Files 2224 2230 +6
Lines 168261 168311 +50
Branches 17101 17108 +7
==================================================================
+ Hits 120544 120603 +59
+ Misses 44177 44167 -10
- Partials 3540 3541 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Add a Library page that lists pending copilot follow-up schedules and lets users cancel them. Backed by a new GET /api/schedules/followups route that returns only CopilotTurnJobInfo rows for the current user, so the generated frontend client gets a single concrete return type instead of a union. - backend: list_copilot_turn_schedules route + filter test - frontend: /library/followups page with empty state, list item with next-run + cron labels, delete confirmation dialog - tests: integration tests (empty / list / link / delete OK / delete fail)
ca617c1 to
1475614
Compare
E2E Test ReportE2E Test Report: PR #13192 — feat(platform): copilot followups UI
Environment notes
Test Results
Detail1. Cold load — empty statePage renders header "Copilot follow-ups" + 2. Schedule via copilot chatTwo follow-ups created in chat session
API verified — 3. List page renderingBoth rows visible with message preview, "Next in less than a minute" / "Next in about 17 hours", "Runs once" / "Every day at 09:00", "Session 96d58196". Cron expression humanized correctly. ( 4. Deep link click
5. DeleteOriginal one-shot fired before I could click delete (only 120s), so I scheduled a new one with Note: button copy is "Delete" (with 6. Copilot cross-check"List my current follow-up schedules" returned 7. Network / error handlingUsed 8. Empty state with zero rowsAfter deleting the cron schedule via direct API DELETE, the page re-mounts to the Summary
Loose ends
Screenshots |
…tch, delete terminology - backend route: replace defensive `[s for s in schedules if isinstance(s, CopilotTurnJobInfo)]` with `cast(...)`. The `kind="copilot_turn"` filter is the source of truth; the trailing isinstance pass was tautology that would silently drop rows if the discriminator ever drifted. Cast narrows the static type without runtime cost. - useFollowupListItem: drop `onDeleted` callback. `invalidateQueries` already triggers a refetch, and the parent-supplied callback was double-work that raced the toast in the failure path. Single source of truth. - FollowupListItem: rename action from "Cancel" to "Delete" everywhere (button label, aria-label, dialog title, confirm button text "Yes, delete", back button "Keep it", toast titles). The action IS a delete (calls `useDeleteV1DeleteExecutionSchedule`); "Cancel" overloaded with cancel-this-dialog. Matching tests updated. - page.tsx: drop now-unused `refetchFollowups` from the destructure.
Roadmap — copilot followup managementCapturing the bigger plan so the scope of this PR (and what's still ahead) is explicit. This PR is the first slice — the UI page only. Everything below is what comes next. What we're ultimately trying to achieveWhen a user changes their mind about a scheduled followup, the happy path is conversational, not a separate UI ritual:
The UI page added by this PR ( Why the chat-mediated path is P0 over UI affordances
So the UI is the "atlas view", chat is the "GPS". What's required for AutoPilot to do thisFor the model to honour "cancel that", it needs three pieces of information that today's prompt does not surface:
The first two are the open work. The prompt-caching constraint (this is the load-bearing concern)Anthropic prompt caching keys off prefix bytes. If we naively inject Concrete rule:
Proposed slices (post-#13192)Slice A — Session self-awareness for the model. Lowest-risk, highest-leverage.
Slice B — Proactive followup reminders in chat. Optional.
Slice C — Modification via chat. "Move that to 8am instead" — needs a Slice D — UI nav link. Add Followups to the library sidebar (the thread above tracks this). Trivial change, but easier once we know where Library / Marketplace / Copilot sit relative to each other in the broader nav redesign. Are we passing session_id today?No — verified by grepping Why this is in this PR's comments and not a separate issueThis PR (#13192) is the UI page slice. The next slice (A) doesn't belong in #13192 because:
Once #13190 + #13192 land, I'll open a fresh PR for Slice A with the cache-impact A/B test attached as PR-test evidence. Open questions worth flagging now
Tagging this so we don't forget when #13190 + #13192 are in dev. |
a59947a
into
feat/copilot-schedule-followup
The action was renamed end-to-end (button label, dialog title, toast, aria-label) in #13192's polish round 1, but the data-testid attributes kept their old 'cancel' names. Rename both to match: 'followup-cancel-button' → 'followup-delete-button' and 'followup-confirm-cancel' → 'followup-confirm-delete'. Tests updated to match. Cosmetic only; no behavior change.












Why
Copilot can already schedule follow-up turns for itself (#13190), but there is no surface for the user to see what's pending or cancel one — they only see the side-effect when the copilot speaks up later. This adds the minimal Library page that lists them and lets the user cancel.
Stacked on top of #13190 — this PR's diff will collapse to just the UI commit once that lands.
What
/library/followupslisting the current user's pending copilot follow-ups, with:/copilot?sessionId=....Cancelaction that opens a confirmation dialog and calls the existingDELETE /api/schedules/{id}route; success toasts and refetches, failure toasts destructively.GET /api/schedules/followupsreturning onlyCopilotTurnJobInforows for the caller. Split out from the polymorphicGET /api/schedulesso the OpenAPI-generated client gets a single concrete return type per route instead of a discriminated union.How
Scheduler.get_execution_schedules(kind="copilot_turn")plus an explicitoperation_id="listCopilotFollowupSchedules"to keep the generated TS client tidy.EmptyFollowups,FollowupListItem) following thepage.tsx + usePageName.ts + helpers.tspattern infrontend/AGENTS.md. Design-system atoms/molecules only (Text,Button,Dialog,LoadingSpinner,ErrorCard), Phosphor icons only.useListCopilotFollowupScheduleshook; delete reusesuseDeleteV1DeleteExecutionScheduleand invalidates the followups query key on success.kind="copilot_turn"filter is forwarded and non-copilot rows are dropped.Checklist