Conversation
Observer-driven contextual nudges. Reads codec_observer's snapshot, checks
declarative patterns, posts at most one suggestion per pattern per cooldown
window. OFF by default — user opts in via PROACTIVE_OVERLAY_ENABLED env var
to avoid surprise notifications.
## What ships
`codec_proactive.py` (NEW, ~280 LOC):
- Pattern registry with 1 v1 pattern: `long_form_dwell` (≥30 min on
Notion/Docs/Substack/Medium/major news → "want me to summarize?")
- 3-gate kill model:
1. Global env: `PROACTIVE_OVERLAY_ENABLED=true|false` (default false)
2. Per-pattern killed forever: `~/.codec/proactive_state.json:killed_patterns`
3. Per-day dismissed: `~/.codec/proactive_state.json:dismissed_today`
- Rate limits:
- Per-pattern cooldown: 1 hour (configurable per Pattern)
- Global rate limit: 30 min between any two suggestions
- Atomic state writes (tmp+rename, mirrors Step 8/10 pattern)
- Public API: `check_for_proactive(snapshot, history)`, `acknowledge`,
`dismiss(pattern_id, scope)`, `list_patterns`, `is_enabled`,
`is_pattern_killed`, `is_pattern_dismissed_today`
## Integration
`codec_observer.poll()` extension:
- After Step 6 trigger evaluation, calls `check_for_proactive(snapshot, history)`
- If a pattern matches, posts via `codec_agent_messaging.post_message`
with `agent_id="proactive"` and `type=agent_question`
- Try/except wrapping so observer keeps running on any failure
(defensive — proactive is a side effect, not a hard dependency)
## PWA endpoints (`routes/agents.py` extended)
- `GET /api/proactive/patterns` — list registered patterns + state for settings UI
- `POST /api/proactive/acknowledge {pattern_id}` — user clicked Acknowledge
- `POST /api/proactive/dismiss {pattern_id, scope: "today"|"forever"}`
## Audit envelope
3 new schema:1 events + `PHASE35_PROACTIVE_EVENTS` frozenset:
- `proactive_suggestion_emitted` (info, extra: pattern_id, title_excerpt)
- `proactive_suggestion_acknowledged` (info, extra: pattern_id)
- `proactive_suggestion_dismissed` (info, extra: pattern_id, scope)
## Tests
12 new tests in `tests/test_proactive.py`:
- Audit constants
- Kill switch (default off, env-var on)
- Pattern matching (long_form_dwell match / short-dwell skip / non-long-form skip)
- Cooldowns (per-pattern + global rate limit)
- Dismiss today + forever
- Atomic state writes
- list_patterns
Full suite: 950 passed / 20 failed / 73 skipped (same 20/73 baseline,
+12 from proactive).
## Why opt-in default
Proactive nudges have a high blast radius for "annoying user". Defaulting
OFF means users explicitly enable via:
`pm2 stop codec-observer && PROACTIVE_OVERLAY_ENABLED=true pm2 start codec-observer`
or by editing ecosystem.config.js. Same pattern as ocr_enabled.
## Future patterns (Phase 4+)
Easy to add to `PATTERNS` list:
- multi_tab_research (3+ tabs same domain → consolidate notes?)
- heavy_editing (5+ edits in 10 min on same file → auto-checkpoint?)
- error_log_dwell (Console.app + recent error → debug?)
Each is just a (match_fn, suggestion_fn, cooldown) tuple. Tests are
template-based.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3.5 Step 13. The last big remaining feature from Phase 3 backlog. Observer-driven contextual nudges via the Step 6 trigger system pattern.
OFF by default. User opts in via
PROACTIVE_OVERLAY_ENABLED=trueenv var to avoid surprise notifications.Anchor example
User has been on
notion.so/my-research-docfor 31 minutes. CODEC's observer notices the dwell. Posts: "Want me to summarize? You've been on this Notion doc for over 30 minutes. I can pull a summary into your notes. [Acknowledge] [Dismiss today] [Disable forever]"If you click Acknowledge → audit emits
proactive_suggestion_acknowledged. Dismiss today → blocked until tomorrow UTC. Disable forever → pattern inkilled_patternspermanently.What ships
codec_proactive.pytests/test_proactive.pycodec_audit.pyPHASE35_PROACTIVE_EVENTSfrozensetcodec_observer.pypoll()extension: after Step 6 triggers, callscheck_for_proactive(). Defensive try/except so observer never breaksroutes/agents.pyGET /api/proactive/patterns,POST /api/proactive/acknowledge,POST /api/proactive/dismissPattern v1:
long_form_dwellMatches when ALL true:
notion.so/docs.google.com/substack.com/medium.com/nytimes.com/ft.com/economist.com/newyorker.comCooldown: 1 hour per pattern. Global rate limit: 30 min between any two suggestions (so cluster of patterns can't burst).
Easy to extend
PATTERNSlist with more:multi_tab_research(3+ tabs on same domain)heavy_editing(5+ edits in 10 min)error_log_dwell(Console.app foreground + recent error)State
~/.codec/proactive_state.json(atomic tmp+rename writes):{ "schema": 1, "last_fired_at": {"long_form_dwell": 1715000000.0}, "dismissed_today": {"long_form_dwell": "2026-05-03"}, "killed_patterns": [], "last_global_fire_at": 1715000000.0 }Audit emits
3 new schema:1 events:
proactive_suggestion_emitted(info, extra: pattern_id, title_excerpt)proactive_suggestion_acknowledged(info, extra: pattern_id)proactive_suggestion_dismissed(info, extra: pattern_id, scope)Test plan
tests/test_proactive.py→ 12 passedWhy opt-in default
Proactive nudges have a high blast radius for "annoying user". Defaulting OFF means users explicitly enable via env var or
pm2 set codec-observer:env.PROACTIVE_OVERLAY_ENABLED true. Same pattern as Phase 2 Step 5'socr_enabled(also defaulted to a safe fallback after the popup-storm incident).🤖 Generated with Claude Code