Skip to content

feat(reminders): implement structured delivery contract (closes #690, folds in #644)#692

Merged
Aaronontheweb merged 11 commits intodevfrom
claude-wt-reminder-delivery-standards
Apr 21, 2026
Merged

feat(reminders): implement structured delivery contract (closes #690, folds in #644)#692
Aaronontheweb merged 11 commits intodevfrom
claude-wt-reminder-delivery-standards

Conversation

@Aaronontheweb
Copy link
Copy Markdown
Collaborator

@Aaronontheweb Aaronontheweb commented Apr 20, 2026

Summary

Replaces the coupled reportToChannel / notifyInstructions / notifyPolicy surface on set_reminder with a structured delivery contract. Addresses #690 and folds in #644 (multi-transport routing).

What Changed

New delivery contract

  • delivery: { kind, transport?, address? }kind is a required enum (current_session / channel / none) that directly selects execution mode. No more inference from which optional field happens to be populated.
  • deliveryRequired: bool — policy, default true.
  • deliveryInstructions: string? — content guidance only, never routing.

Transport-keyed resolver dispatch

Adds IReminderTargetResolver.Transport property so the tool dispatches by transport key. Uses ChannelType.ToWireValue() for transport comparison (no stringly-typed checks). ReminderDelivery.GetNotificationToolName() centralizes tool name derivation.

Protocol changes

  • Added DeliveryKind enum and ReminderDelivery class to ReminderProtocol.cs
  • ReminderDefinition now uses Delivery struct instead of separate SessionId/OriginChannelType/ReportToChannel fields
  • ReminderInfo updated to expose Delivery, DeliveryRequired, DeliveryInstructions

Execution changes

  • ReminderManagerActor: Dispatches based on Delivery.Kind instead of inferring mode
  • ReminderExecutionActor: Uses RoutesBackToOriginSession property; throws on unexpected DeliveryKind
  • ExecutionOutputAccumulator: Accepts notification tool name as constructor parameter

OpenSpec artifacts

Planning artifacts in openspec/changes/reminder-delivery-contract/ with proposal, design, specs, and tasks.

Archived predecessors:

  • reminder-audience-authorization
  • reminder-session-reentry

Remaining Work (tracked in tasks.md)

  • ReminderDeliveryObserved signal for delivery confirmation
  • Stale schema hard-delete at store load
  • System skill update
  • Evals
  • Quality gates

Related

Test plan

  • All 2,250 tests pass
  • dotnet build — 0 warnings, 0 errors
  • dotnet slopwatch analyze — verify no new violations
  • Manual test: create reminder with each delivery kind

Addresses #690 and folds in #644.

Creates the `reminder-delivery-contract` OpenSpec change with full
artifact set (proposal, design, specs delta, tasks). Replaces the
coupled `reportToChannel` / `notifyInstructions` / `notifyPolicy`
surface on `set_reminder` with four structured fields:

- `delivery: { kind, transport?, address? }` — kind is a required
  enum (`current_session` / `channel` / `none`) that directly
  selects execution mode (no more inference from populated fields)
- `deliveryRequired: bool` — policy
- `resultTemplate: string?` — content guidance only, never routing

Adds transport-keyed `IReminderTargetResolver` (`Transport` property)
as the structural hook for #644 so the next transport ships without
another schema rewrite.

Closes the silent-failure gap observed in journal session
`D0AC6CKBK5K/1776697725.361339` by introducing a
`ReminderDeliveryObserved` outbound gateway signal: for
`CurrentSession` with `DeliveryRequired = true`, the execution actor
waits for the session's assistant reply to actually surface through
the channel before acking the envelope. Miss → execution failed,
alert fires, Akka.Reminders redelivers.

Netclaw is pre-production with N=1. Stale reminder rows are
hard-deleted at startup; no migration code.

Also archives `reminder-audience-authorization` (previously complete
but unarchived) and `reminder-session-reentry` (60/67 tasks; remaining
tasks were verify/sync/archive bookkeeping + three follow-up issues,
two of which were YAGNI and one — SignalR channel extraction — is
filed as #691). Both use `--skip-specs` because the main specs were
already synced.

No code changes in this commit — planning artifacts only.
- Rename ResultTemplate → ResultGuidance for clearer intent
- Clarify that DeliveryRequired is ignored for None deliveries
- Move duplicate transport detection to startup (fail-fast on boot)
- Add session rehydration note to skill guidance
Clearer semantics: field provides guidance for what to include in
the delivery to the user, not guidance about task output in general.
Replace the inference-based reminder routing model with an explicit
DeliveryKind-based contract. The mixed ReportToChannel/NotifyInstructions/
NotifyPolicy surface is replaced with:

- DeliveryKind enum: CurrentSession, Channel, None
- ReminderDelivery struct: Kind, Transport, Address, SessionId, OriginChannelType
- DeliveryRequired: bool (replaces NotificationPolicy enum)
- DeliveryInstructions: content guidance (was NotifyInstructions)

Key changes:
- SetReminderTool now requires explicit delivery_kind parameter
- IReminderTargetResolver gains Transport property for keyed dispatch
- ReminderManagerActor branches on Delivery.Kind instead of field inference
- ReminderExecutionActor renamed Mode B → CurrentSession delivery path
- ExecutionOutputAccumulator updated for explicit delivery semantics
- All tests updated for new API surface

This is a breaking schema change; existing reminders will fail deserialization
and be dropped at startup (N=1 pre-production user, hard-delete acceptable).

Closes #690, folds in #644
Replace silent default case with ArgumentOutOfRangeException to catch
bugs early if new DeliveryKind values are added without updating this
switch expression.
…l names

- Use ChannelType.ToWireValue() constants instead of raw "signalr"/"tui" strings
- Add ReminderDelivery.GetNotificationToolName() to derive tool from transport
- ReminderExecutionActor now uses transport-derived notification tool name
  instead of hardcoded "send_slack_message"

This completes task 5.7 (transport-derived accumulator tool name).
@Aaronontheweb Aaronontheweb changed the title docs(openspec): plan reminder-delivery-contract (closes #690, folds in #644) feat(reminders): implement structured delivery contract (closes #690, folds in #644) Apr 21, 2026
The /api/reminders POST endpoint was still passing old params
(ReportToChannel, NotifyInstructions, NotifyPolicy) to SetReminderTool
which now requires the new delivery contract fields (DeliveryKind,
DeliveryTransport, DeliveryAddress, DeliveryRequired, DeliveryInstructions).

Changes:
- Add new delivery fields to CreateReminderRequest
- Keep legacy fields for backward compatibility
- Endpoint maps legacy fields to new contract when new fields not provided
- Default DeliveryKind to "none" when no delivery target specified

This fixes the smoke test failure where CLI reminder create was missing
the required DeliveryKind parameter.
Drop backward compatibility - no external consumers exist yet.

CLI now uses:
  --delivery <kind>     (none, channel)
  --transport <slack>
  --address <target>

REST endpoint uses DeliveryKind (required), DeliveryTransport,
DeliveryAddress directly. Legacy reportToChannel/NotifyInstructions
fields removed.
@Aaronontheweb Aaronontheweb marked this pull request as ready for review April 21, 2026 14:10
@Aaronontheweb Aaronontheweb enabled auto-merge (squash) April 21, 2026 14:10
@Aaronontheweb Aaronontheweb disabled auto-merge April 21, 2026 14:22
@Aaronontheweb Aaronontheweb merged commit 3981955 into dev Apr 21, 2026
4 checks passed
@Aaronontheweb Aaronontheweb deleted the claude-wt-reminder-delivery-standards branch April 21, 2026 14:47
Aaronontheweb added a commit that referenced this pull request Apr 29, 2026
)

* chore(openspec): archive 19 completed changes and sync delta specs

Archive all implemented OpenSpec changes, syncing their delta specs to
main specs in chronological order before moving to archive. Resolves
13 cross-change spec conflicts by applying older changes first so
newer implementations take precedence.

Archived changes:
- add-discord-init-reminder-dm-support
- background-job-execution
- channel-ingress-attachments
- channels-content-delivery-guarantees
- containerize-daemon-and-evals
- device-pairing
- discord-channel-with-interactions
- exposure-modes
- graceful-config-restart-drain
- hub-auth-framework
- inbound-webhooks
- mcp-audience-tool-grants
- mcp-server-approval-defaults
- multi-speaker-attribution
- public-audience-security-hardening
- session-cwd-tracking
- structured-tool-call-metadata
- tool-approval-gates
- working-context-grounding

New main specs created: audience-context-filtering, daemon-container,
daemon-exposure, device-pairing, feature-selection-wizard, hub-auth,
inbound-webhooks, netclaw-discord-socket, project-instructions,
session-cwd, tool-call-metadata

Remaining active changes: reminder-delivery-contract (partial),
subagent-explicit-model-selection (blocked on #648)

* chore(openspec): archive reminder-delivery-contract and sync specs

Implementation is complete (PR #692, #752). Remaining eval cases
tracked in #793.
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.

Standardize Reminder Delivery via Structured Contract (Mode B/Session Re-entry)

1 participant