Skip to content

fix(reminders): surface the autonomy hold instead of failing silently (#1806) - #1807

Merged
vybe merged 1 commit into
devfrom
fix/1806-reminder-autonomy-hold
Jul 27, 2026
Merged

fix(reminders): surface the autonomy hold instead of failing silently (#1806)#1807
vybe merged 1 commit into
devfrom
fix/1806-reminder-autonomy-hold

Conversation

@dolho

@dolho dolho commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What

Makes the autonomy hold on self-reminders visible. The gate is not changed — a paused agent still must not wake itself. Only the silence is fixed.

Why

get_active_reminders filters ao.autonomy_enabled = 1, so a reminder on a paused agent is never armed. No job is created, so there is no skip to log — and the API reported a healthy-looking row:

status: pending | fire_at: 2026-07-27T10:43:51.215559Z | attempts: None | error: None

Clean A/B on a fresh 0.8.5 instance, two agents, identical 60s reminder set in the same second, only autonomy_enabled differing:

10:43:53Z  scout(autonomy ON)=fired    corbin(autonomy OFF)=pending
10:44:08Z  scout(autonomy ON)=fired    corbin(autonomy OFF)=pending

Worse than the sibling schedules case (#1796) on every observability axis: schedules at least log skipped: … autonomy is disabled, and scheduling.md documents the dependency. Reminders had neither.

Why not reject at create

Considered and rejected. Chat is not autonomy-gated, so an agent can be mid-conversation with a user who says "remind me in an hour". A 409 would fail a legitimate request, and queue-until-unpaused is genuinely useful — the reminder does fire past-due once autonomy returns (verified: held reminder fired ~1 reconcile cycle after enabling). The problem was never the holding; it was not saying so.

Changes

Layer Change
models.py autonomy_hold on ReminderSummary (inherited by Reminder), derived at read time — no column, no dual-track migration. Defaults False.
routers/reminders.py _autonomy_hold() applied to create, list, cancel. Terminal reminders never flagged and skip the lookup. List resolves autonomy once per page (all rows share one agent — a per-row resolve would be a pointless N+1). Fail-open on a read error.
src/scheduler log a count of held reminders per reconcile pass, only when non-zero, best-effort. The only place that can report them, since they never enter the job store.
MCP set_reminder explicit warning sentence when held — not just the boolean, since the caller is an LLM about to tell a user "done" — plus the dependency in the tool description (Invariant #13).
docs/user-docs/automation/agent-reminders.md documents the dependency, mirroring scheduling.md.

Verification

Unittests/unit/test_1806_reminder_autonomy_hold.py, 11 cases, all passing: held for pending/firing, not held when autonomy is on, never flagged for fired/cancelled/failed (asserting the lookup is not even attempted), fail-open on a read error, precomputed autonomy short-circuits the per-row lookup, the shared response shaper carries the flag, and both models default to False.

Live, patched into a running 0.8.5 instance (its backend source is bind-mounted):

CREATE on paused agent   → status: pending | autonomy_hold: True
CREATE on active agent   → status: pending | autonomy_hold: False
LIST on paused agent     → fired   | autonomy_hold: False      # terminal, correctly not flagged
                           pending | autonomy_hold: True

Then toggling autonomy on, without touching the row:

pending | autonomy_hold: False   <- flipped, proving it is derived live, not frozen at create

The instance was restored to stock afterwards and verified clean.

MCP server compiles (tsc via the image build, exit 0).

Not verified

The scheduler log line is unit-and-inspection level only — the scheduler image is not bind-mounted on my test stack, so I did not rebuild it to watch the line appear. The query is the exact complement of get_active_reminders' predicate and is wrapped best-effort.

Follow-up worth its own issue

A reminder held for days fires immediately on unpause, and several would fire at once. "Remind me in an hour" arriving three days late may be worse than never arriving. Deliberately out of scope here — flagging it rather than widening this fix before the release.

Related to #1806

🤖 Generated with Claude Code

Fixes #1806

…#1806)

A reminder set on an agent with autonomy off is never armed: the scheduler's
get_active_reminders filters `ao.autonomy_enabled = 1`, so no job is created
and there is no skip to log. The API reported a bare `pending` next to a
fire_at that quietly slid into the past — fire_attempts null, error null, no
scheduler line, and no mention of autonomy in the reminders docs.

The gate is deliberate (a paused agent must not wake itself) and is NOT
changed. Only its visibility is. Nor is create rejected: chat is not
autonomy-gated, so an agent can be mid-conversation when a user asks for a
reminder, and queue-until-unpaused is the useful behavior — it just has to be
stated.

- models: `autonomy_hold` on ReminderSummary (inherited by Reminder), derived
  at read time from agent_ownership.autonomy_enabled. No column, so no
  dual-track migration. Defaults False so any construction path is safe.
- routers/reminders: `_autonomy_hold()` helper applied to create, list and
  cancel responses. Terminal reminders are never flagged (and skip the lookup
  entirely). The list path resolves autonomy ONCE per page — every row shares
  one agent, so a per-row resolve would be a pointless N+1. Fail-open: a
  settings-read error reports no hold rather than inventing a warning.
- scheduler: log a COUNT of held reminders once per reconcile pass, only when
  non-zero, best-effort. This is the only place that can report them, since
  they never enter the job store.
- MCP set_reminder: an explicit `warning` sentence when held (not just the
  boolean — the caller is an LLM about to tell a user "done"), plus the
  dependency in the tool description.
- docs: agent-reminders.md gains the autonomy dependency, mirroring what
  scheduling.md already documents for schedules.

Related to #1806

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Validated via /validate-pr: derived autonomy_hold flag (read-time, no column — correctly avoids a dual-track migration), all three surfaces in sync (router+models, MCP tool with an explicit LLM-facing HELD warning, scheduler once-per-pass held count). db.get_autonomy_enabled verified present (db/agent_settings/autonomy.py:53). Gate behavior unchanged; fail-open on settings-read failure; terminal rows skip the lookup; list path avoids N+1. 7 named regression tests + user-docs update. Security scan clean. Added 'Fixes #1806' for status auto-promotion.

@vybe
vybe merged commit 204d4bf into dev Jul 27, 2026
22 checks passed
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