Status:
draft · Version 0.2 · Filed 2026-04-22spec_id: SPEC-029 version: “0.2” title: Agent nudges — persistent cross-session reminders for implicit obligations status: draft supersedes: (SPEC-029 v0.1 conversational draft, never filed)
SPEC-029 v0.2 — Agent nudges
1. Summary
Introduces anudges table: persistent, tool-written prompts that surface
across sessions when an agent action creates an implicit obligation a
future surface should check on. Ships with five write points and three
resolution paths. First-class primitive alongside TODOs / notes / WIP —
fills the gap between ephemeral rules_reminders (computed fresh each call)
and TODOs (explicit user work).
Closes the 2026-04-22 “3 unwrapped deltas went silently stale” failure mode
from SPEC-023 §5.4’s heuristic-based detection, and generalizes the fix so
the same mechanism handles draft SPECs, proposed ADRs, unsealed WIPs, open
invites, and issued handoffs without per-case bespoke logic.
2. Origin
2026-04-22 PM discussion (Frank + Donna). Starting from a failed SPEC-029 v0.1 draft that proposed silent auto-wrap onprism_start, two objections
collapsed the design into a better primitive:
- SPEC-023 already rejected auto-wrap on start — “produces artifact
that looks like wrap but isn’t.” v0.1’s
delta_kind='auto_wrap'enum extension was a workaround, not a fix. - Concurrent sessions (Lola on Claude Desktop + Donna on Claude Code) — a time-threshold-based auto-wrap would silently close a live-but-quiet session. No liveness signal exists in the current model.
3. Problem
Three overlapping failure modes existed:- Abandoned sessions stay invisible. SPEC-023 §5.4 detects unwrapped prior sessions by delta-sequence heuristic. The signal is lossy — it rebuilds state from delta history every call, with no persistent marker of “this session owes a wrap.”
- Draft-arc abandonment. A SPEC filed as
status=draftor an ADR asstatus=proposedhas no future-surface reminder. It lives inprism_*(action=list)output, which agents rarely scan proactively. - Cross-session obligations have no primitive. WIP states, open invites, outstanding handoffs — each is tracked in its own table with its own surfacing convention (if any). No uniform “open things that need attention” query exists.
4. Schema
New tablenudges:
Indexes:
(tenant_id, project_id, resolved_at)partialWHERE resolved_at IS NULL— fast open-nudge listing(kind, source_type, source_id)— fast auto-resolution lookup(snooze_until)partialWHERE resolution='snoozed'— reopener sweep
resolved_at and
resolution; the row stays on disk. Re-opening a snoozed nudge creates a
new row with a reference to the prior one in metadata rather than mutating
the resolved row. (Mirrors SPEC-024 projection-retirement ethos.)
4.1 Nudge kinds (v1)
Scope lock for v1: these six. Agent writers do not invent new
kind
values; new kinds require a spec amendment. This prevents “write a nudge
for anything” pollution.
5. Write semantics
Writers MUST:- Use the kind’s canonical
source_type+source_idper §4.1. - Idempotence: if an open nudge already exists with the same
(kind, source_type, source_id), do not write a duplicate. The write path is an UPSERT onresolved_at IS NULL— existing open row wins, new write is a no-op. Audit counter in metadata optional. - Failure-soft: nudge write errors must NEVER fail the parent verb.
Log-and-proceed. (Matches
prism_wrappatch_project pattern,mcp/server.py:1618–1634.)
- Write a nudge with a
kindnot in §4.1. - Write a nudge whose
source_idrefers to an entity that doesn’t exist. - Write a nudge with
severity=blocking(reserved for future spec; v1 enum isinfo | nudgeonly).
6. Resolution semantics
Three resolution paths:6.1 Auto-resolution on state change (preferred)
When the resolving verb fires with matching(kind, source_type, source_id), the server UPDATES open nudge rows in the same transaction:
prism_wrap: (kind='wrap_session', source_type='session', source_id=<current session_id>). Critical: matches current session’s
session_id only — structurally solves the concurrent-Lola-and-Donna case
because Lola’s open wrap_session nudge has a different source_id.
6.2 Manual resolution (user-initiated)
New verbprism_nudge:
list— returns open nudges for the project (optional filter by kind).resolve— setsresolved_at,resolution=ignored|completed|snoozed,resolved_by_verb='prism_nudge.resolve', plus optional note.snooze— shortcut for resolve withresolution='snoozed'plussnooze_until = now() + snooze_hours.
6.3 Scheduled re-opening (snooze sweep)
A background sweep (or on-read check) finds nudges withresolution='snoozed' AND snooze_until <= now() and writes a fresh open
row with the same (kind, source_type, source_id) plus
metadata.reopened_from = <prior row id>. The prior resolved row stays on
disk per the append-never-delete invariant.
v1 implementation: on-read check inside prism_start’s open-nudge query
(WHERE resolved_at IS NULL OR (resolution='snoozed' AND snooze_until <= now()) — no separate background worker needed). The fresh-row write
happens on the next prism_start that observes the expiry. Simple and
avoids cron infrastructure.
7. Surfacing contract
prism_start response adds a top-level pending_nudges array:
prism_sync_bios into CLAUDE.md /
AGENTS.md under the Session Status Card section):
Ifpending_nudgesis non-empty in theprism_startreturn, the agent MUST present the open nudges at the top of the first substantive response, with their available actions clearly offered to the user. The agent proceeds with the user’s current request only after either (a) the user has chosen an action for each nudge, or (b) the user has explicitly said to defer (“later”, “ignore for now”), in which case the agent writes asnoozewith a sensible default (1 hour) and proceeds.
rules_reminders keeps its computed entries (wrap_rate, BIOS drift) and
gains a nudges sibling:
_check_recent_unwrapped_sessions is removed — its job is
now done by the wrap_session nudge lifecycle. Migration note: on first
deploy, run a one-shot backfill that writes wrap_session nudges for any
historical unwrapped sessions in the last 30 days (see §10.2).
8. Agent contract: when to write a nudge
Writer tools follow this decision tree:- Does the action create an implicit obligation a future surface should check on? If no → no nudge.
- Is the obligation cheaply recomputable at read time? If yes →
keep in
rules_reminders(ephemeral, no storage). - Is the obligation an explicit user work item? If yes → use
prism_todo, not a nudge. - Is the obligation scoped to a specific entity’s lifecycle that already has a resolution verb? If yes → nudge with the matching kind per §4.1.
9. Rejected alternatives
- SPEC-029 v0.1 silent auto-wrap — superseded by this spec. Objection: SPEC-023’s “artifact that looks like wrap but isn’t”; concurrency race with live-but-quiet sessions.
- Staleness threshold on the writer side — pushes “is this abandoned?” judgment to wall-clock math. Unreliable for live-but-idle sessions. Nudges defer that judgment to the user at read time instead.
- Reuse session_deltas as nudge storage — would overload
delta_kindand mix ephemeral nudges with append-only history. Separate table keeps semantics clean. - Blocking on start (hard-gate until user resolves) — violates SPEC-021’s soft-gate ethos; breaks scripted use. Kept as soft surfacing contract only.
- Scope-small v1 (wrap nudge only, defer others) — Frank: “I don’t want to create something else I need to come back later to complete. Only defer when I have to.” The five other kinds are trivial write hooks once the table exists; no reason to defer.
10. Implementation
10.1 Sequencing — single PR (est. 1 day)
10.2 Historical backfill (one-shot, same migration)
On first deploy, writewrap_session nudges for any session_ids in
session_deltas from the last 30 days where:
- At least one non-wrap delta exists
- No wrap delta exists for that session_id
10.3 Observability
- Counter:
nudges_opened_total{kind}(30d rolling) - Counter:
nudges_resolved_total{kind, resolution}(30d rolling) - Gauge:
nudges_open{kind}(current open count per kind) - Derived: per-kind median age, resolution rate, snooze rate.
wrap_rate
surfaces).
10.4 Install / deploy impact
Zero install script changes.backend/docker-entrypoint.sh runs
alembic upgrade head on boot; the migration applies on next backend
restart. No env var additions. No compose file changes.
11. Relationships
- Extends SPEC-023 — replaces its §5.4 detection heuristic with
persistent nudge rows. SPEC-023’s
delta_kindcolumn,session_idminting, andwrap_rateobservability all stand. - Uses SPEC-022’s
last_wrapped_*columns unchanged. Auto-resolution ofwrap_sessiononprism_wrapstill patches those columns per SPEC-022 before resolving the nudge. - Supersedes SPEC-029 v0.1 (conversational draft; never filed; preserved in session deltas).
- Related SPEC-021 Ring 0 — nudge surfacing uses the status-card / bootstrap contract as its delivery channel but does not add new gates.
- Informs future spec on org-level nudges (project_id=null) — deliberately scoped out of v1.
12. Open questions
- Q1: Default snooze duration — 1h in the agent contract (§7). Tune with usage; revisit after 30d of data.
- Q2: Should
handoff_pendingauto-resolve on the next persona’sprism_start(as an implicit ack)? Lean no: handoff acknowledgment should be explicit. Defer to follow-up spec. - Q3: Should
spec_draftandadr_proposednudges self-snooze if the draft is being actively edited (delta activity in the last hour referencing the spec_id)? Lean no in v1; nudges are about cross-session memory, not intra-session noise. Revisit if users complain. - Q4: Do we need an
org_nudgestable or can org-scoped nudges reuse this table withproject_id=null? Lean reuse with null. Defer until first org-level use case. - Q5: Per-project agent-contract overrides (e.g. “this project: block
on
wrap_session, don’t for other kinds”) — deferred to follow-up SPEC. v1 is uniform across projects.
13. Acceptance criteria
- ✅
nudgestable exists with all columns + indexes per §4. - ✅
prism_startreturnspending_nudgesarray; empty when no open nudges exist. - ✅
prism_wrapon session_id X auto-resolves any openwrap_sessionnudges withsource_id=Xand does not resolve nudges with different source_ids (concurrency test). - ✅ Writing two
wrap_sessionnudges for the same session_id in a row results in exactly one open row (idempotence). - ✅ All six kinds in §4.1 have integration tests: write, auto-resolve
on matching verb, manual resolve via
prism_nudge. - ✅ Historical backfill (§10.2) populates expected wrap_session rows for known unwrapped session_ids on the Prism project.
- ✅ Smoke test
mcp/smoke_spec029.pypasses locally + against server1. - ✅ BIOS templates updated with agent contract; propagated via
prism_sync_bios. - ✅
rules_remindersno longer emitsunwrapped_sessionentries; those are replaced bypending_nudgesentries ofkind=wrap_session. - ✅ Wrap-rate metric (SPEC-023 §7) unchanged behaviorally.

