Version: 7.40.4 (LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts)
What's broken
On Linux, POST /notify returns 200 {"status":"success","message":"Notification sent"} and no
notification is ever displayed. Nothing is logged about it either — the module reports success for a
channel it knows it cannot reach on this platform.
This is the second half of #1239, which you closed on 2026-05-14 after landing the platform guard.
Your own comment at the time called it out and deferred it:
(Linux notify-send equivalent and the WSL2 powershell-shim path you mentioned are worth doing
eventually but they're a feature, not a fix — separate scope from the ENOENT silencing.)
That was the right call for that issue: the guard traded a loud wrong failure (ENOENT flooding the
journal) for a quiet one, which is a strict improvement while the branch doesn't exist. Filing this
now because three separate threads asked for it to come back on the current release:
It still applies on 7.40.4, unchanged.
file:line
LIFEOS/PULSE/VoiceServer/voice.ts:486-489
async function showDesktopNotification(title: string, message: string): Promise<void> {
if (!voiceConfig.desktopNotifications) return
// osascript is macOS-only; on Linux/WSL2 the spawn ENOENT's and floods journal
if (process.platform !== "darwin") return
The relevant contrast is 100 lines up in the same file. resolveAudioPlayer() at
voice.ts:381-425 already does per-platform resolution for the audio path — afplay on darwin,
then ffplay → mpg123 → paplay → aplay on everything else, with a cached null when none is
found. The notification path never got the same treatment, so one of the two adjacent output
channels resolves per platform and the other hard-codes one.
Repro against a clean tree
Runs on Linux, needs only bun and git. No install, no config, no API key — TTS is switched off
so the only thing exercised is the notification path.
mkdir -p /tmp/lifeos-repro/home/.claude && cd /tmp/lifeos-repro
git clone --depth 1 --branch v7.40.4 --quiet https://github.com/danielmiessler/LifeOS.git clean
echo '{ "notifications": { "desktop": { "enabled": true } } }' > home/.claude/settings.json
cat > repro.ts <<'TS'
import { startVoice, handleVoiceRequest } from "./clean/LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts"
startVoice({ enabled: true })
const res = await handleVoiceRequest(new Request("http://localhost/notify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ title: "repro", message: "a desktop notification should be visible now", voice_enabled: false }),
}))
console.log("HTTP", res!.status, await res!.text())
TS
HOME=/tmp/lifeos-repro/home bun repro.ts
Negative control
Measured on Linux 7.0.11 (Pop!_OS), notify-send present at /usr/bin/notify-send, desktop
notifications explicitly enabled in settings.json:
{"level":"info","msg":"Voice: notification \"repro\" - \"a desktop notification should be visible now\"","voiceEnabled":false,...}
HTTP 200 {"status":"success","message":"Notification sent"}
Nothing appeared on screen. There is no warn line and no error line about the notification — the
only trace is the info line that says it is notifying. The same script on macOS displays the
notification, so the observable difference is entirely the platform branch.
That is the part worth fixing independently of how it gets fixed: a channel that cannot emit on this
platform reports success. If notify-send never lands, a one-line log("warn", ...) in that branch
would still be an improvement over a green 200.
Suggested fix
Same shape as resolveAudioPlayer(), one function down — resolve a notifier once, cache it, warn
once when there is none:
interface DesktopNotifier {
path: string
buildArgs: (title: string, message: string) => string[]
}
let resolvedNotifier: DesktopNotifier | null | undefined = undefined
function resolveDesktopNotifier(): DesktopNotifier | null {
if (resolvedNotifier !== undefined) return resolvedNotifier
if (process.platform === "darwin") {
const path = Bun.which("osascript")
resolvedNotifier = path
? {
path,
buildArgs: (title, message) => [
"-e",
`display notification "${escapeForAppleScript(message)}" with title "${escapeForAppleScript(title)}" sound name ""`,
],
}
: null
return resolvedNotifier
}
const notifySend = Bun.which("notify-send")
resolvedNotifier = notifySend
? { path: notifySend, buildArgs: (title, message) => ["--app-name=LifeOS", "--", title, message] }
: null
return resolvedNotifier
}
and in showDesktopNotification, replace the early return with a lookup that says so when it fails:
const notifier = resolveDesktopNotifier()
if (!notifier) {
log("warn", `Voice: no desktop notifier found on ${process.platform} — notification will not be shown`)
return
}
Two things this buys beyond Linux coverage:
Bun.which on darwin too. Today the darwin path spawns /usr/bin/osascript by absolute
path, so a Mac without it at that exact location hits the original ENOENT rather than the guard.
Resolving both platforms the same way removes that.
- No shell string on the Linux side.
notify-send takes title and body as separate argv
entries, so there is nothing to escape — no escapeForAppleScript equivalent to get wrong. The
-- terminator keeps a message starting with - from being read as a flag.
Running this here since 2026-08-12. Happy to open the PR if you want it; also happy to send just the
log("warn") half if the notify-send branch is still out of scope, since that alone removes the
silent-success.
Separate, not in this issue
The same install also needed a non-ElevenLabs TTS engine, and the gate at voice.ts:531 checks
moduleConfig.elevenlabs_api_key directly rather than "is any engine available". That is a
provider-architecture question, it is what discussion #834 was about, and it does not belong bolted
onto a two-function platform fix. If a fresh version of that discussion is useful I will open it on
its own.
Version: 7.40.4 (
LifeOS/install/LIFEOS/PULSE/VoiceServer/voice.ts)What's broken
On Linux,
POST /notifyreturns200 {"status":"success","message":"Notification sent"}and nonotification is ever displayed. Nothing is logged about it either — the module reports success for a
channel it knows it cannot reach on this platform.
This is the second half of #1239, which you closed on 2026-05-14 after landing the platform guard.
Your own comment at the time called it out and deferred it:
That was the right call for that issue: the guard traded a loud wrong failure (ENOENT flooding the
journal) for a quiet one, which is a strict improvement while the branch doesn't exist. Filing this
now because three separate threads asked for it to come back on the current release:
once the skill-based version is out, reopen or file a fresh one and we'll jump on it."
concern still applies against the v5 voice layer, please open a fresh PR against the new release."
issue and we'll pick it up there."
It still applies on 7.40.4, unchanged.
file:line
LIFEOS/PULSE/VoiceServer/voice.ts:486-489The relevant contrast is 100 lines up in the same file.
resolveAudioPlayer()atvoice.ts:381-425already does per-platform resolution for the audio path —afplayon darwin,then
ffplay→mpg123→paplay→aplayon everything else, with a cachednullwhen none isfound. The notification path never got the same treatment, so one of the two adjacent output
channels resolves per platform and the other hard-codes one.
Repro against a clean tree
Runs on Linux, needs only
bunandgit. No install, no config, no API key — TTS is switched offso the only thing exercised is the notification path.
Negative control
Measured on Linux 7.0.11 (Pop!_OS),
notify-sendpresent at/usr/bin/notify-send, desktopnotifications explicitly enabled in
settings.json:Nothing appeared on screen. There is no
warnline and noerrorline about the notification — theonly trace is the
infoline that says it is notifying. The same script on macOS displays thenotification, so the observable difference is entirely the platform branch.
That is the part worth fixing independently of how it gets fixed: a channel that cannot emit on this
platform reports success. If
notify-sendnever lands, a one-linelog("warn", ...)in that branchwould still be an improvement over a green 200.
Suggested fix
Same shape as
resolveAudioPlayer(), one function down — resolve a notifier once, cache it, warnonce when there is none:
and in
showDesktopNotification, replace the early return with a lookup that says so when it fails:Two things this buys beyond Linux coverage:
Bun.whichon darwin too. Today the darwin path spawns/usr/bin/osascriptby absolutepath, so a Mac without it at that exact location hits the original ENOENT rather than the guard.
Resolving both platforms the same way removes that.
notify-sendtakes title and body as separate argventries, so there is nothing to escape — no
escapeForAppleScriptequivalent to get wrong. The--terminator keeps a message starting with-from being read as a flag.Running this here since 2026-08-12. Happy to open the PR if you want it; also happy to send just the
log("warn")half if thenotify-sendbranch is still out of scope, since that alone removes thesilent-success.
Separate, not in this issue
The same install also needed a non-ElevenLabs TTS engine, and the gate at
voice.ts:531checksmoduleConfig.elevenlabs_api_keydirectly rather than "is any engine available". That is aprovider-architecture question, it is what discussion #834 was about, and it does not belong bolted
onto a two-function platform fix. If a fresh version of that discussion is useful I will open it on
its own.