close
Skip to content

fix: suppress NO_REPLY streaming with unified quiet mode#127

Merged
ChrisRomp merged 4 commits intomainfrom
fix/no-reply-streaming
Mar 21, 2026
Merged

fix: suppress NO_REPLY streaming with unified quiet mode#127
ChrisRomp merged 4 commits intomainfrom
fix/no-reply-streaming

Conversation

@ChrisRomp
Copy link
Copy Markdown
Owner

Summary

Replaces the simple nudgePending Set with a unified quiet mode system that fully suppresses all streaming output until we determine whether the response is NO_REPLY.

What it does

  • Suppresses all streaming events (deltas, tool activity, status, permissions, user input) during startup nudges and scheduled tasks
  • Defers stream creation no more "Working..." flash before NO_REPLY responsesentirely
  • Flushes with real content via initialContent when the response is not NO_REPLY
  • Auto-denies permissions and resolves user input requests during quiet mode (prevents session deadlock)
  • 60s timeout safety net per quiet state entry

Key changes

  • src/core/quiet-mode.ts: New QuietState interface, enterQuietMode(), exitQuietMode(), getQuietState(), isQuiet()module
  • src/core/quiet-mode.test.ts: 13 tests covering lifecycle, timeout, cleanup, state tracking
  • src/index.ts: Wires quiet mode into event handler, scheduler wrapper, and nudge function; removes inline nudgePending code

Design decisions

  1. Don't buffer just track hasContent boolean. Use assistant.message content (authoritative) for flush.deltas
  2. Skip empty assistant. SDK fires these as tool-call signals before tool execution.message
  3. Let session.idle pass so markIdle() fires and waitForChannelIdle() resolves normally.through
  4. Resolve (not just suppress) permissions/ prevents SDK session deadlock on unresolved promises.input

Closes #119

ChrisRomp and others added 3 commits March 21, 2026 15:07
Replace nudgePending Set with quietState Map that fully suppresses
all streaming output (deltas, tool activity, status, permissions)
until we determine if the response is NO_REPLY.

Key changes:
- Buffer  just track hasContent flag. Use assistant.messagenothing
  content (authoritative) for flush, not accumulated deltas.
- Skip empty assistant.message events (SDK tool-call signals)
- Defer stream creation in scheduler wrapper (no Working... flash)
- Auto-deny permissions during quiet mode
- 60s timeout safety net per quiet state entry
- Clear on: non-empty assistant.message, session.error, session.idle

Applies to both startup nudges and scheduled tasks.

Closes #119

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move QuietState, enterQuietMode, exitQuietMode from inline definitions
in index.ts to src/core/quiet-mode.ts. Add getQuietState() and isQuiet()
accessors. Wire index.ts to use the new module.

Tests cover: enter/exit lifecycle, channel isolation, cleanup function
idempotency, 60s timeout safety net, hasContent tracking, threadRoot
preservation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hrough

Address two code review findings:
1. Permission/user-input auto-deny during quiet now calls
   resolvePermission(false) / resolveUserInput('') instead of just
    prevents session from blocking on unresolved promise.returning
2. session.idle events pass through quiet mode so markIdle() fires
   and waitForChannelIdle() resolves normally. Other status events
   remain suppressed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 21, 2026 22:34
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a unified “quiet mode” mechanism to fully suppress streaming output (especially assistant.message_delta) until the bridge can determine whether a response is NO_REPLY, preventing leaked NO_REPLY text in DM channels after restart.

Changes:

  • Added a new quiet-mode core module to track per-channel quiet state with a timeout safety net.
  • Updated handleSessionEvent() to suppress streaming/tool/status output during quiet mode and flush only once real content is confirmed.
  • Wired quiet mode into startup nudges and scheduled tasks, replacing the old nudgePending Set behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/index.ts Integrates quiet mode into scheduler + admin nudge flows and adds quiet-mode suppression/flush logic inside session event handling.
src/core/quiet-mode.ts New quiet-mode state tracker with per-channel state + 60s timeout cleanup.
src/core/quiet-mode.test.ts Adds Vitest coverage for quiet mode lifecycle, timeout behavior, and state tracking.
Comments suppressed due to low confidence (2)

src/core/quiet-mode.ts:29

  • The 60s quiet-mode safety timeout is not unref()'d. Other long-lived timers in this repo (e.g., channel-idle) call unref() to avoid keeping the Node process alive unintentionally. Consider unref'ing this timeout as well for consistency and to prevent shutdown hangs in non-process.exit() scenarios (tests/CLI usage).
  const timeout = setTimeout(() => {
    log.warn(`Quiet mode timeout (60s) for channel ${channelId.slice(0, 8)}... — force-clearing`);
    state.delete(channelId);
  }, TIMEOUT_MS);

  state.set(channelId, { hasContent: false, timeout });

src/index.ts:2238

  • Same as scheduler: enterQuietMode() returns a cleanup function that is only called on error. If no events are processed (or handleSessionEvent returns early), the channel can remain quiet for up to 60s and interfere with subsequent messages. Consider invoking the cleanup in a finally once sendMessage() completes (or after an idle wait, if you add one).
      const clearQuiet = enterQuietMode(channelId);
      try {
        await sessionManager.sendMessage(channelId, NUDGE_PROMPT);
      } catch (err) {
        clearQuiet();
        throw err;
      }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core/quiet-mode.ts
Comment thread src/index.ts
Comment thread src/index.ts Outdated
- Remove dead QuietState fields (threadRoot,  threadRoothasContent)
  was never set (flush uses channelThreadRoots), hasContent was mutated
  but never read for decisions
- Add finally{} cleanup in scheduler wrapper and nudge function so
  quiet mode is always cleared on success path, not just on error
- Fix misleading comment: errors exit quiet and fall through (not suppressed)
- Remove unused getQuietState import from index.ts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ChrisRomp ChrisRomp merged commit cd0b4ec into main Mar 21, 2026
6 checks passed
@ChrisRomp ChrisRomp deleted the fix/no-reply-streaming branch March 21, 2026 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NO_REPLY text leaks into DM channels on restart

2 participants