pi-smart-voice-notify 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2026-04-30
4
+
5
+ ### Added
6
+ - Detect agent turn failures reported at `agent_end` and send error notifications instead of completion alerts.
7
+
8
+ ### Changed
9
+ - Renamed bundled notification sound assets to stable lowercase filenames and migrate legacy bundled sound paths automatically.
10
+ - Updated `@mariozechner/pi-*` peer dependency ranges to `^0.70.6` and synchronized package lock metadata.
11
+
3
12
  ## [0.3.5] - 2026-04-27
4
13
 
5
14
  ### Fixed
package/README.md CHANGED
@@ -166,10 +166,10 @@ Forwarded permission watcher notifications use privacy-safe text, require the re
166
166
 
167
167
  | Option | Type | Default |
168
168
  |--------|------|---------|
169
- | `idleSoundFile` | string | `"assets/Soft-high-tech-notification-sound-effect.mp3"` |
170
- | `permissionSoundFile` | string | `"assets/Machine-alert-beep-sound-effect.mp3"` |
171
- | `questionSoundFile` | string | `"assets/Machine-alert-beep-sound-effect.mp3"` |
172
- | `errorSoundFile` | string | `"assets/Machine-alert-beep-sound-effect.mp3"` |
169
+ | `idleSoundFile` | string | `"assets/soft-notification.mp3"` |
170
+ | `permissionSoundFile` | string | `"assets/attention-alert.mp3"` |
171
+ | `questionSoundFile` | string | `"assets/attention-alert.mp3"` |
172
+ | `errorSoundFile` | string | `"assets/attention-alert.mp3"` |
173
173
 
174
174
  Paths can be absolute or relative to the extension directory.
175
175
 
@@ -94,10 +94,10 @@
94
94
  "openaiTtsSpeed": 1,
95
95
 
96
96
  "_comment_sounds": "Direct sound file fallbacks.",
97
- "idleSoundFile": "assets/Soft-high-tech-notification-sound-effect.mp3",
98
- "permissionSoundFile": "assets/Machine-alert-beep-sound-effect.mp3",
99
- "questionSoundFile": "assets/Machine-alert-beep-sound-effect.mp3",
100
- "errorSoundFile": "assets/Machine-alert-beep-sound-effect.mp3",
97
+ "idleSoundFile": "assets/soft-notification.mp3",
98
+ "permissionSoundFile": "assets/attention-alert.mp3",
99
+ "questionSoundFile": "assets/attention-alert.mp3",
100
+ "errorSoundFile": "assets/attention-alert.mp3",
101
101
 
102
102
  "_comment_sound_theme": "Sound theme settings.",
103
103
  "themePath": "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-smart-voice-notify",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "Windows-optimized smart voice, sound, and desktop notifications for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -61,8 +61,8 @@
61
61
  ]
62
62
  },
63
63
  "peerDependencies": {
64
- "@mariozechner/pi-coding-agent": "^0.70.2",
65
- "@mariozechner/pi-tui": "^0.70.2"
64
+ "@mariozechner/pi-coding-agent": "^0.70.6",
65
+ "@mariozechner/pi-tui": "^0.70.6"
66
66
  },
67
67
  "dependencies": {
68
68
  "node-notifier": "^10.0.1"
@@ -174,10 +174,10 @@ export const DEFAULT_CONFIG: VoiceNotifyConfig = {
174
174
  openaiTtsFormat: "mp3",
175
175
  openaiTtsSpeed: 1,
176
176
 
177
- idleSoundFile: "assets/Soft-high-tech-notification-sound-effect.mp3",
178
- permissionSoundFile: "assets/Machine-alert-beep-sound-effect.mp3",
179
- questionSoundFile: "assets/Machine-alert-beep-sound-effect.mp3",
180
- errorSoundFile: "assets/Machine-alert-beep-sound-effect.mp3",
177
+ idleSoundFile: "assets/soft-notification.mp3",
178
+ permissionSoundFile: "assets/attention-alert.mp3",
179
+ questionSoundFile: "assets/attention-alert.mp3",
180
+ errorSoundFile: "assets/attention-alert.mp3",
181
181
  themePath: "",
182
182
  themeName: "default",
183
183
  themesRootPath: "",
@@ -344,6 +344,34 @@ function stringOrDefault(value: unknown, fallback: string): string {
344
344
  return fallback;
345
345
  }
346
346
 
347
+ const LEGACY_BUNDLED_SOUND_FILES: Record<string, string> = {
348
+ "assets/machine-alert-beep-sound-effect.mp3": "assets/attention-alert.mp3",
349
+ "assets/soft-high-tech-notification-sound-effect.mp3": "assets/soft-notification.mp3",
350
+ };
351
+
352
+ function normalizeSoundFileLookup(value: string): string {
353
+ return value.replaceAll("\\", "/").replace(/^\.\//, "").toLowerCase();
354
+ }
355
+
356
+ function soundFileExists(value: string): boolean {
357
+ return existsSync(isAbsolute(value) ? value : join(CONFIG_DIR, value));
358
+ }
359
+
360
+ function normalizeSoundFile(value: unknown, fallback: string): string {
361
+ const selected = stringOrDefault(value, fallback);
362
+ const lookup = normalizeSoundFileLookup(selected);
363
+ const migrated = LEGACY_BUNDLED_SOUND_FILES[lookup];
364
+ if (migrated) {
365
+ return migrated;
366
+ }
367
+
368
+ if (lookup.startsWith("assets/") && !soundFileExists(selected) && soundFileExists(fallback)) {
369
+ return fallback;
370
+ }
371
+
372
+ return selected;
373
+ }
374
+
347
375
  function stringOrEmpty(value: unknown): string {
348
376
  if (typeof value === "string") {
349
377
  return value.trim();
@@ -746,13 +774,13 @@ export function normalizeConfig(raw: unknown): VoiceNotifyConfig {
746
774
  openaiTtsFormat: stringOrDefault(record.openaiTtsFormat, DEFAULT_CONFIG.openaiTtsFormat),
747
775
  openaiTtsSpeed: clampNumber(record.openaiTtsSpeed, DEFAULT_CONFIG.openaiTtsSpeed, 0.25, 4),
748
776
 
749
- idleSoundFile: stringOrDefault(record.idleSoundFile ?? record.idleSound, DEFAULT_CONFIG.idleSoundFile),
750
- permissionSoundFile: stringOrDefault(
777
+ idleSoundFile: normalizeSoundFile(record.idleSoundFile ?? record.idleSound, DEFAULT_CONFIG.idleSoundFile),
778
+ permissionSoundFile: normalizeSoundFile(
751
779
  record.permissionSoundFile ?? record.permissionSound,
752
780
  DEFAULT_CONFIG.permissionSoundFile,
753
781
  ),
754
- questionSoundFile: stringOrDefault(record.questionSoundFile ?? record.questionSound, DEFAULT_CONFIG.questionSoundFile),
755
- errorSoundFile: stringOrDefault(record.errorSoundFile ?? record.errorSound, DEFAULT_CONFIG.errorSoundFile),
782
+ questionSoundFile: normalizeSoundFile(record.questionSoundFile ?? record.questionSound, DEFAULT_CONFIG.questionSoundFile),
783
+ errorSoundFile: normalizeSoundFile(record.errorSoundFile ?? record.errorSound, DEFAULT_CONFIG.errorSoundFile),
756
784
  themePath: stringOrEmpty(record.themePath ?? record.soundThemeDir ?? record.themeDirectory),
757
785
  themeName: stringOrDefault(record.themeName, DEFAULT_CONFIG.themeName),
758
786
  themesRootPath: stringOrEmpty(record.themesRootPath ?? record.themesRootDirectory),
@@ -989,7 +1017,7 @@ export function isWindows(): boolean {
989
1017
 
990
1018
  export function resolveSoundFile(config: VoiceNotifyConfig, type: NotificationType): string | null {
991
1019
  const field = SOUND_FILE_FIELD[type];
992
- const value = config[field];
1020
+ const value = normalizeSoundFile(config[field], DEFAULT_CONFIG[field]);
993
1021
  if (!value.trim()) {
994
1022
  return null;
995
1023
  }
package/src/index.ts CHANGED
@@ -127,6 +127,52 @@ function classifyToolResult(
127
127
  return "error";
128
128
  }
129
129
 
130
+ type AgentEndStatus = "completed" | "error" | "aborted";
131
+
132
+ interface AgentEndOutcome {
133
+ status: AgentEndStatus;
134
+ reason?: string;
135
+ }
136
+
137
+ function readAgentEndOutcome(event: unknown): AgentEndOutcome {
138
+ const messages = toRecord(event).messages;
139
+ if (!Array.isArray(messages)) {
140
+ return { status: "completed" };
141
+ }
142
+
143
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
144
+ const message = toRecord(messages[index]);
145
+ if (message.role !== "assistant") {
146
+ continue;
147
+ }
148
+
149
+ const stopReason = normalizeOptionalString(message.stopReason);
150
+ const errorMessage = normalizeOptionalString(message.errorMessage);
151
+ if (stopReason === "error") {
152
+ return { status: "error", reason: errorMessage ?? extractTextContent(message.content) };
153
+ }
154
+ if (stopReason === "aborted") {
155
+ return { status: "aborted", reason: errorMessage };
156
+ }
157
+ if (errorMessage) {
158
+ return { status: "error", reason: errorMessage };
159
+ }
160
+
161
+ return { status: "completed" };
162
+ }
163
+
164
+ return { status: "completed" };
165
+ }
166
+
167
+ function formatAgentErrorNotification(reason: string | undefined): string {
168
+ const normalizedReason = reason?.replace(/\s+/g, " ").trim();
169
+ if (!normalizedReason) {
170
+ return "❌ Agent ended with an error. Check the latest output before continuing.";
171
+ }
172
+
173
+ return `❌ Agent ended with an error: ${normalizedReason.slice(0, 160)}`;
174
+ }
175
+
130
176
  function statusLine(config: VoiceNotifyConfig): string | undefined {
131
177
  if (!config.enabled) {
132
178
  return "voice:off";
@@ -1750,6 +1796,7 @@ export default function smartVoiceNotifyExtension(
1750
1796
  pendingPermissionToolCallIds.clear();
1751
1797
  processedToolResultToolCallIds.clear();
1752
1798
  resetPermissionBatch("agent_start");
1799
+ cancelReminderActivity("agent_start");
1753
1800
  logger.debug("agent.start", {});
1754
1801
  });
1755
1802
 
@@ -1803,8 +1850,30 @@ export default function smartVoiceNotifyExtension(
1803
1850
  });
1804
1851
  });
1805
1852
 
1806
- pi.on("agent_end", async (_event, ctx) => {
1853
+ pi.on("agent_end", async (event, ctx) => {
1807
1854
  activeSessionContext = ctx;
1855
+ const outcome = readAgentEndOutcome(event);
1856
+ if (outcome.status === "error") {
1857
+ hadErrorInTurn = true;
1858
+ logger.debug("agent.end.error_detected", {
1859
+ reason: outcome.reason,
1860
+ });
1861
+ if (config.enabled && config.enableErrorNotification) {
1862
+ triggerNotification("error", ctx, {
1863
+ customMessage: formatAgentErrorNotification(outcome.reason),
1864
+ reason: outcome.reason,
1865
+ });
1866
+ }
1867
+ return;
1868
+ }
1869
+ if (outcome.status === "aborted") {
1870
+ hadErrorInTurn = true;
1871
+ logger.debug("agent.end.idle_skipped", {
1872
+ reason: "agent_aborted",
1873
+ errorMessage: outcome.reason,
1874
+ });
1875
+ return;
1876
+ }
1808
1877
  if (!config.enabled || !config.enableIdleNotification) {
1809
1878
  logger.debug("agent.end.idle_skipped", {
1810
1879
  reason: "idle_notification_disabled",
Binary file