Skip to content

feat(server): push agent questions and results straight to phones through Expo - #230

Merged
jayleaton merged 1 commit into
mainfrom
feat/push-notifier-20261113
Nov 14, 2026
Merged

jayleaton merged 1 commit into
mainfrom
feat/push-notifier-20261113

Conversation

@jayleaton

@jayleaton jayleaton commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Sits on #229. The server pushes to registered phones straight through Expo's push service.

Rule (JJ, 2026-11-14): only top-level threads push, meaning no parent of any kind and not delegated from another machine. They push when they need the user (input or approval), finish, or fail. Child chats, subagents and side chats never push on their own; their questions go out as their top-level thread's.

streamDomainEvents ─filter─▶ worker(threadId)
   └ observation
        top-level thread : projectThreadAwarenessV2 phase (settled results already hidden)
        any child        : its own pending request only (never a result)
   └ decidePush(previous, next, { topLevel, snoozed })      // pure, apps/server/src/push/pushPolicy.ts
        new input / approval (new request id = new wait)
        completed / failed: top-level only, at most 2 min old; completed is quiet while snoozed
        first sighting after start: baseline only
   └ completed: hold 5 s and recheck, so a continuation stays quiet
   └ recipient thread = itself if top-level, else walk parentThreadId / lineage.parentThreadId
                        to the top-level ancestor (a delegatedFrom thread on the way stops it)
   └ send to listDeliverable() in batches of 100; DeviceNotRegistered in tickets or in
     15-minute receipts deletes that row

Payload: { to, title: <top-level thread title>, body: "<Needs your input|Needs approval|Finished|Failed> · <project>", sound, data: { environmentId, threadId, kind } }. No message content. T3CODE_EXPO_PUSH_URL overrides https://exp.host for testing. AgentAwarenessThread gains snoozedUntil and delegatedFrom. Desktop and web notifications (ThreadNotificationCoordinator) are not touched; a separate PR changes them.

Evidence

  • Before: no notifier exists, so a server sends no push.
  • After: tests:
    • pushPolicy.test.ts: 17 rows covering phase pairs × child, snoozed, stale, and repeat waits.

    • PushNotifier.test.ts: real store, fake Expo HTTP. It checks:

      • the exact payload;
      • ticket and receipt DeviceNotRegistered deletes the row;
      • the 5 s completion hold;
      • a subagent approval and a child-chat question both go out under the top-level thread's title and id;
      • a child chat's finish sends nothing.

      The child-chat step fails on the previous routing, which sent under the child's own title.

  • Runtime transcript (before the rule change; the top-level path is unchanged). Setup: vp run dev with T3CODE_EXPO_PUSH_URL pointing at a local fake Expo, one registration, and t3 thread start --provider devReplay. The fake Expo received:
    todo-list : {"to":"ExponentPushToken[proof-iphone]","title":"go","body":"Finished · push-proof","sound":"default","data":{"environmentId":"c7353c4e-…","threadId":"mcp:f54711f8-…","kind":"completed"}}
    question  : {"to":"ExponentPushToken[proof-iphone]","title":"ask me something","body":"Needs your input · push-proof","sound":"default","data":{"environmentId":"c7353c4e-…","threadId":"mcp:adad8caf-…","kind":"input"}}
    
  • vp run check:changed on this layer: all pass.

Merge Danger

Door: two-way. It adds no persisted state beyond #229's table, and a revert stops pushes.

Blast Radius: service. It reads one thread snapshot per relevant event and calls exp.host only when a push is due and a phone is registered. Thread and project titles pass through Expo and Apple. Expo's access-token requirement stays off by decision.

🤖 Generated with Claude Code

@github-actions github-actions Bot added size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Nov 13, 2026
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 5.0 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.8 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 21.5 KiB — 24.4 KiB ✅
Codex Live turn messages — 1 — 8 ✅
Claude Total thread wire — 5.0 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.8 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 21.9 KiB — 24.4 KiB ✅
Claude Live turn messages — 1 — 8 ✅

Baseline: unavailable · PR result: cc4ade5 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@jayleaton
jayleaton force-pushed the feat/push-registration-20261113 branch from 98f9e6b to 0d4dfa9 Compare November 14, 2026 03:41
@jayleaton
jayleaton force-pushed the feat/push-notifier-20261113 branch from 3d403e7 to d87cbe9 Compare November 14, 2026 03:41
…ough Expo

A PushNotifier watches domain events, projects each thread with
projectThreadAwarenessV2, and decides pushes with a pure policy: only top-level threads (no parent
of any kind, not delegated) push; a child's question or approval goes out
as its top-level thread's, finishes and failures only from top-level threads, no finish while
snoozed, nothing on the first sighting, and a 5-second hold so a run's
continuation stays quiet. Payloads carry thread title, project title,
environment id, thread id and kind, never message content. Tokens Expo
reports as DeviceNotRegistered, in tickets or 15-minute receipts, are
deleted. T3CODE_EXPO_PUSH_URL points it at a fake for testing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton force-pushed the feat/push-notifier-20261113 branch from d87cbe9 to cc4ade5 Compare November 14, 2026 04:05
@jayleaton
jayleaton marked this pull request as ready for review November 14, 2026 04:21
@jayleaton
jayleaton changed the base branch from feat/push-registration-20261113 to main November 14, 2026 04:21
@jayleaton
jayleaton merged commit 552639d into main Nov 14, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant