Skip to content

Add scheduled prompt timers, per-session notification opt-in, and "still running" reminders#247

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/build-notification-and-timers
Draft

Add scheduled prompt timers, per-session notification opt-in, and "still running" reminders#247
Copilot wants to merge 3 commits intomainfrom
copilot/build-notification-and-timers

Conversation

Copy link
Contributor

Copilot AI commented Feb 28, 2026

Adds three notification and timer features to Copilot sessions.

Scheduled prompt timers

The core feature: set a timer that automatically runs a given prompt in a session and reports back with the results via system notification.

  • ScheduledPrompt model — stores session name, prompt text, optional label, NextRunAt, RepeatIntervalMinutes, IsEnabled, LastRunAt
  • ScheduledPromptService — background timer checks every minute for due prompts, fires them via SendPromptAsync (enqueues if session is busy, retries if session is not found), sends a system notification when the result arrives. Persists schedules to ~/.polypilot/scheduled-prompts.json
  • "📅 Schedule a prompt…" added to both the sidebar session menu and the dashboard card menu
  • Schedule form in the sidebar footer and a modal overlay on the dashboard — enter the prompt, an optional label, and choose:
    • After a delay: 1 / 5 / 10 / 15 / 30 min, 1h, 2h (one-shot)
    • Repeating: every 5 / 15 / 30 min, 1h, 2h, 6h, or daily
  • The form also lists active schedules for the session with a countdown and a remove button

Per-session notification opt-in

  • AgentSessionInfo.NotifyOnComplete (bool, default false) — per-session flag independent of the global EnableSessionNotifications setting
  • CopilotService.SetSessionNotifyOnComplete(sessionName, bool) — public toggle method
  • Session card menu and sidebar item menu both get "🔔 Notify when done" / "🔔 Watching (tap to stop)" toggle; bell badge appears in card header and sidebar when active
  • Completion notification fires when either the global setting or per-session flag is set

Timer-based "still running" reminders

  • ConnectionSettings.NotificationReminderIntervalMinutes (int, default 0 = off) — configures reminder cadence
  • The processing watchdog calls SendReminderNotificationIfDueAsync each tick with interval-based deduplication via Interlocked.CompareExchange
  • Reminder body: "⏱ Still running · 5m elapsed" / "⏱ Still running · 1h 10m elapsed"
  • Settings UI shows a reminder interval dropdown (Off / 2 / 5 / 10 / 15 / 30 min) below the existing notification toggle

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Copilot AI changed the title [WIP] Add notification and timer functionality for prompts Add per-session notification opt-in and timer-based "still running" reminders Feb 28, 2026
@PureWeen
Copy link
Owner

@copilot no, what i wanted here is a way to set timers thay will run a given prompt and them report back to me

Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 28, 2026

@copilot no, what i wanted here is a way to set timers thay will run a given prompt and them report back to me

Implemented in c21cd08. The feature works as follows:

Scheduling a prompt — from any session's context menu (sidebar or card), tap "📅 Schedule a prompt…" to open a form where you enter:

  • The prompt text to send
  • An optional label
  • Run mode: after a delay (1 min / 5 / 10 / 15 / 30 / 1h / 2h) or repeating (every 5 min / 15 / 30 / 1h / 2h / 6h / daily)

How it works:

  • A background timer checks every minute for due prompts
  • When a timer fires, the prompt is sent to the target session via SendPromptAsync; if the session is busy, it's enqueued instead
  • A system notification is sent when the result arrives (title 📅 <label>, body = first 80 chars of the response)
  • Repeating schedules automatically advance to the next interval; one-shot prompts disable themselves

Managing schedules — the panel also lists existing schedules for the session with a countdown (e.g. "12m", "1h 5m") and an ✕ to remove them.

Schedules persist across restarts in ~/.polypilot/scheduled-prompts.json.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 192.0.2.1
    • Triggering command: /usr/share/dotnet/dotnet /usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/PolyPilot/PolyPilot/PolyPilot.Tests/bin/Debug/net10.0/PolyPilot.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/PolyPilot/PolyPilot/PolyPilot.Tests/bin/Debug/net10.0/PolyPilot.Tests.deps.json /home/REDACTED/work/PolyPilot/PolyPilot/PolyPilot.Tests/bin/Debug/net10.0/testhost.dll --port 44609 --endpoint 127.0.0.1:044609 --role client --parentprocessid 4396 --telemetryoptedin false (packet block)
    • Triggering command: /usr/share/dotnet/dotnet /usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/PolyPilot/PolyPilot/PolyPilot.Tests/bin/Debug/net10.0/PolyPilot.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/PolyPilot/PolyPilot/PolyPilot.Tests/bin/Debug/net10.0/PolyPilot.Tests.deps.json /home/REDACTED/work/PolyPilot/PolyPilot/PolyPilot.Tests/bin/Debug/net10.0/testhost.dll --port 45365 --endpoint 127.0.0.1:045365 --role client --parentprocessid 5352 --telemetryoptedin false (packet block)
  • josvsblobprodcus372.vsblob.vsassets.io
    • Triggering command: /usr/bin/dotnet dotnet restore --no-dependencies /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/622A4908CDBE24FF535A100F8AC55E4F/missingpackages_workingdir --packages /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/missingpackages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal --configfile /tmp/codeql-scratch-abbd4936ea2bb08e/dbs/csharp/working/nugetconfig/nuget.config --force (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Add per-session notification opt-in and timer-based "still running" reminders Add scheduled prompt timers, per-session notification opt-in, and "still running" reminders Feb 28, 2026
PureWeen added a commit that referenced this pull request Mar 5, 2026
Root cause of wrong-remote pushes:
- Workers used 'git fetch origin pull/<N>/head:pr-<N>' which creates a branch
  with NO tracking info. Subsequent 'git push' defaulted to origin, silently
  pushing to PureWeen/PolyPilot even for fork PRs.
- Workers also used 'git rebase + --force-with-lease' which is unnecessary
  when using merge.

Fix:
- .squad/routing.md: updated Fix Process to use 'gh pr checkout <N>' (sets
  correct tracking) and 'git merge origin/main' (no force push needed)
- .squad/decisions.md: explicit rules against force push and manual fetch
- push-to-pr.sh: script that derives correct remote from gh pr view metadata
  and pushes without --force, with pre-push safety checks

Verified: 'gh pr checkout' correctly sets tracking to fork remote for PR #280
(vitek-karas) and to origin for same-repo PRs like #247 (PureWeen).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen added a commit that referenced this pull request Mar 9, 2026
## Problem

PR review workers were pushing to the wrong remote (e.g., pushing to
`origin/PureWeen` when the PR comes from a fork like `vitek-karas`), and
using `--force-with-lease` after rebasing unnecessarily.

### Root Cause (verified by testing)

Workers were using manual checkout:
```bash
git fetch origin pull/<N>/head:pr-<N>
git checkout pr-<N>
```

This creates a branch with **no tracking info** (`Remote: NONE`, `Merge:
NONE`). When the worker then runs `git push` or `git push origin`, git
defaults to `origin` (PureWeen/PolyPilot) — even if the PR came from a
fork.

In contrast, `gh pr checkout <N>` correctly sets up tracking:
- Same-repo PR #247: `Remote: origin`, `Merge:
refs/heads/copilot/build-notification-and-timers` ✅
- Fork PR #280: `Remote: vitek-karas`, `Merge:
refs/heads/Add-a-way-to-specify-polypilot-clone-root-7299` ✅

The second problem was `git rebase origin/main` + `--force-with-lease`,
which requires force pushing. Using `git merge origin/main` adds a merge
commit instead — no force push needed.

## Changes

### `push-to-pr.sh`
A shell script that:
1. Reads PR metadata via `gh pr view` to find the correct owner and
branch
2. Maps the owner to the correct git remote
3. Pushes to that remote **without** `--force`
4. Verifies the push landed by comparing local and remote HEADs
5. Refuses to push if current branch doesn't match the PR branch (guards
against wrong-branch pushes)

### `.squad/routing.md`
Updated Fix Process replacing rebase+force-push with merge+safe-push,
with clear explanation of why `gh pr checkout` is required.

### `.squad/decisions.md`
Explicit shared rules:
- Never force push
- Always use `gh pr checkout` (not manual fetch)
- Always merge (not rebase)
- Always verify push target before pushing
- Use `push-to-pr.sh` for all PR pushes

### `.squad/team.md`
Team definition file (required for squad directory to be recognized by
PolyPilot's squad discovery).

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen
Copy link
Owner

PR #247 Review — Scheduled Prompt Timers & Notifications

CI: ⚠️ No CI configured. Tests: ✅ 1655 passed, 0 failed (checked out branch and ran dotnet test).
Prior reviews: None.


Consensus Findings (2+ of 5 models)

# Severity Location Issue
1 🔴 CRITICAL ScheduledPromptService.cs ~155–175 State persisted before send succeeds — one-shot prompts silently lost
2 �� CRITICAL ScheduledPromptService.cs ~168–176 TOCTOU: IsProcessing race drops prompt or leaves message stuck in queue
3 🟡 MODERATE Dashboard.razor ~253–266 ConfirmSchedule missing try/catch — unhandled exception crashes component
4 🟡 MODERATE ScheduledPromptService.cs ~107–110 SetEnabled mutates IsEnabled outside the lock
5 🟡 MODERATE AgentSessionInfo.cs:88 / CopilotService.cs:2296 NotifyOnComplete is in-memory only — lost on app restart
6 🟢 MINOR CopilotService.Events.cs ~1373 ConnectionSettings.Load() disk I/O every 15s per active session

Detail

🔴 1 — Save-before-send: one-shot prompts silently lost

In FirePromptAsync, sp.NextRunAt is set to null, sp.IsEnabled = false, and Save() is called before SendPromptAsync is called:

// State advanced and persisted here...
sp.NextRunAt = null;
sp.IsEnabled = false;
Save();               // ← prompt now permanently disabled on disk
NotifyChanged();

// ...then send attempted here
var result = await _copilot.SendPromptAsync(sp.SessionName, sp.Prompt);

If SendPromptAsync throws (see issue #2), the outer catch swallows the error. The one-shot prompt is permanently disabled on disk without ever executing — silently lost with no retry possible.

Fix: Move the advance/disable logic to after SendPromptAsync returns successfully. On failure, either re-enable (for one-shots) or log clearly.


🔴 2 — TOCTOU: IsProcessing race

session.IsProcessing is read on the timer thread with no synchronization. Two failure modes:

(a) Another sender starts processing between the IsProcessing == false check and the SendPromptAsync call. SendPromptAsync has an internal CompareExchange guard and throws InvalidOperationException. The outer catch swallows it. Combined with issue #1, the one-shot prompt is dropped.

(b) Session transitions from busy→idle between the IsProcessing == true check and EnqueueMessage. CompleteResponse runs on the UI thread and drains the (empty) queue and clears IsProcessing. EnqueueMessage then adds to a now-unmonitored queue — the message sits there until the user manually sends another prompt.

Fix: Either use EnqueueMessage unconditionally (let the existing queue-drain logic handle both idle and busy sessions), or catch InvalidOperationException from SendPromptAsync and fall back to EnqueueMessage.


🟡 3 — Dashboard ConfirmSchedule missing error handling

SessionSidebar.razor's CreateScheduledPrompt wraps ScheduledPrompts.Add in try/catch and surfaces errors via _scheduleError. The Dashboard's ConfirmSchedule does not:

// Dashboard.razor ~261 — no try/catch
ScheduledPrompts.Add(_scheduleDialogSession.Name, _schedulePrompt.Trim(), ...);
_scheduledConfirmed = true;

If Add throws (e.g., during Save()), the exception propagates unhandled to the Blazor renderer. Fix: match the sidebar's try/catch pattern.


🟡 4 — SetEnabled lock gap

target is found under _lock, lock released, then target.IsEnabled = enabled is written outside the lock. Simultaneously, CheckDuePromptsAsync reads p.IsEnabled inside _lock. The lock does not protect this read/write pair. FirePromptAsync also writes sp.IsEnabled = false (~line 164) outside any lock. Fix: perform the mutation inside _lock.


�� 5 — NotifyOnComplete not persisted

SetSessionNotifyOnComplete sets an in-memory flag. ActiveSessionEntry has no NotifyOnComplete field. Unlike PinSession (which calls SaveOrganization()), this preference is silently reset on app restart. For long-running sessions this defeats the purpose of the feature. Fix: add NotifyOnComplete to ActiveSessionEntry and include it in the debounced save.


🟢 6 — ConnectionSettings.Load() in watchdog hot path

Called once per 15s tick per active processing session. With N sessions active, that is N disk reads/JSON deserializations per 15s. The reminder interval rarely changes. Fix: cache the value in SessionState (set at send time, refreshed on settings save).


Test Coverage Gaps

The ScheduledPromptServiceTests.cs (1655 total tests pass) does not cover:


⚠️ Request Changes

Specific asks:

  1. Move Save() + state mutation after successful send (fixes Polish UI, Rename Sessions, Markdown Output Support, Queued Messages #1 + #2a together)
  2. Make EnqueueMessage the unconditional fallback when session is busy or SendPromptAsync throws InvalidOperationException (fixes #2b)
  3. Add try/catch to Dashboard.razor ConfirmSchedule matching the sidebar pattern (fixes Persistent sessions, streaming fixes, stop button #3)
  4. Wrap IsEnabled mutation inside _lock in both SetEnabled and FirePromptAsync (fixes Tab navigation + shared ChatMessageList component #4)
  5. Persist NotifyOnComplete in ActiveSessionEntry (fixes Add DevTunnel, QR, and MauiDevFlow support #5)

Co-reviewed by: Copilot 223556219+Copilot@users.noreply.github.com

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.

2 participants