DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/phase-44-sse-and-proactive-behavior/phase-44-cr-sse-and-proactive-behavior-v0_1.md

Phase 44 — Change Request — SSE + Proactive Behavior

CR identifier. CR-2026-058 Version. 0.1 Date. 2026-05-06 Status. Approved for execution. Author. Claude.ai (CR drafting layer). Operator: Marvin Percival. Executing agent. Claude Code (CC) on DUNIN7-M4. Depends on. Phase 43 complete (tag phase-43-personal-memory-contribution at 0869c7d). Phase 42 complete (classify → route → respond pipeline, conversation turns). Phase 41 complete (personal engagement, companion naming). Phase 33 complete (engagement activity observability). Informed by. Phase 44 scoping note v0.1 (eight decisions settled). Personal memory investigation v0.1 (§§3, 6–7 — SSE, trigger evaluator, proactive delivery). Companion as agent investigation v0.1 (§§3–4, §7 — proactive triggers, capability tiers). Companion expertise note v0.1 (four founding principles). Product identity standing note v0.1. Phase 42 CR (pipeline architecture, prompt assembly). Phase 43 CR (personal memory loader, cross-promotion instruction, prompt assembly order).


1. Purpose

Phase 44 adds the other direction. Phases 41–43 built the reactive Companion: the user speaks, the Companion classifies intent, routes to engine operations, responds in its voice, and reads/writes/retracts personal memory. Phase 44 makes the Companion proactive — it notices things that need the Operator's attention and surfaces them through a push channel.

Three capabilities ship:

  1. SSE infrastructure. A Server-Sent Events endpoint delivers typed push events to authenticated clients. The Companion can now initiate communication without waiting for the Operator to speak.
  1. Trigger evaluator. A background process scans personal memory for approaching dates and engagement activity for stale items (time-based triggers). An event listener hooks into engine event emission points for render completions and shape confirmations (event-based triggers). Both paths create persistent notifications and push them through SSE.
  1. Observation pathway. The Companion gains a prompt-level instruction to notice recurring themes and patterns across conversations. When a pattern is clear, the Companion offers to remember it — routing through the existing remember_about_me flow from Phase 43.

Additionally, this is the first frontend work since Phase 36. A notification badge and notification list/drawer appear on the dashboard. The Operator can see, dismiss, and globally mute proactive messages.

Full-stack phase. One new migration. Two checkpoints (substrate then frontend).


2. Scoping decisions adopted

All eight decisions from the scoping note v0.1 are adopted without modification.

| Decision | Summary | |----------|---------| | S1 | Push channel: SSE. Unidirectional server→client over HTTP. Converse endpoint stays for user-initiated conversation; SSE carries Companion-initiated messages. WebSocket rejected (overcapacity). Push notifications (APNs/FCM) deferred. | | S2 | Two trigger types: time-based (date scanning in personal memory, stale engagement activity) and event-based (render completed, shape confirmed, assertion committed by contributor). Insight-based triggers deferred (LLM-expensive, O(engagements²)). | | S3 | Proactive message delivery: separate notification surface (badge + list/drawer on dashboard). Not injected into conversation stream. Separate surface respects Operator attention. | | S4 | New companion_notifications table for operational state. Rejected: storing notifications as assertions in personal engagement (operational noise ≠ personal memory). | | S5 | Observation pathway ships. Prompt-level addition — Companion notices recurring themes, offers to remember via existing remember_about_me flow. Response-time (not background), no SSE dependency. | | S6 | User controls: global quiet mode. Single toggle stored as personal memory preference assertion. Category-level muting and availability windows deferred. Snooze has substrate support (snoozed_until column) but no frontend UI. | | S7 | Frontend work ships: SSE connection management, notification badge, notification list/drawer, dismiss action, quiet mode indicator. No conversation-injection UI. No snooze UI. | | S8 | Trigger evaluator: dual architecture. Periodic scanner (time-based, background task on interval) + event listener (event-based, synchronous with engine events). Both create companion_notifications rows and push SSE events. |


3. Pre-flight and baseline

3.1 Baseline

3.2 Pre-flight checklist

Step 0 confirms:

  1. Converse endpoint exists at POST /operator/converse. [CC verifies: exact file, function name, that the classify → route → respond pipeline is intact.]
  2. Prompt assembly function exists in src/loomworks/orchestration/prompt.py. [CC verifies: assemble_prompt signature, component structure (persona + engagement context + personal memory + cross-promotion instruction + intent instruction), assembly order.]
  3. Observation instruction does not yet exist in prompt assembly. [CC verifies: no observation component in prompt assembly — this is what Phase 44 adds.]
  4. Personal memory loader exists (load_personal_memory). [CC verifies: file, signature, that it loads committed assertions from the personal engagement.]
  5. remember_about_me intent and router handler exist (Phase 43). [CC verifies: classifier prompt includes remember_about_me, router dispatches it, creates held assertion in personal engagement.]
  6. Cross-promotion instruction exists in prompt assembly (Phase 43). [CC verifies: text content, position in assembly order — observation instruction inserts after it.]
  7. BackgroundAgentRunner exists in src/loomworks/agents/runner.py. [CC verifies: class, run method signature, FastAPI lifespan integration (startup/shutdown).]
  8. Engagement activity data is queryable (Phase 33). [CC verifies: the function or endpoint that returns engagement activity timestamps — activity summary or last_activity_at equivalent. File, signature.]
  9. Engine event emission points for render production and shape confirmation. [CC verifies: the functions where renders reach produced state and shapes reach confirmed state — files and function names. Identify the insertion point for event listener hooks.]
  10. Assertion commit event emission point. [CC verifies: the function where assertions transition from held to committed — file, function name. Identify how to detect "committed by another contributor" (actor differs from engagement Operator).]
  11. Person model has personal_engagement_id FK (Phase 41) and companion_name (Phase 41). [CC verifies: column types.]
  12. Vocabulary wall test exists (Phase 40). [CC verifies: file, how new schemas are registered, that it catches engine terms in response schemas.]
  13. FastAPI StreamingResponse is available. [CC verifies: import path, text/event-stream content type support.]
  14. Person authentication dependency exists in orchestration routers. [CC verifies: the dependency that extracts the authenticated person from the request — used to scope the SSE stream.]
  15. Dashboard endpoints exist (Phase 39: active, needs_you, recent). [CC verifies: router file, that the notification badge can coexist alongside existing dashboard endpoints.]
  16. Frontend auth context exists. [CC verifies: the auth provider/hook that provides the authenticated person's token — needed for SSE connection authentication.]
  17. Frontend dashboard page exists. [CC verifies: file, component structure, where the notification badge and drawer attach.]
  18. No existing companion_notifications table or model. [CC verifies: no migration, no model file for this table.]

Pre-flight divergence stops execution at Step 0 and drives a CR amendment.

3.3 CR archival

Archive this CR to docs/phase-crs/phase-44-cr-sse-and-proactive-behavior-v0_1.md at Step 0 before Step 1 begins.


4. Construction decisions this CR closes

D1 — Migration 0060: companion_notifications table

A new table storing the operational state of proactive messages.

Table design (CC determines exact column names and types within this schema):

Indexes:

[CC determines: whether trigger_type is a PostgreSQL enum (like actor_kind) or a check-constrained string column. Enum is preferred for type safety if the list is stable.]

D2 — SSE endpoint

A new endpoint that returns an SSE stream of typed events.

Endpoint: GET /operator/notifications/stream

Authentication: same dependency as other orchestration endpoints. The stream is person-scoped — only events for the authenticated person are delivered.

Response: StreamingResponse with content-type: text/event-stream. The endpoint holds the connection open and pushes events as they occur.

Event format (SSE standard):


event: notification_created
data: {"id": "...", "trigger_type": "time_based", "title": "...", "body": "...", "engagement_id": "...", "created_at": "..."}

event: notification_updated
data: {"id": "...", "status": "seen"}

The SSE endpoint reads from an in-process event bus. When the trigger evaluator or event listener creates a notification, it pushes to the bus. Connected SSE clients receive the event. If no client is connected, the notification is still persisted in the table — the client catches up on reconnection by querying recent unseen notifications via the REST endpoint (D3).

[CC determines: event bus implementation — asyncio.Queue per connection, a lightweight pub/sub within the process, or an alternative. The requirements are: (a) notifications reach connected clients in real time, (b) notifications persist for clients that reconnect, (c) the bus is scoped per person (no cross-person leakage), (d) the bus is cleaned up when the SSE connection closes.]

Heartbeat: The endpoint sends a comment line (: heartbeat) on a regular interval (e.g., every 30 seconds) to keep the connection alive and detect stale connections.

[CC determines: heartbeat interval and stale-connection cleanup strategy.]

D3 — Notification REST endpoints

Alongside the SSE stream, REST endpoints for notification management:

GET /operator/notifications — list recent notifications for the authenticated person. Query params: status filter (optional), pagination (limit/cursor). Returns notifications in Operator vocabulary. Default: unseen + pending notifications, ordered by created_at DESC.

PATCH /operator/notifications/{id}/seen — mark a notification as seen. Sets status = seen, seen_at = now(). Returns the updated notification.

PATCH /operator/notifications/{id}/dismiss — mark a notification as dismissed. Sets status = dismissed, dismissed_at = now(). Returns the updated notification.

All three endpoints use the vocabulary wall — no engine terms in request/response schemas.

Response schema:


class NotificationResponse(BaseModel):
    id: UUID
    trigger_type: str  # "time_based" | "event_based" | "observation"
    title: str
    body: str
    engagement_id: UUID | None
    engagement_name: str | None  # resolved for display
    status: str  # "pending" | "delivered" | "seen" | "dismissed"
    created_at: datetime
    delivered_at: datetime | None
    seen_at: datetime | None

class NotificationListResponse(BaseModel):
    items: list[NotificationResponse]
    total_count: int
    next_cursor: str | None = None

[CC determines: whether engagement_name is joined at query time or resolved separately. Joining is simpler; separate resolution is more cache-friendly.]

D4 — Trigger evaluator: periodic scanner

A background task registered at application startup. Runs on a configurable interval (default 5 minutes). Each cycle:

  1. Connection check. Load all persons with active SSE connections. [CC determines: whether to also evaluate for disconnected persons to ensure notifications are ready on reconnect. Evaluating only connected persons is an optimization; evaluating all persons ensures notifications accumulate and are ready on reconnect.]
  1. Quiet mode check. For each person, query personal memory for the quiet mode preference assertion. If quiet mode is enabled, skip this person entirely.
  1. Time-based scan. Load the person's personal memory assertions (committed assertions from personal engagement). Identify content containing date-associated information (birthdays, deadlines, recurring events). Compare against current time. If a threshold is met (e.g., 3 days before, 1 hour before, day-of), check the companion_notifications table for deduplication — has this trigger already fired for this trigger_ref (assertion ID) within the relevant time window? If not already surfaced, create a notification row and push to the SSE bus.
  1. Concern-based scan (time-based variant). Query engagement activity data (Phase 33 observability). For each engagement the person is a member of (excluding personal engagement), identify engagements with no activity in N days (configurable threshold, default 14 days). Check deduplication. Create notification if warranted. Example: "You haven't looked at the puppy breeding project in two weeks."

Date extraction from personal memory assertions: The assertion content is natural language ("My birthday is March 15th"). [CC determines: extraction approach. Options: (a) regex patterns for common date formats applied during the scan cycle, (b) LLM-assisted extraction during the remember_about_me commit flow (storing extracted dates as structured metadata alongside the assertion content — this is more robust but adds complexity to Phase 43's commit path and requires modifying assertion creation), (c) a hybrid where the scanner applies regex first and falls back to a lightweight heuristic. The requirement is that common date formats (month/day, month day, ISO dates, relative dates like "every Tuesday") are detectable.]

Background task registration: Follows the BackgroundAgentRunner pattern (Phase 3). The trigger evaluator is registered in the FastAPI lifespan startup path and cancelled on shutdown. [CC determines: whether this reuses BackgroundAgentRunner directly or is a separate periodic-task abstraction. The evaluator is different from agent work — it's a scheduled scan, not a one-shot task. A simple asyncio.create_task with a sleep loop may be more appropriate than the runner pattern.]

D5 — Event listener

Hooks into engine event emission points. When specific engine events fire, the listener creates notifications.

Monitored events:

Hook mechanism: [CC determines: implementation — decorator on existing engine functions, an event dispatch table, or explicit calls at each emission point. The requirement is that new engine events can be added to the listener without modifying core engine code. A lightweight event dispatch table (registry of callbacks keyed by event type) is the cleanest extension point.]

Notification creation: The listener creates a companion_notifications row with trigger_type = "event_based", trigger_ref set to the relevant engine event identifier (render event ID, shape event ID, assertion ID), and pushes to the SSE bus. The notification body is generated in Operator vocabulary — the listener translates engine events through the vocabulary wall.

Target person resolution: For render and shape events, the notification target is the engagement's Operator. For assertion-committed events, the target is every member of the engagement except the person who committed the assertion. [CC determines: how to resolve engagement membership — the contributor registry from Phase 3, or a direct query on the engagement's contributor list.]

D6 — Observation pathway: prompt addition

The system prompt gains an observation instruction component (~150 tokens), inserted after the cross-promotion instruction in the prompt assembly order:


persona (~500) → engagement context (~2,000) → personal memory (~500) →
cross-promotion instruction (~100) → observation instruction (~150) →
intent instruction (~200) → conversation history (variable)

Instruction content: tells the Companion to notice recurring themes, domain-specific preferences, and working patterns across conversations. When a pattern is clear (appearing in 3+ conversations or stated repeatedly within a conversation), the Companion offers to remember it. The offer routes through the existing remember_about_me flow — the Companion's offer is a conversational proposal, the Operator confirms, and the assertion commits to personal memory through the Phase 43 held-with-conversational-commit pattern.

Implementation: a versioned text asset alongside the existing persona, intent instruction, and cross-promotion instruction templates. Loaded by the prompt assembly function. The observation instruction is always included (not gated by engagement scope or intent type).

[CC determines: the text asset storage location and naming convention, consistent with the Phase 42/43 text assets for persona, classifier prompt, and intent instructions.]

D7 — Quiet mode

Storage: a committed assertion in the personal engagement with recognizable content (e.g., "Quiet mode: enabled"). The trigger evaluator queries personal memory for this assertion before evaluating any triggers.

Toggle mechanism: the Operator tells the Companion "mute notifications" or "turn off proactive messages." [CC determines: whether this warrants a new intent (toggle_quiet_mode) or is handled within general_conversation / remember_about_me guidance. A dedicated intent gives cleaner routing; handling within remember_about_me reuses existing infrastructure. The requirement is that quiet mode toggling works conversationally without a new frontend control.]

Evaluator behavior: if quiet mode is enabled, the periodic scanner skips the person entirely. Event-based notifications are still created (they're synchronous with engine events and should be available when quiet mode is turned off) but marked with status = "pending" without SSE push. [CC determines: whether to suppress event-based notification creation entirely during quiet mode or create-but-hold. Creating-but-holding is more conservative — the Operator turns off quiet mode and sees what they missed.]

D8 — Frontend: SSE connection management

The authenticated client opens an SSE connection to GET /operator/notifications/stream on load.

Connection lifecycle:

State management: incoming SSE events update local state (notification count, notification list). [CC determines: state management approach — React context, zustand, or local component state with prop drilling. The notification count needs to be accessible from the dashboard chrome (badge) and the notification drawer.]

Initial load: on connection, fetch unseen notifications via GET /operator/notifications to populate the badge count and list. SSE events after initial load are additive.

D9 — Frontend: notification badge

A badge/indicator on the dashboard chrome showing the count of unseen notifications (status = pending or delivered, not seen or dismissed).

Placement: in the dashboard header, consistent with the existing dashboard layout. [CC determines: exact placement — near the engagement list, near the companion name, or as a standalone icon.]

Behavior:

D10 — Frontend: notification list/drawer

A surface showing recent notifications, accessible from the notification badge.

Layout: [CC determines: drawer (slides in from right), dropdown (appears below badge), or panel (inline). The requirement is minimal: a list of notifications with dismiss capability. Drawer is the most conventional choice for a notification surface.]

Notification card content:

Interactions:

Quiet mode indicator: if quiet mode is enabled (detectable from personal memory or a dedicated flag), the notification surface shows a muted state — a visual indicator that proactive messages are suppressed. No toggle UI in Phase 44; quiet mode is controlled through conversation.

[CC determines: how to detect quiet mode from the frontend. Options: (a) a dedicated GET /operator/notifications/preferences endpoint, (b) a field on the notification list response, (c) inferred from the absence of new notifications. Option (b) is simplest — include a quiet_mode boolean in the notification list response.]


5. Migration

One new migration: Alembic 0060.

Creates the companion_notifications table per D1. Columns, constraints, and indexes per D1.

No data backfill. The table starts empty.

[CC determines: whether trigger_type and status are PostgreSQL enums or check-constrained string columns. Enum is preferred if the value set is stable.]


6. Module changes

Phase 44 modifies existing files and creates new files.

| File | Change | |------|--------| | New: src/loomworks/notifications/models.py | CompanionNotification SQLAlchemy model per D1. | | New: src/loomworks/notifications/schemas.py | NotificationResponse, NotificationListResponse per D3. | | New: src/loomworks/notifications/service.py | Notification CRUD (create, list, mark seen, dismiss), deduplication check, quiet mode check. | | New: src/loomworks/notifications/bus.py | In-process event bus for SSE delivery per D2. Person-scoped pub/sub. | | New: src/loomworks/notifications/evaluator.py | Periodic scanner (time-based + concern-based triggers) per D4. Background task with sleep loop. | | New: src/loomworks/notifications/listener.py | Event listener hooks per D5. Translates engine events to notifications. | | New: src/loomworks/orchestration/routers/notifications.py | SSE endpoint (D2) and REST endpoints (D3). | | src/loomworks/orchestration/prompt.py | Add observation instruction component (D6). Extend assemble_prompt with observation instruction loading and insertion. | | New: observation instruction text asset | Versioned text template per D6, alongside persona and intent instruction assets. | | Classifier prompt text asset | [CC determines: whether quiet mode toggle warrants a new intent or guidance within existing intents (D7).] | | Router (src/loomworks/orchestration/router.py) | [CC determines: if a new toggle_quiet_mode intent is added, register handler (D7).] | | Engine functions (render production, shape confirmation, assertion commit) | Add event listener hook calls per D5. [CC determines: exact files and insertion points.] | | FastAPI lifespan | Register trigger evaluator background task at startup; cancel on shutdown. Register event bus. | | Alembic | Migration 0060 per D1. |

Frontend changes:

| File | Change | |------|--------| | New: SSE connection hook/provider | Manages SSE connection lifecycle per D8. | | New: Notification state context/store | Holds notification count and list per D8. | | New: Notification badge component | Badge in dashboard chrome per D9. | | New: Notification drawer/list component | Notification list with dismiss per D10. | | Dashboard page | Integrate notification badge and drawer. | | API client | Add notification REST endpoint calls (list, seen, dismiss). |

[CC determines: exact file paths and component names following existing frontend conventions.]


7. Test suite

Substrate tests

SSE endpoint (D2):

  1. SSE endpoint requires authentication — unauthenticated request returns 401.
  2. SSE endpoint delivers notification_created events to connected client.
  3. SSE endpoint is person-scoped — person A does not receive person B's notifications.
  4. SSE heartbeat keeps connection alive.
  5. Client disconnection cleans up the event bus subscription.

Notification CRUD (D3):

  1. GET /operator/notifications returns notifications for the authenticated person, ordered by created_at DESC.
  2. GET /operator/notifications respects status filter (e.g., only unseen).
  3. GET /operator/notifications does not return notifications belonging to other persons.
  4. PATCH /operator/notifications/{id}/seen sets status = seen and seen_at.
  5. PATCH /operator/notifications/{id}/dismiss sets status = dismissed and dismissed_at.
  6. Marking a notification belonging to another person returns 404.
  7. Notification responses use Operator vocabulary — no engine terms.

Trigger evaluator — time-based (D4):

  1. Scanner identifies a personal memory assertion with a date approaching within threshold and creates a notification.
  2. Scanner does not create a duplicate notification for a trigger that already fired (deduplication by trigger_ref).
  3. Scanner does not fire for persons with quiet mode enabled.
  4. Scanner handles persons with no personal memory assertions gracefully (empty scan, no error).

Trigger evaluator — concern-based (D4):

  1. Scanner identifies an engagement with no activity in N days and creates a notification.
  2. Scanner excludes the personal engagement from stale-engagement detection.
  3. Concern-based deduplication: does not re-fire for the same stale engagement within a time window.

Event listener (D5):

  1. Render reaching produced state creates an event-based notification for the engagement Operator.
  2. Shape reaching confirmed state creates an event-based notification for the engagement Operator.
  3. Assertion committed by another contributor creates a notification for other engagement members.
  4. Assertion committed by the engagement Operator does not create a self-notification.
  5. Event listener notifications use Operator vocabulary (vocabulary wall compliance).

Observation instruction (D6):

  1. Prompt assembly includes observation instruction component at the correct position (after cross-promotion, before intent instruction).
  2. Observation instruction is a versioned text asset loaded from the same location as other prompt templates.

Quiet mode (D7):

  1. Quiet mode preference assertion suppresses all time-based and concern-based notifications.
  2. Quiet mode does not suppress event-based notification creation (but suppresses SSE push). [CC adjusts: based on D7 determination about event-based behavior during quiet mode.]
  3. Toggling quiet mode off re-enables trigger evaluation on next scan cycle.
  4. [CC adds: quiet mode toggle test if a new intent is created.]

Event bus (D2):

  1. Event bus delivers to the correct person's subscriber (no cross-person leakage).
  2. Event bus handles multiple concurrent subscribers for the same person (multiple browser tabs).
  3. Event bus cleans up subscriber on disconnect.

Notification model:

  1. companion_notifications table constraints: person_id not nullable, trigger_type valid values only, status valid values only.
  2. Index on (person_id, status, created_at DESC) exists.
  3. snoozed_until accepts nullable timestamp (substrate support, no frontend).

Backward compatibility:

  1. All Phase 43 tests still pass (personal memory, cross-promotion, classifier).
  2. All Phase 42 tests still pass (classify → route → respond pipeline).
  3. Vocabulary wall test passes with all new notification schemas.
  4. Converse endpoint unaffected — observation instruction is additive to prompt assembly.

Expected substrate test count: ~35–45 new tests.

Frontend tests

SSE connection (D8):

  1. SSE connection hook opens connection with auth token.
  2. SSE connection hook handles reconnection on drop.
  3. Incoming notification_created event updates notification state.

Notification badge (D9):

  1. Badge shows count of unseen notifications.
  2. Badge is hidden when count is zero.
  3. Badge count updates when a new SSE event arrives.
  4. Badge count decrements when a notification is marked seen or dismissed.

Notification drawer (D10):

  1. Drawer renders notification cards with title, body, relative timestamp.
  2. Dismiss button removes notification from list and calls dismiss endpoint.
  3. Engagement link renders when engagement_id is present.
  4. Quiet mode indicator renders when quiet mode is active.

Expected frontend test count: ~10–15 new vitest tests.


8. Order of operations

| Step | What | Auto/checkpoint | |------|------|-----------------| | 0 | Pre-flight: verify baseline (substrate 1,624 + 26 skipped, frontend 246 vitest, Alembic 0059). Verify all 18 pre-flight items. Archive this CR to docs/phase-crs/. | Auto | | 1 | Migration 0060: companion_notifications table (D1). Verify migration runs, table exists, indexes created. | Auto | | 2 | Event bus (D2 bus portion). In-process pub/sub for person-scoped event delivery. Tests §7 items 31–33. | Auto | | 3 | Notification service (D3 service portion). CRUD functions: create notification, list by person, mark seen, dismiss, deduplication check, quiet mode check. Tests §7 items 6–12, 34–36. | Auto | | 4 | SSE endpoint (D2 endpoint portion). GET /operator/notifications/stream with auth, heartbeat, event delivery. REST endpoints: GET /operator/notifications, PATCH .../seen, PATCH .../dismiss. Tests §7 items 1–5. | Auto | | 5 | Trigger evaluator: periodic scanner (D4). Time-based scan (date extraction from personal memory), concern-based scan (stale engagement activity). Background task registration in lifespan. Tests §7 items 13–19. | Auto | | 6 | Event listener (D5). Hooks into render production, shape confirmation, assertion commit. Notification creation with Operator vocabulary. Tests §7 items 20–24. | Auto | | 7 | Observation instruction (D6). Versioned text asset, prompt assembly integration. Tests §7 items 25–26. | Auto | | 8 | Quiet mode (D7). Preference assertion, evaluator integration, toggle mechanism. Tests §7 items 27–30. | Auto | | 9 | Backward compatibility and vocabulary wall. Tests §7 items 37–40. Full substrate test sweep. | Auto | | A | Checkpoint A — Operator confirms substrate work. All substrate tests pass. | Checkpoint | | 10 | Frontend: SSE connection hook/provider (D8). API client additions for notification endpoints. Tests §7 items 41–43. | Auto | | 11 | Frontend: notification badge (D9) and notification drawer (D10). Dashboard integration. Quiet mode indicator. Tests §7 items 44–51. | Auto | | 12 | Frontend verification: npm run lint && npx tsc --noEmit && npm run build && npm run test clean. | Auto | | B | Checkpoint B — Operator confirms. Tag both repos phase-44-sse-and-proactive-behavior. Implementation notes. | Checkpoint |


9. Acceptance gate

  1. SSE endpoint delivers typed events to authenticated clients.
  2. Notification REST endpoints (list, seen, dismiss) work through vocabulary wall.
  3. Trigger evaluator runs on interval, produces time-based notifications from personal memory date content.
  4. Trigger evaluator produces concern-based notifications from stale engagement activity.
  5. Event listener produces notifications on render completion and shape confirmation.
  6. Deduplication prevents re-firing of already-surfaced notifications.
  7. Quiet mode preference suppresses all proactive notifications.
  8. Observation instruction appears in prompt assembly at correct position.
  9. Frontend SSE connection opens on auth, reconnects on drop.
  10. Frontend notification badge shows unseen count.
  11. Frontend notification list/drawer renders notifications with dismiss action.
  12. Vocabulary wall passes with all new schemas.
  13. All substrate tests pass. All frontend tests pass (vitest, lint, tsc, build).

10. What this phase does not build


11. Post-CR state (expected)


12. Relationship to subsequent phases


13. Kickoff prompt for the Claude Code session


Read the Change Request document at the path I supply below. This is
CR-2026-058 v0.1, the Phase 44 Change Request. You are the executing
agent named in the CR.

CR path: ~/Downloads/phase-44-cr-sse-and-proactive-behavior-v0_1.md

Phase 44 adds proactive Companion behavior: SSE push channel, trigger
evaluator (time-based + event-based), notification surface, observation
pathway, quiet mode. Full-stack phase — first frontend work since
Phase 36. One new migration (companion_notifications table). Two
checkpoints: A after substrate, B after frontend.

Code baseline: tag phase-43-personal-memory-contribution (substrate
at 0869c7d). Substrate: 1,624 tests, 26 skipped, Alembic 0059.
Frontend: 246 vitest. eslint/tsc/build clean.

Run pre-flight (Step 0) per Section 3.2. Eighteen pre-flight items
covering substrate (items 1–15) and frontend (items 16–18).

Per Section 3.3: archive this CR to
docs/phase-crs/phase-44-cr-sse-and-proactive-behavior-v0_1.md in
the substrate repo at Step 0.

Per Section 8, twelve steps with two checkpoints. Steps 1–9
(substrate) then Checkpoint A. Steps 10–12 (frontend) then
Checkpoint B (final, tag both repos).

Pre-flight surprises stop at Step 0 and drive a CR amendment.

Implementation notes at Checkpoint B:
docs/phase-impl-notes/phase-44-implementation-notes-v0_1.md

DUNIN7 — Done In Seven LLC — Miami, Florida Phase 44 CR — v0.1 — 2026-05-06