Skip to content

Add discord-send.py for direct Discord messaging - #36

Closed
liususan091219 wants to merge 1 commit into
mainfrom
feature/discord-proactive-send
Closed

liususan091219 wants to merge 1 commit into
mainfrom
feature/discord-proactive-send

Conversation

@liususan091219

@liususan091219 liususan091219 commented Mar 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add src/discord-send.py — simple script to send messages to Discord channels or DMs
  • Used by the proactive loop to notify the user, and by the core session to reply in channels on demand
  • No changes to discord-bridge.py

Usage

python3 src/discord-send.py <channel_id> "message"
python3 src/discord-send.py <channel_id> "msg" --reply-to <msg_id>
python3 src/discord-send.py --dm <user_id> "message"

Test plan

  • Send message to private #dev channel
  • Reply to a specific message in #dev
  • Send DM to user

🤖 Generated with Claude Code

Simple script to send messages to channels or DMs via gateway.
No task bridge needed — just call directly:
  python3 src/discord-send.py <channel_id> "message"
  python3 src/discord-send.py <channel_id> "msg" --reply-to <msg_id>
  python3 src/discord-send.py --dm <user_id> "message"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@liususan091219
liususan091219 force-pushed the feature/discord-proactive-send branch from 51d478c to 9cf31bc Compare March 29, 2026 16:15
@liususan091219 liususan091219 changed the title Discord bridge: proactive send to channels and DMs Add discord-send.py for direct Discord messaging Mar 29, 2026
@liususan091219
liususan091219 deleted the feature/discord-proactive-send branch March 29, 2026 18:52
yixuan-ag2 added a commit to yixuan-ag2/sutando that referenced this pull request Aug 18, 2026
- capability gate requires version 1 + every normative capability
- parked human retry: NO premature accepted ack — the ack waits for the
  authorization that mints the real epoch, and the deferred dial keeps
  its human-retry provenance; duplicates of the parked request stay
  silent until that ack exists
- accepted acks are never evicted from the dedup cache (duplicates never
  redial twice has no expiry); the cap bounds only non-accepted entries
- stop() is a terminal latch: late activated rejections, lifecycle
  events, timers and commands are inert after shutdown
- model progress (turn.start) now feeds the reducer's silence anchor,
  generation-fenced by the event's own transport generation; pending
  tools are keyed by {generation,id} so a stale-generation completion
  can neither settle a reused current id nor advance the anchor
- the delivered-audio baseline moved into the coordinator: a fresh
  epoch's reset-from-zero counters that are already positive count as
  delivery (a response landing before its epoch's first tick was lost)
- the reducer clock domain is monotonic (performance.now) in the live
  wiring — wall-clock jumps can no longer strand waiting-retry or
  bypass cooldowns; snapshot speech timestamps translate by age; wire
  frames and ledger rows keep wall time
- the legacy CLOSED guard stands down whenever the coordinator owns the
  episode (any non-idle phase or a recovered origin), and its cast-call
  fake attach no longer mints a coordinator client epoch
- terminal reattach: bodhi's suppressClientAutoActions gates greeting /
  context replay / the CLOSED auto-reconnect (bodhi sonichi#36 amendment);
  pin -> yixuan-ag2#bce828f

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yixuan-ag2 added a commit to yixuan-ag2/sutando that referenced this pull request Aug 18, 2026
…hi#36 landed)

The interim fork pin is gone. The merged main additionally hardened the
surface beyond the reviewed branch heads: STT capture-generation fencing
(a pre-recovery external transcript can no longer cross the boundary as
fresh evidence) and turn.start now carries attemptEpoch alongside
transportGeneration. Both additive; coordinator suite and full suite
green against the merged build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sonichi pushed a commit that referenced this pull request Aug 21, 2026
… + arming) (#3135)

* feat(voice): ACTIVE-silence watchdog — trigger facts + recovery reducer + Phase 0a shadow mode

First implementation slice of the approved ACTIVE-silence recovery design
(desktop docs/design-voice-active-silence-recovery.md; the 2026-08-17
incident: 3+ minutes of user speech into a dead ACTIVE session, recovery
only by abandonment). This PR detects and records; it never touches the
live session.

- voice-health-matrix: MatrixResult.facts — typed, verdict-independent,
  fail-closed raw facts (formulas fixed: ingressAdvanced =
  delta(deliveredFrames) > 0; modelSilentFor15s null-safe), attached to
  every result. Truth-table tests incl. first-tick/epoch-boundary/
  reconnect-window fail-closed rows.
- voice-active-silence-watchdog: pure reduceRecovery event reducer
  implementing the design's normative transition table — latched speech
  evidence, continuous-silence anchor, epoch-fenced admission, episode
  attempt budget, waiting-retry with schedule-retry effects (settles
  design residue 5), fatal-backoff clear/park semantics (residue 6),
  validated VOICE_ACTIVE_SILENCE_MODE / _TICKS parsing. The 2026-08-17
  incident is a named test (fires on the 3rd qualifying tick).
- voice-watchdog-shadow: Phase 0a diagnostic observer — derives events
  from the health tick chronologically (epoch-change attach detection,
  no stale-speech relatch), epoch-keyed tool veto (background work
  excluded; first-edge adoption), would-restart / would-terminal /
  post-fire-progress evidence rows; armed degrades to shadow (no bodhi
  capability descriptor exists yet).
- voice-watchdog-ledger: bounded, counted-drop, awaitable JSONL appender;
  flushed on shutdown.
- voice-audio-health: SpeechEvidence.lastOnsetAt — the utterance onset
  retained past the hangover, so 30s ticks see the FIRST sample of a
  completed utterance (real-ledger integration test).
- voice-agent: additive wiring only (tool hooks, tick-end observe,
  shutdown flush). The CONNECTING watchdog and CLOSED recovery guard are
  untouched; the coordinator refactor and armed mode land in PR 2 with
  the bodhi recovery surface, per the design's phasing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(voice): shadow round-2 review — boundary-first replay, fenced tool settles, honest post-fire milestones

Second Codex verification round, all findings:
- client-epoch boundaries now apply BEFORE snapshot-carried events and rebase
  the speech watermarks, so fresh new-epoch speech latches instead of being
  cleared by its own reload (named test);
- tool settles are fenced by originating epoch (a stale generation's settle
  emits no outcome and cannot advance the anchor; named test);
- post-fire classification anchors to the episode's FIRST fire only, splits
  upstream progress from functional response (post-fire-progress vs
  post-fire-functional rows), and counts toolOutcome as progress;
- meeting-mode mutations reach the shadow from every mutation site via
  noteMeetingMode (1s request path + summon/dismiss auto-switch), replayed
  chronologically — meeting-window speech between ticks never latches
  (named test);
- ledger rows gain capabilitySetId + per-row monotonic offset; drop-counter
  changes are surfaced in the log; prefer-const lint fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(voice): shadow observer cannot take the agent down

Every public entry on VoiceWatchdogShadow sits on a live path, and
voice-agent installs a crash-only uncaughtException handler
(writeCrashRecordAndExit). observeTick is the last statement in the bare
30s setInterval callback with nothing wrapping it, and the module had
zero containment across its 340 lines — so a null deref or an unexpected
snapshot shape in the shadow reducer terminated the voice agent on a
timer, taking the working paths down with it.

Guard lives inside the module rather than at the call sites: there are
seven of them (noteMeetingMode x4, noteToolCall, noteToolSettled,
observeTick) and a call site added later would not inherit a wrapper.
flush() is guarded against a SYNC throw too, since the shutdown path
awaits it.

Control, same tests, guard bypassed vs present:

  guard bypassed   FAIL a throwing reducer never escapes the tick
                   FAIL a throwing flush never escapes shutdown
  guard present    both ok, 23/23 checks pass

Raised by @sonichi.

* test(voice): the containment ledger was constructed with the wrong signature

WatchdogLedger takes one options object ({path, meta, onError?}); my
containment block passed two positional args, so o.path and o.meta were
both undefined, mkdirSync(dirname(undefined)) threw into the bare catch,
and every append failed into an explicitly-noop onError. The object
constructed fine and wrote nowhere. Eleven sibling sites in the same file
use the object form; this was the lone outlier.

It does not weaken what the block asserts today — guard() only calls
this.log(), never the ledger, and the assertion is escaped === null. The
reason to fix it is that it is a loaded trap: the moment someone extends
this block to assert a degraded observation was RECORDED, that assertion
passes against a ledger that wrote nothing, with the noop onError eating
the evidence.

Not caught by tsc because tsconfig is include:["src"], so no test file is
in typecheck scope at all — a wrong-signature call in tests/ is
structurally undetectable here. That is a repo-level gap, not this PR's.

Found by @liususan091219.

* feat(voice): ACTIVE-silence recovery coordinator — armed-mode driver

The impure driver around PR 1's pure reducer, per the design doc's
Phase 1: executes restart effects as injection-free held recoverUpstream
calls with reducer<->dial attempt-epoch correlation via the lifecycle
stream; owns the cooldown/backoff retry timer (re-validated at
delivery); latches terminal into the durable voice-stalled push with
attach-time resend on a freshly minted clientEpoch; serves the human
retry wire (schema-faithful parse, {voiceSessionId,requestId} dedup
replaying the original ack, accepted/stale/not-terminal dispositions,
fatal-backoff parking); clears terminal only on client-confirmed
delivery or an accepted retry. Structural session surface — compiles
against any bodhi pin; recoverySurfaceSupported() is the runtime gate.

17 tests drive it end-to-end against a scripted fake session with a
virtual clock and manual timers (the design's integration fake).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(voice): arm the coordinator in voice-agent behind VOICE_ACTIVE_SILENCE_MODE

Armed only on explicit VOICE_ACTIVE_SILENCE_MODE=armed AND the full
bodhi recovery surface (falls back to shadow with a loud line);
VOICE_ACTIVE_SILENCE_TICKS=0 still disables. The Phase 0a shadow keeps
running unchanged in every mode. Wiring: client command/attach/detach
hooks and the connection-lifecycle stream (bodhi #34/#36), tool and
meeting-mode taps beside the shadow's, fatal-backoff set/clear, a
per-epoch delivered-audio baseline (client-confirmed playback first,
server egress fallback) feeding functional-recovery clears, and the
ordinary CLOSED guard gated while terminal — the design's no-uncounted-
bypass rule. bodhi pin -> yixuan-ag2#b323779 (PR #36 head; interim —
repin to the sonichi merge SHA when #35/#36 land).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): coordinator hardening — nine findings from the Codex round

- capability gate requires version 1 + every normative capability
- parked human retry: NO premature accepted ack — the ack waits for the
  authorization that mints the real epoch, and the deferred dial keeps
  its human-retry provenance; duplicates of the parked request stay
  silent until that ack exists
- accepted acks are never evicted from the dedup cache (duplicates never
  redial twice has no expiry); the cap bounds only non-accepted entries
- stop() is a terminal latch: late activated rejections, lifecycle
  events, timers and commands are inert after shutdown
- model progress (turn.start) now feeds the reducer's silence anchor,
  generation-fenced by the event's own transport generation; pending
  tools are keyed by {generation,id} so a stale-generation completion
  can neither settle a reused current id nor advance the anchor
- the delivered-audio baseline moved into the coordinator: a fresh
  epoch's reset-from-zero counters that are already positive count as
  delivery (a response landing before its epoch's first tick was lost)
- the reducer clock domain is monotonic (performance.now) in the live
  wiring — wall-clock jumps can no longer strand waiting-retry or
  bypass cooldowns; snapshot speech timestamps translate by age; wire
  frames and ledger rows keep wall time
- the legacy CLOSED guard stands down whenever the coordinator owns the
  episode (any non-idle phase or a recovered origin), and its cast-call
  fake attach no longer mints a coordinator client epoch
- terminal reattach: bodhi's suppressClientAutoActions gates greeting /
  context replay / the CLOSED auto-reconnect (bodhi #36 amendment);
  pin -> yixuan-ag2#bce828f

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): second independent round — four blockers + canary telemetry

1. bodhi's attach auto-actions now gate on ownsRecovery, not just
   terminal: a reattach during waiting-retry started an uncounted
   CLOSED auto-redial racing the coordinator's own timer (the synthetic
   hold already gates injections; this gates the dial)
2. fatal backoff crossed clock domains — a wall-time deadline entered
   the monotonic reducer and never expired (with 1 fixed, the session
   would stay dead forever); the reducer now receives its own domain,
   the wall deadline stays for agent-state
3. server egress without heartbeat confirmation can no longer clear
   terminal: sent is not delivered — egress advancement now counts only
   as model activity (anchor), and only client-confirmed playback
   (chunksEnded) dispatches userVisibleResponse; the test that pinned
   the old fallback as intended now pins the opposite
4. tool settle resolves by STORED epoch (oldest first) instead of
   prefer-current, so a late old-generation completion can neither
   settle a reused current id nor advance the current anchor; every
   admitted successor generation prunes stranded entries, so they
   cannot veto recovery forever (the old test's observable was vacuous
   — toolOutcome produces no ledger row; anchors are asserted now)

Canary telemetry majors: incumbent-closed outcome rows (closed vs
forced), first-upstream-progress per restart, first-user-visible-
response on the clearing delivery — the Phase 1 gates are computable
from the recovery ledger now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: repin bodhi to the sonichi merge SHA c726e1d (#35+#36 landed)

The interim fork pin is gone. The merged main additionally hardened the
surface beyond the reviewed branch heads: STT capture-generation fencing
(a pre-recovery external transcript can no longer cross the boundary as
fresh evidence) and turn.start now carries attemptEpoch alongside
transportGeneration. Both additive; coordinator suite and full suite
green against the merged build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(voice): state the consumers-own-structural-gating contract on MatrixFacts

Review ask from @qingyun-wu on #3130: the safety of the narrowed
`factsAvailable` rests on a contract that until now lived implicitly in the
one existing consumer — voice-active-silence-watchdog's tick branch requires
`ev.state === 'ACTIVE' && s.clientAttached` before the facts can matter.

`factsAvailable` answers "did the data exist this tick?", not "is the session
fit to act?". A second consumer reading it as safe-to-act would fire without
the structural gate. Writing the contract on the family doc is the cheapest
place to stop that, since it is where the field's meaning is defined.

Comment-only: no behavior change, no type change.

* fix(voice): a stopped recovery coordinator owns nothing

ownsRecovery was `phase !== 'idle' || origin === 'active-silence'` and never
consulted the stop() latch, so stopping mid-episode left it true. voice-agent
stands the legacy F5 redial down on exactly that value, so a coordinator that
can no longer dial would also be suppressing the path that could.

Latent today: stop() has one caller, shutdown(), which ends in process.exit(0),
so nothing outlives it. The safety therefore rests on "stop() implies the
process is exiting" -- true, but stated nowhere and not enforced. A future
caller disarming mid-session or restarting would get no dial from either side.

Reported by sonichi on 0eea97b.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013BSd8tgpzEMbH2nURksZwG

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant