mailbox notify: name the protocol, because docker exec hides the terminal

Auto-detection is useless in the deployment this ships in, measured on tor-ms22.
Terminal identity lives in env vars set by the emulator (KITTY_WINDOW_ID,
TERM_PROGRAM) and docker exec does not forward them — pi inside the container
sees only TERM=xterm-256color no matter what is rendering it. So the =desktop
detection can never see Kitty from in there and always falls through to OSC 777,
which Kitty does not implement. Result: on a containerised client the ping
silently does nothing, which is the worst available failure for a feature whose
entire purpose is to break a silence.

Adds MEMPALACE_MAILBOX_NOTIFY=kitty and =osc777 to force one protocol and skip
detection. =desktop keeps the notify.ts-style autodetect for the non-container
case, unset still means in-TUI only, 0/off still silent. The Windows toast branch
is dropped from the comment rather than the code path it never had here: it needs
powershell.exe on PATH, which is a WSL fact, not a Linux-container one.
This commit is contained in:
2026-08-27 12:39:16 +02:00
parent a92c75d070
commit e91766286e
+20 -3
View File
@@ -1183,20 +1183,37 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
// emulator on the human's machine. Opt-in because writing raw
// escapes to stdout is a behaviour change on shared machines,
// not because it is unreliable.
// =kitty =osc777 force one protocol, skipping detection entirely.
// =0 silent, for anyone who wants the mailbox without pings.
//
// WHY FORCING EXISTS — measured on tor-ms22 2026-08-26, and it invalidates
// detection in exactly the deployment this ships in. Terminal identity lives in
// env vars set by the emulator (KITTY_WINDOW_ID, TERM_PROGRAM) and `docker exec`
// does NOT forward them: inside the container pi sees only TERM=xterm-256color
// no matter what is rendering it. So detection can never see Kitty from in here
// and always falls through to OSC 777, which Kitty does not implement — on a
// containerised client the notification would silently do nothing, the worst
// possible failure for a feature whose whole job is to break a silence. Naming
// the protocol ends the guessing.
//
// Fired only when something is actually due, i.e. the same condition as the
// delivery itself: a notification that fires on an empty poll would teach its
// reader to ignore it, which is the failure this whole feature exists to undo.
const mailboxNotifyMode = (process.env.MEMPALACE_MAILBOX_NOTIFY ?? "").trim().toLowerCase();
const mailboxNotifyEnabled = mailboxNotifyMode !== "0" && mailboxNotifyMode !== "off";
const mailboxNotifyDesktop = mailboxNotifyMode === "desktop";
const mailboxNotifyTerminal =
mailboxNotifyMode === "desktop" ||
mailboxNotifyMode === "kitty" ||
mailboxNotifyMode === "osc777";
/** Terminal-native notification. Best-effort; never throws into a handler. */
const notifyTerminal = (title: string, body: string): void => {
try {
const esc = (s: string): string => s.replace(/[\x00-\x1f\x7f;]/g, " ");
if (process.env.KITTY_WINDOW_ID) {
if (
mailboxNotifyMode === "kitty" ||
(mailboxNotifyMode === "desktop" && !!process.env.KITTY_WINDOW_ID)
) {
process.stdout.write(`\x1b]99;i=1:d=0;${esc(title)}\x1b\\`);
process.stdout.write(`\x1b]99;i=1:p=body;${esc(body)}\x1b\\`);
return;
@@ -1257,7 +1274,7 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
} catch {
/* ignore: UI may be gone by the time the poll resolves */
}
if (mailboxNotifyDesktop) notifyTerminal("MemPalace mailbox", summary);
if (mailboxNotifyTerminal) notifyTerminal("MemPalace mailbox", summary);
}
} catch {
/* best-effort delivery: never break a turn over a mailbox read */