chat notifications reach the phone, on the same rule as the bell

the browser bell and phone push are now two subscribers to one signal. both
ask shouldNotify(); neither owns the rule.

deliberately a second subscriber rather than a branch inside notify-delivery.
the two channels share only the question "does this account want to hear about
this" — the browser needs an open tab and sends the chat's real title over a
loopback socket on the owner's own tailnet, the phone needs no tab and must
send NO title, because that hop goes through apple. one function doing both
means an `if (isPush)` at every step, and the first thing to go wrong is a
title leaking because the branch was missed in one place.

two codes, officer/core:chat.turn-ended and chat.input-needed. the route is
written out with its scheme rather than derived from the target: the hub has
no chat screen — chat is Butler, a sibling app — so these deep-link OUT over
officer-chat://, and two of the suite's twenty-one schemes already break the
officer-<app> pattern (officer-videos, officer-readaloud). deriving that works
until it silently does not.

`thread` interpolates now, so `chat:{id}` groups per conversation. a static
thread forces a choice between "three pings from one chat collapse" and "two
busy chats stay apart", and the wrong answer is that every chat is the same
chat.

dropped officer/core:agent: chat.turn-ended supersedes it and its route
pointed at officer://chat/{id}, a hub screen that does not exist.

the banner cannot name which conversation. a chat's title is content and
content does not travel; it says what happened, and tapping opens the chat,
which is where the name lives.
This commit is contained in:
2026-08-19 22:00:17 +00:00
parent 72d327e360
commit 46d621c91b
5 changed files with 109 additions and 10 deletions
+17 -1
View File
@@ -101,6 +101,21 @@ index form to escape to — a primary key must be a constraint. `music_now_playi
re-adds `pk_music_now_playing` on every push. Two statements, silent, no prompt even with rows in the
table, data unaffected. Naming it explicitly does not help. Leave it.
**Four foreign keys are over 63 characters and churn the same way**, found 2026-08-19 while pushing
`chat_notification_prefs`. These are the case the paragraph above says to name explicitly, and none of
them is:
command_hq_session_tags_session_row_id_command_hq_sessions_id_fk 64
command_hq_watcher_fires_session_row_id_command_hq_sessions_id_fk 65
bitcoin_wallet_frozen_utxos_wallet_id_bitcoin_wallet_wallets_id_fk 66
bitcoin_wallet_chain_cache_wallet_id_bitcoin_wallet_wallets_id_fk 65
The tell is in the plan itself: the `DROP` names them at 63 characters and the `ADD` at their full
length, which is Postgres's truncation and drizzle's copy disagreeing. Eight statements per push, no
prompt, data unaffected — deferred rather than fixed (see `TODO.md`), because it is noise rather than
a fault. Two live in `officer_db/src/command-hq/schema.ts`; the other two are the bitcoin-wallet
plugin's, in its own repository.
**Rules that still apply:**
1. **Never answer "Yes, truncate the table"** if a prompt ever does appear. It destroys rows and fixes
@@ -111,7 +126,8 @@ table, data unaffected. Naming it explicitly does not help. Leave it.
branch it takes at a truncate prompt.
4. **Read the plan before applying it:**
`bunx drizzle-kit push --config=drizzle.config.ts --verbose` prints every statement first. Expect the
two `pk_music_now_playing` lines. Anything else is your change.
two `pk_music_now_playing` lines and the eight from the four over-long foreign keys above — ten in
all. Anything else is your change.
To test a schema change without touching `officer_dev`: `createdb officer_scratch`, then
`POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:5432/officer_scratch bunx drizzle-kit push