# Outreach "ask me later" / defer

Plan document: deferral as a third outcome (not dismiss, not a full answer). The codebase already has pieces (mind_queue time windows, micro-brain routing, heartbeat outreach context) but needs an explicit defer path and must avoid relying on `created_at` alone because prompt vs outreach ordering disagree.

## Why this fits the product

Deferral is a normal signal: the user is still engaged with the topic but not ready to spend time on it. The system should **keep** the question and **ease off** proactive pressure, instead of treating the thread as dismissed or fully answered.

## What the code does today (relevant facts)

**Mind queue ordering for proactive sends** — `voice_dispatcher.get_top_item` orders by priority, then **`created_at ASC`** (older items first within the same priority band).

**Time gating** — Rows with `valid_after_local`, `valid_before_local`, or `valid_date` are skipped until the window is open (`_is_time_window_open` in `voice_dispatcher.py`).

**Prompt "queued thoughts"** — `db.get_top_mind_queue_items` uses a **different** ordering (priority buckets plus a stable order from a `created_at DESC` prefetch). So **changing only `created_at`** to "deprioritize" outreach can have the **wrong** effect on how items appear in the conversational preview. **`created_at` alone is not a safe single lever.**

**Reply-queue micro-brain** — The closure prompt treats deferrals ("not right now") as **good-faith engagement** that **closes** reply-queue tasks. Downstream, closing can trigger **`expire_mind_queue_for_last_proactive_outreach`** in `telegram_listener.py`, which **expires pending** mind_queue rows tied to `last_proactive_outreach`. That is the **opposite** of "save for later" when the user only wants to **snooze**.

**After a proactive mind_queue send** — `mark_sent` sets the row to `status='sent'`. `expire_mind_queue_for_last_proactive_outreach` only updates **`pending`** rows. So a defer path must also cover "already sent, nothing pending" via **heartbeat-level snooze** or a **new pending reminder** row—not only updating an already-sent mind_queue id.

## Recommended design

1. **Intent detection**
   - **Fast path:** High-precision regex/keywords for short messages ("ask me later", "not now", "remind me tomorrow", "busy", "ping me in ...") **only when `last_proactive_outreach` is present**. The guard on `last_proactive_outreach` is mandatory — without it, casual user messages like "not now" sent with no active outreach context would silently trigger a defer on nothing.
   - **LLM path:** Small `MODEL_MICRO` JSON classifier (same family as `resolve_reply_queue_closures`) with user text plus `format_dismiss_context_for_llm` / recent outreach previews. Output must separate **`dismiss` vs `answer` vs `defer`**, with optional **`snooze_hours`** or **`until_local`** when the user names a time. Default `snooze_hours` comes from tier1 (not a module-level constant).

2. **Effects (prefer explicit semantics over rewriting timestamps)**
   - **Pending `mind_queue`:** Set **`valid_after_local`** / **`valid_date`** (and optionally demote **`priority`** one step) so `get_top_item` skips until the window. Optionally record deferral in **`source_context`** for debugging — **without** using `created_at` as the only signal.
   - **Reply-queue tasks:** On **`defer`**, **push `due_at`** (and metadata like `snoozed_at`) instead of closing. Do **not** run **`expire_mind_queue_for_last_proactive_outreach`** for deferrals.
   - **Already-sent outreach / no pending row:** Persist a **heartbeat** field (under existing `outreach` / `last_proactive_outreach`, following `db.write_heartbeat_state` merge rules), e.g. **`topic_defer_until_iso`** plus a short **fingerprint** of the last proactive summary. The guard in `should_send_now` (or as a post-`get_top_item` check) should block sends for the **same topic** while inside the defer window. Use the stored fingerprint as the first-tier check (cheap); fall back to `mind_queue_topic_overlaps_corpus` only when no exact fingerprint match is found. Running full NLP similarity on every outreach pulse for a snooze guard is heavier than needed.
   - **`pending_ask_user` in heartbeat:** On defer, also snooze or date-gate matching entries in the heartbeat `pending_ask_user` list. The dismiss path already calls `prune_pending_ask_user_for_questions`; without an equivalent snooze step on defer, a deferred question can resurface through the ask-user flow before the defer window expires.

3. **User-facing ack**
   A short Telegram line ("Got it — I'll bring this up later") must be sent, matching the same pattern as the dismiss ack. Silence would leave the user uncertain whether the deferral registered and break parity with the dismiss path which always acknowledges.

4. **Organic re-introduction clears the defer window**
   If the user raises the deferred topic themselves during a later conversation, the heartbeat `topic_defer_until_iso` field should be cleared — either by `post_conversation.py` detecting topic coverage, or by the soul engine tick. Without this, the system would suppress its own proactive follow-up even after the user already engaged with the topic voluntarily.

5. **Operator knobs**
   Defaults and `memory/operator_settings.json` merge in **`scripts/operator_tier1.py`** (e.g. default defer hours, caps), per operator-tunable-constants rules.

6. **Tests**
   - Classifier: defer vs dismiss vs answer; no false-positive defer when `last_proactive_outreach` is absent.
   - DB/heartbeat: pending mind_queue gets a future window; reply_queue `due_at` moves; mind_queue **not** expired on defer; `pending_ask_user` entries snoozed.
   - Heartbeat snooze guard: same-topic outreach blocked inside defer window; lifted after window expires or topic re-introduced.
   - Regression: existing dismiss and answer paths unchanged.

7. **Documentation**
   When implementing: update **`docs/SELF-AWARENESS-DESIGN.md`** / **`docs/INTEGRATION.md`** if heartbeat or user-visible contract changes; run **`furoshiki design update`** when design prose changes.

## Flow (defer vs current "deferral closes task")

- **Dismiss** → close reply-queue where applicable → expire linked mind_queue (today's behavior).
- **Answer** → close reply-queue → expire linked mind_queue where that path applies.
- **Defer (new)** → snooze mind_queue and/or heartbeat topic defer → snooze `pending_ask_user` entries → **bump** reply_queue `due_at` → **do not** expire mind_queue for defer → send ack.

## Opinion

The feature is **worth building**: it matches real behavior and avoids treating a deferral as a completed answer. The important details are: **do not** treat deferrals like answers for reply-queue closure; **do not** rely on **`created_at` alone** because outreach ordering and prompt queue previews are not aligned; **guard the fast-path regex on `last_proactive_outreach` presence**; and **snooze `pending_ask_user` alongside mind_queue** so the defer window is honored across all outreach surfaces.

## Implementation todos

1. **Intent split** — Add defer vs dismiss vs answer (regex fast path guarded by `last_proactive_outreach` + `MODEL_MICRO` JSON, reuse last-outreach context helpers). Default `snooze_hours` in tier1.
2. **Mind queue + reply queue + `pending_ask_user` actions** — Snooze: `valid_after` / priority for pending mind_queue; push reply_queue `due_at` on defer; snooze matching `pending_ask_user` heartbeat entries; skip mind_queue expiry on defer.
3. **Heartbeat snooze** — Add `topic_defer_until_iso` + fingerprint and two-tier guard (fingerprint first, `mind_queue_topic_overlaps_corpus` fallback) in `voice_dispatcher` when the mind_queue row is already `sent`.
4. **Defer window clearance** — In `post_conversation.py` or soul engine tick, detect organic topic coverage and clear stale `topic_defer_until_iso`.
5. **Ack** — Send a short Telegram confirmation on defer (same short-circuit pattern as dismiss ack).
6. **Tier1, tests, docs** — Operator defaults; unit tests covering defer/dismiss/answer tri-split and no-outreach-context guard; design/integration updates and `furoshiki design update` if the contract changes.
