pi-smart-voice-notify 0.3.3 → 0.3.5

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,16 +1,21 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.3.5] - 2026-04-27
4
4
 
5
- ## [0.3.3] - 2026-04-22
5
+ ### Fixed
6
+ - Permission notifications now require an authoritative `pi-permission-system:permission-request` waiting event before alerts or reminders are queued, preventing permission-looking `tool_call` and `tool_result` payloads from producing false permission alerts.
7
+ - Forwarded permission notifications now watch only the active session's scoped request/response directories, require matching `targetSessionId`, and ignore unscoped legacy paths, stale requests, resolved requests, malformed files, and mismatched request filenames.
6
8
 
7
- ### Changed
8
- - Updated README and example config documentation to reflect `PI_CODING_AGENT_DIR`-aware global extension paths and `session_start` lifecycle reasons
9
- - Resolved extension config/debug paths and forwarded permission watcher paths from `getAgentDir()`
10
- - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.68.1
9
+ ## [0.3.4] - 2026-04-25
11
10
 
12
- ### Fixed
13
- - Prevented reminders, notification playback, and webhook queue flushing/retry waits from continuing after session shutdown
11
+ ### Added
12
+ - Added shutdown-aware notification lifecycle handling so pending reminders and queued playback stop cleanly during session shutdown
13
+ - Added `session_start.reason` handling for startup, reload, new, resume, and fork session transitions
14
+
15
+ ### Changed
16
+ - Updated global extension path documentation to account for `PI_CODING_AGENT_DIR`-aware configuration and debug paths
17
+ - Synchronized package metadata and lockfile for the 0.3.4 patch release while preserving `@mariozechner/pi-*` peer dependency range `^0.70.2`
18
+ - Release note context: `v0.3.2` is not on `main`; no tag or history repair is included in this release prep
14
19
 
15
20
  ## [0.3.2] - 2026-04-01
16
21
 
@@ -91,7 +96,7 @@
91
96
  - Rewrote README.md with professional documentation standards
92
97
  - Added comprehensive feature documentation, configuration reference, and usage examples
93
98
 
94
- ## [0.1.0] - 2026-03-02
99
+ ## 0.1.0
95
100
 
96
101
  - Standardized repository structure to `index.ts` shim + `src/` implementation.
97
102
  - Added config template and package metadata/scripts aligned with Pi extension conventions.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # 🔔 pi-smart-voice-notify
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/pi-smart-voice-notify?style=flat-square)](https://www.npmjs.com/package/pi-smart-voice-notify) [![License](https://img.shields.io/github/license/MasuRii/pi-smart-voice-notify?style=flat-square)](LICENSE)
4
+
3
5
  Windows-optimized smart notification extension for the Pi coding agent.
4
6
 
5
7
  **pi-smart-voice-notify** monitors Pi session and tool events to alert you via **multi-engine TTS**, **sound playback**, **desktop toast notifications**, and optional **webhook/AI-assisted messaging** when the agent requires your attention.
@@ -17,7 +19,7 @@ Windows-optimized smart notification extension for the Pi coding agent.
17
19
 
18
20
  - **Intelligent event detection**
19
21
  - Task completion (idle)
20
- - Direct permission blocks plus forwarded subagent permission requests
22
+ - Authoritative permission-system wait events plus current-session forwarded subagent permission requests
21
23
  - Questions requiring input (when custom `question` tool is loaded)
22
24
  - Errors
23
25
 
@@ -123,17 +125,17 @@ A starter template is provided in `config/config.example.json`. On startup, the
123
125
  | Option | Type | Default | Description |
124
126
  |--------|------|---------|-------------|
125
127
  | `enableIdleNotification` | boolean | `true` | Notify when agent finishes a task |
126
- | `enablePermissionNotification` | boolean | `true` | Notify on permission blocks |
127
- | `enableForwardedPermissionWatcher` | boolean | `true` | Watch forwarded permission request files and notify when new requests arrive |
128
+ | `enablePermissionNotification` | boolean | `true` | Notify when the permission system reports requests waiting for approval |
129
+ | `enableForwardedPermissionWatcher` | boolean | `true` | Watch current-session forwarded permission request/response files and notify when new requests arrive |
128
130
  | `includeForwardedPermissionAgentName` | boolean | `true` | Include sanitized requester agent name in forwarded permission notification text |
129
- | `watchLegacyForwardedPermissionPath` | boolean | `true` | Also watch the legacy forwarded-permission directory (default: `~/.pi/agent/permission-forwarding/requests`, respects `PI_CODING_AGENT_DIR`) when present |
131
+ | `watchLegacyForwardedPermissionPath` | boolean | `true` | Retained for config compatibility; unscoped legacy forwarded-permission paths are ignored because they cannot prove the current target session |
130
132
  | `enableQuestionNotification` | boolean | `true` | Notify when agent asks a question* |
131
133
  | `enableErrorNotification` | boolean | `true` | Notify on errors |
132
134
  | `suppressIdleAfterError` | boolean | `true` | Skip idle notification if turn had errors |
133
135
 
134
136
  *Question notifications only work when a custom `question` tool is loaded.
135
137
 
136
- Forwarded permission watcher notifications use privacy-safe text and never include raw forwarded `message` content.
138
+ Forwarded permission watcher notifications use privacy-safe text, require the request `targetSessionId` to match the active Pi session, and never include raw forwarded `message` content.
137
139
 
138
140
  ### Reminder Settings
139
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-smart-voice-notify",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Windows-optimized smart voice, sound, and desktop notifications for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "build": "npx --yes -p typescript@5.7.3 -p @types/node@20.17.57 tsc -p tsconfig.json --noEmit",
21
21
  "lint": "npm run build",
22
- "test": "node --experimental-strip-types --test src/abortable-command.test.ts src/reminder-playback.test.ts src/index.test.ts",
22
+ "test": "node --experimental-strip-types --test src/abortable-command.test.ts src/reminder-playback.test.ts src/permission-forwarding-watcher.test.ts src/index.test.ts",
23
23
  "check": "npm run build && npm run test"
24
24
  },
25
25
  "keywords": [
@@ -61,8 +61,8 @@
61
61
  ]
62
62
  },
63
63
  "peerDependencies": {
64
- "@mariozechner/pi-coding-agent": "^0.68.1",
65
- "@mariozechner/pi-tui": "^0.68.1"
64
+ "@mariozechner/pi-coding-agent": "^0.70.2",
65
+ "@mariozechner/pi-tui": "^0.70.2"
66
66
  },
67
67
  "dependencies": {
68
68
  "node-notifier": "^10.0.1"
@@ -70,7 +70,7 @@ const DEFAULT_TEMPLATES: Record<CoreAIEventType, string[]> = {
70
70
  permission: [
71
71
  "Permission needed for {projectName}. Please approve in the terminal.",
72
72
  "I need your approval to continue {projectName}.",
73
- "Action blocked. Please confirm the pending permission request.",
73
+ "Permission approval is pending. Please confirm the request.",
74
74
  ],
75
75
  question: [
76
76
  "I need your input for {projectName}. Please check the terminal.",
@@ -45,7 +45,7 @@ const DEFAULT_WEBHOOK_EVENTS: NotificationType[] = [...NOTIFICATION_TYPE_VALUES]
45
45
 
46
46
  export const INLINE_NOTIFY_TEXT: Record<NotificationType, string> = {
47
47
  idle: "✅ Agent finished its current task.",
48
- permission: "⚠️ Action blocked by permission policy.",
48
+ permission: "⚠️ Permission approval is pending.",
49
49
  question: "❓ Agent needs your input.",
50
50
  error: "❌ Agent encountered an error.",
51
51
  };
@@ -108,15 +108,6 @@ export const MESSAGE_LIBRARY: Record<NotificationType, MessageSet> = {
108
108
  },
109
109
  };
110
110
 
111
- export const PERMISSION_HINTS = [
112
- "permission",
113
- "not permitted",
114
- "requires approval",
115
- "approval",
116
- "user denied",
117
- "blocked by",
118
- ];
119
-
120
111
  export const QUESTION_HINTS = ["question", "need your input", "please answer", "requires your input"];
121
112
 
122
113
  export const DEFAULT_CONFIG: VoiceNotifyConfig = {
package/src/index.test.ts CHANGED
@@ -61,6 +61,7 @@ class FakeEventBus {
61
61
 
62
62
  class FakePi {
63
63
  private readonly handlers = new Map<string, EventHandler[]>();
64
+ private tools: Array<{ name: string }> = [];
64
65
 
65
66
  public readonly events = new FakeEventBus();
66
67
 
@@ -73,8 +74,12 @@ class FakePi {
73
74
  public registerCommand(): void {
74
75
  }
75
76
 
77
+ public setAvailableTools(tools: Array<{ name: string }>): void {
78
+ this.tools = [...tools];
79
+ }
80
+
76
81
  public getAllTools(): Array<{ name: string }> {
77
- return [];
82
+ return [...this.tools];
78
83
  }
79
84
 
80
85
  public sendMessage(): void {
@@ -397,7 +402,13 @@ test("skipWhenFocused=false still notifies even when the terminal is focused", a
397
402
 
398
403
  await pi.emit("session_start", {}, ctx);
399
404
  await flushAsyncWork();
400
- await pi.emit("tool_call", permissionEvent("call-focus-disabled"), ctx);
405
+ pi.events.emit(
406
+ PERMISSION_SYSTEM_EVENT_CHANNEL,
407
+ permissionSystemEvent("waiting", "permission-focus-disabled", {
408
+ toolCallId: "call-focus-disabled",
409
+ toolName: "write_file",
410
+ }),
411
+ );
401
412
  await flushAsyncWork();
402
413
  await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
403
414
 
@@ -425,7 +436,13 @@ test("skipWhenFocused=true suppresses focused notifications and uses config focu
425
436
 
426
437
  await pi.emit("session_start", {}, ctx);
427
438
  await flushAsyncWork();
428
- await pi.emit("tool_call", permissionEvent("call-focus-enabled"), ctx);
439
+ pi.events.emit(
440
+ PERMISSION_SYSTEM_EVENT_CHANNEL,
441
+ permissionSystemEvent("waiting", "permission-focus-enabled", {
442
+ toolCallId: "call-focus-enabled",
443
+ toolName: "write_file",
444
+ }),
445
+ );
429
446
  await flushAsyncWork();
430
447
  await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
431
448
 
@@ -525,7 +542,13 @@ test("permission reminders use the permission-specific reminder interval", async
525
542
 
526
543
  await pi.emit("session_start", {}, ctx);
527
544
  await flushAsyncWork();
528
- await pi.emit("tool_call", permissionEvent("call-permission-reminder-delay"), ctx);
545
+ pi.events.emit(
546
+ PERMISSION_SYSTEM_EVENT_CHANNEL,
547
+ permissionSystemEvent("waiting", "permission-reminder-delay", {
548
+ toolCallId: "call-permission-reminder-delay",
549
+ toolName: "write_file",
550
+ }),
551
+ );
529
552
  await flushAsyncWork();
530
553
  await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
531
554
 
@@ -539,6 +562,47 @@ test("permission reminders use the permission-specific reminder interval", async
539
562
  assert.equal(countReminderCalls(ttsCalls), 1);
540
563
  });
541
564
 
565
+ test("blocked tool_call events do not trigger permission notifications without permission-system waiting state", async (t) => {
566
+ disableFocusDetection(t);
567
+ useMockClock(t);
568
+
569
+ const { ctx, pi, ttsCalls } = createHarness();
570
+
571
+ await pi.emit("session_start", {}, ctx);
572
+ await flushAsyncWork();
573
+ await pi.emit("tool_call", permissionEvent("call-no-authoritative-wait"), ctx);
574
+ await flushAsyncWork();
575
+ await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
576
+
577
+ assert.equal(immediateNotificationCalls(ttsCalls).length, 0);
578
+ assert.equal(countReminderCalls(ttsCalls), 0);
579
+ });
580
+
581
+ test("permission-looking tool_result errors do not trigger permission notifications without permission-system waiting state", async (t) => {
582
+ disableFocusDetection(t);
583
+ useMockClock(t);
584
+
585
+ const { ctx, pi, ttsCalls } = createHarness();
586
+
587
+ await pi.emit("session_start", {}, ctx);
588
+ await flushAsyncWork();
589
+ await pi.emit(
590
+ "tool_result",
591
+ {
592
+ toolCallId: "result-no-authoritative-wait",
593
+ toolName: "permission_guard",
594
+ isError: true,
595
+ content: [{ type: "text", text: "Requires approval from the user before continuing." }],
596
+ },
597
+ ctx,
598
+ );
599
+ await flushAsyncWork();
600
+ await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
601
+
602
+ assert.equal(immediateNotificationCalls(ttsCalls).length, 0);
603
+ assert.equal(countReminderCalls(ttsCalls), 0);
604
+ });
605
+
542
606
  test("permission-system waiting events trigger a permission notification and cancel on resolution", async (t) => {
543
607
  disableFocusDetection(t);
544
608
  useMockClock(t);
@@ -605,9 +669,21 @@ test("tool_execution_start only cancels the resolved permission reminder flow",
605
669
 
606
670
  await pi.emit("session_start", {}, ctx);
607
671
  await flushAsyncWork();
608
- await pi.emit("tool_call", permissionEvent("call-a"), ctx);
672
+ pi.events.emit(
673
+ PERMISSION_SYSTEM_EVENT_CHANNEL,
674
+ permissionSystemEvent("waiting", "permission-call-a", {
675
+ toolCallId: "call-a",
676
+ toolName: "write_file",
677
+ }),
678
+ );
609
679
  await flushAsyncWork();
610
- await pi.emit("tool_call", permissionEvent("call-b"), ctx);
680
+ pi.events.emit(
681
+ PERMISSION_SYSTEM_EVENT_CHANNEL,
682
+ permissionSystemEvent("waiting", "permission-call-b", {
683
+ toolCallId: "call-b",
684
+ toolName: "write_file",
685
+ }),
686
+ );
611
687
  await flushAsyncWork();
612
688
 
613
689
  assert.equal(countReminderCalls(ttsCalls), 0);
@@ -640,9 +716,21 @@ test("tool_result resolution keeps another permission reminder active while drop
640
716
 
641
717
  await pi.emit("session_start", {}, ctx);
642
718
  await flushAsyncWork();
643
- await pi.emit("tool_call", permissionEvent("call-a"), ctx);
719
+ pi.events.emit(
720
+ PERMISSION_SYSTEM_EVENT_CHANNEL,
721
+ permissionSystemEvent("waiting", "permission-result-call-a", {
722
+ toolCallId: "call-a",
723
+ toolName: "write_file",
724
+ }),
725
+ );
644
726
  await flushAsyncWork();
645
- await pi.emit("tool_call", permissionEvent("call-b"), ctx);
727
+ pi.events.emit(
728
+ PERMISSION_SYSTEM_EVENT_CHANNEL,
729
+ permissionSystemEvent("waiting", "permission-result-call-b", {
730
+ toolCallId: "call-b",
731
+ toolName: "write_file",
732
+ }),
733
+ );
646
734
  await flushAsyncWork();
647
735
  await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
648
736
  await tickAndFlush(1_000);
@@ -662,7 +750,7 @@ test("tool_result resolution keeps another permission reminder active while drop
662
750
  assert.equal(countReminderCalls(ttsCalls), 1);
663
751
  });
664
752
 
665
- test("tool_result with the same toolCallId still classifies after a permission-blocked tool_call", async (t) => {
753
+ test("tool_result with the same toolCallId still classifies after a permission-system waiting event", async (t) => {
666
754
  disableFocusDetection(t);
667
755
  useMockClock(t);
668
756
 
@@ -672,7 +760,13 @@ test("tool_result with the same toolCallId still classifies after a permission-b
672
760
 
673
761
  await pi.emit("session_start", {}, ctx);
674
762
  await flushAsyncWork();
675
- await pi.emit("tool_call", permissionEvent("call-shared"), ctx);
763
+ pi.events.emit(
764
+ PERMISSION_SYSTEM_EVENT_CHANNEL,
765
+ permissionSystemEvent("waiting", "permission-shared", {
766
+ toolCallId: "call-shared",
767
+ toolName: "write_file",
768
+ }),
769
+ );
676
770
  await flushAsyncWork();
677
771
  await tickAndFlush(PERMISSION_BATCH_WINDOW_MS);
678
772
  assert.equal(immediateNotificationCalls(ttsCalls).length, 1);
@@ -692,6 +786,79 @@ test("tool_result with the same toolCallId still classifies after a permission-b
692
786
  assert.equal(immediateNotificationCalls(ttsCalls).length, 2);
693
787
  });
694
788
 
789
+ test("agent_end triggers an idle notification when idle notifications are enabled", async (t) => {
790
+ disableFocusDetection(t);
791
+ useMockClock(t);
792
+
793
+ const { ctx, pi, ttsCalls } = createHarness({
794
+ enableIdleNotification: true,
795
+ reminderEnabled: false,
796
+ });
797
+
798
+ await pi.emit("session_start", {}, ctx);
799
+ await flushAsyncWork();
800
+ await pi.emit("agent_end", {}, ctx);
801
+ await flushAsyncWork();
802
+
803
+ assert.equal(immediateNotificationCalls(ttsCalls).length, 1);
804
+ assert.equal(countReminderCalls(ttsCalls), 0);
805
+ });
806
+
807
+ test("question-classified tool_result triggers a question notification when the question tool is available", async (t) => {
808
+ disableFocusDetection(t);
809
+ useMockClock(t);
810
+
811
+ const { ctx, pi, ttsCalls } = createHarness({
812
+ enableQuestionNotification: true,
813
+ reminderEnabled: false,
814
+ });
815
+ pi.setAvailableTools([{ name: "question" }]);
816
+
817
+ await pi.emit("session_start", {}, ctx);
818
+ await flushAsyncWork();
819
+ await pi.emit(
820
+ "tool_result",
821
+ {
822
+ toolCallId: "call-question-available",
823
+ toolName: "custom_tool",
824
+ isError: false,
825
+ content: [{ type: "text", text: "This request requires your input before continuing." }],
826
+ },
827
+ ctx,
828
+ );
829
+ await flushAsyncWork();
830
+
831
+ assert.equal(immediateNotificationCalls(ttsCalls).length, 1);
832
+ assert.equal(countReminderCalls(ttsCalls), 0);
833
+ });
834
+
835
+ test("question-classified tool_result does not notify when the question tool is unavailable", async (t) => {
836
+ disableFocusDetection(t);
837
+ useMockClock(t);
838
+
839
+ const { ctx, pi, ttsCalls } = createHarness({
840
+ enableQuestionNotification: true,
841
+ reminderEnabled: false,
842
+ });
843
+
844
+ await pi.emit("session_start", {}, ctx);
845
+ await flushAsyncWork();
846
+ await pi.emit(
847
+ "tool_result",
848
+ {
849
+ toolCallId: "call-question-unavailable",
850
+ toolName: "custom_tool",
851
+ isError: false,
852
+ content: [{ type: "text", text: "This request requires your input before continuing." }],
853
+ },
854
+ ctx,
855
+ );
856
+ await flushAsyncWork();
857
+
858
+ assert.equal(immediateNotificationCalls(ttsCalls).length, 0);
859
+ assert.equal(countReminderCalls(ttsCalls), 0);
860
+ });
861
+
695
862
  test("forwarded permission resolution cancels a queued reminder before it fires", async (t) => {
696
863
  disableFocusDetection(t);
697
864
  useMockClock(t);
package/src/index.ts CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  isWindows,
20
20
  MESSAGE_LIBRARY,
21
21
  normalizeConfig,
22
- PERMISSION_HINTS,
23
22
  QUESTION_HINTS,
24
23
  readConfigFromDisk,
25
24
  SOUND_LOOPS,
@@ -125,45 +124,9 @@ function classifyToolResult(
125
124
  return "question";
126
125
  }
127
126
 
128
- if (normalizedTool.includes("permission") || PERMISSION_HINTS.some((hint) => normalizedText.includes(hint))) {
129
- return "permission";
130
- }
131
-
132
127
  return "error";
133
128
  }
134
129
 
135
- function readBlockedReason(value: unknown): string | null {
136
- const record = toRecord(value);
137
- const blockValue = record.block;
138
- const isBlocked = blockValue === true || blockValue === "true";
139
- if (!isBlocked) {
140
- return null;
141
- }
142
-
143
- const reason = record.reason;
144
- if (typeof reason !== "string") {
145
- return null;
146
- }
147
-
148
- const normalizedReason = reason.trim();
149
- return normalizedReason.length > 0 ? normalizedReason : null;
150
- }
151
-
152
- function extractToolCallBlockReason(event: unknown): string | null {
153
- const directReason = readBlockedReason(event);
154
- if (directReason) {
155
- return directReason;
156
- }
157
-
158
- const record = toRecord(event);
159
- return readBlockedReason(record.result);
160
- }
161
-
162
- function isPermissionReason(reason: string): boolean {
163
- const normalizedReason = reason.toLowerCase();
164
- return PERMISSION_HINTS.some((hint) => normalizedReason.includes(hint));
165
- }
166
-
167
130
  function statusLine(config: VoiceNotifyConfig): string | undefined {
168
131
  if (!config.enabled) {
169
132
  return "voice:off";
@@ -215,6 +178,28 @@ function envInteger(defaultValue: number, ...keys: string[]): number {
215
178
  return Number.isFinite(parsed) ? parsed : defaultValue;
216
179
  }
217
180
 
181
+ function normalizePermissionForwardingSessionId(value: unknown): string | null {
182
+ if (typeof value !== "string") {
183
+ return null;
184
+ }
185
+ const trimmed = value.trim();
186
+ if (!trimmed || trimmed.toLowerCase() === "unknown") {
187
+ return null;
188
+ }
189
+ return trimmed;
190
+ }
191
+
192
+ function getPermissionForwardingSessionId(ctx: ExtensionContext): string | null {
193
+ try {
194
+ const sessionManager = "sessionManager" in ctx
195
+ ? ctx.sessionManager as { getSessionId?: () => unknown }
196
+ : null;
197
+ return normalizePermissionForwardingSessionId(sessionManager?.getSessionId?.());
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+
218
203
  function sanitizeAgentName(value: string | null): string | null {
219
204
  if (!value) {
220
205
  return null;
@@ -362,7 +347,6 @@ export default function smartVoiceNotifyExtension(
362
347
  const pendingReminders = new Map<ReminderKey, ReminderState>();
363
348
  const reminderPlayback = new ReminderPlaybackController();
364
349
  const pendingPermissionToolCallIds = new Set<string>();
365
- const blockedPermissionToolCallIds = new Set<string>();
366
350
  const processedToolResultToolCallIds = new Set<string>();
367
351
  const lastNotificationAt = new Map<NotificationType, number>();
368
352
 
@@ -590,6 +574,7 @@ export default function smartVoiceNotifyExtension(
590
574
  permissionForwardingWatcher.start({
591
575
  enabled: config.enabled && config.enablePermissionNotification && config.enableForwardedPermissionWatcher,
592
576
  watchLegacyPath: config.watchLegacyForwardedPermissionPath,
577
+ targetSessionId: getPermissionForwardingSessionId(activeSessionContext),
593
578
  });
594
579
  };
595
580
 
@@ -1415,7 +1400,6 @@ export default function smartVoiceNotifyExtension(
1415
1400
  if (event.state === "waiting") {
1416
1401
  if (event.toolCallId) {
1417
1402
  pendingPermissionToolCallIds.add(event.toolCallId);
1418
- rememberScopedToolCallId(event.toolCallId, blockedPermissionToolCallIds);
1419
1403
  }
1420
1404
  logger.debug("permission_system.wait_detected", {
1421
1405
  requestId: event.requestId,
@@ -1687,7 +1671,6 @@ export default function smartVoiceNotifyExtension(
1687
1671
  hadErrorInTurn = false;
1688
1672
  warnedDesktopUnsupported = false;
1689
1673
  pendingPermissionToolCallIds.clear();
1690
- blockedPermissionToolCallIds.clear();
1691
1674
  processedToolResultToolCallIds.clear();
1692
1675
  lastNotificationAt.clear();
1693
1676
 
@@ -1730,7 +1713,6 @@ export default function smartVoiceNotifyExtension(
1730
1713
  activeSessionContext = null;
1731
1714
  permissionForwardingWatcher.stop();
1732
1715
  pendingPermissionToolCallIds.clear();
1733
- blockedPermissionToolCallIds.clear();
1734
1716
  processedToolResultToolCallIds.clear();
1735
1717
  resetPermissionBatch("session_shutdown");
1736
1718
  cancelReminderActivity("session_shutdown");
@@ -1766,37 +1748,13 @@ export default function smartVoiceNotifyExtension(
1766
1748
  pi.on("agent_start", async () => {
1767
1749
  hadErrorInTurn = false;
1768
1750
  pendingPermissionToolCallIds.clear();
1769
- blockedPermissionToolCallIds.clear();
1770
1751
  processedToolResultToolCallIds.clear();
1771
1752
  resetPermissionBatch("agent_start");
1772
1753
  logger.debug("agent.start", {});
1773
1754
  });
1774
1755
 
1775
- pi.on("tool_call", async (event, ctx) => {
1756
+ pi.on("tool_call", async (_event, ctx) => {
1776
1757
  activeSessionContext = ctx;
1777
- if (!config.enabled || !config.enablePermissionNotification) {
1778
- return {};
1779
- }
1780
-
1781
- const reason = extractToolCallBlockReason(event);
1782
- if (!reason || !isPermissionReason(reason)) {
1783
- return {};
1784
- }
1785
-
1786
- if (!rememberScopedToolCallId(event.toolCallId, blockedPermissionToolCallIds)) {
1787
- return {};
1788
- }
1789
-
1790
- pendingPermissionToolCallIds.add(event.toolCallId);
1791
- logger.debug("tool_call.permission_blocked", {
1792
- toolCallId: event.toolCallId,
1793
- toolName: event.toolName,
1794
- reason,
1795
- });
1796
- queuePermissionNotification(ctx, {
1797
- reminderKey: permissionReminderKey(event.toolCallId),
1798
- reason,
1799
- });
1800
1758
  return {};
1801
1759
  });
1802
1760
 
package/src/logging.ts CHANGED
@@ -59,14 +59,13 @@ export function createExtensionLogger(options: LoggerOptions): ExtensionLogger {
59
59
  ...details,
60
60
  });
61
61
  appendFileSync(debugLogPath, `${line}\n`, "utf-8");
62
- } catch (error) {
63
- console.error(`[${extensionId}] Failed to write debug log: ${getErrorMessage(error)}`);
62
+ } catch {
63
+ // Debug logging must never write to stdout/stderr from extension code.
64
64
  }
65
65
  };
66
66
 
67
67
  const error = (cause: unknown): void => {
68
68
  debug("runtime.error", { error: cause });
69
- console.error(`[${extensionId}] ${getErrorMessage(cause)}`);
70
69
  };
71
70
 
72
71
  return { debug, error };