Skip to content

feat: deliver proactive sub-session results to opted-in rooms#232

Open
overcuriousity wants to merge 2 commits into
mainfrom
feat/proactive-room-delivery
Open

feat: deliver proactive sub-session results to opted-in rooms#232
overcuriousity wants to merge 2 commits into
mainfrom
feat/proactive-room-delivery

Conversation

@overcuriousity
Copy link
Copy Markdown
Owner

Summary

  • Root cause fixed: Proactive sub-session results were silently dropped by broadcast() because thread_id == "default" was explicitly excluded from Matrix routing. Users never saw proactive check-ins despite them running and completing.
  • Design: No concept of a "default room" exists — routing to the last active session would leak across users. Per-room explicit opt-in is the correct model.
  • Delivery chain: is_proactive flag threads from sub_session._report()_QueueItembroadcast(), which fans out to all opted-in rooms.

Changes

File Change
wintermute/infra/thread_config.py Add proactive_enabled per-thread field (default False), get_proactive_thread_ids()
wintermute/interfaces/slash_commands.py Add /proactive [on|off] command
wintermute/core/sub_session.py Tag [PROACTIVE] results with is_proactive=True
wintermute/core/llm_thread.py is_proactive on _QueueItem, forwarded through queue and broadcast
wintermute/main.py broadcast() fans out to all opted-in rooms when is_proactive=True
docs/configuration.md Document feature as experimental
config.yaml.example Update proactive config comments

Test plan

  • Send /proactive on in a Matrix DM → confirm confirmation message received
  • Wait for or trigger a proactive session → confirm result appears in that room
  • Rooms without /proactive on receive nothing
  • Send /proactive off → subsequent proactive sessions no longer delivered
  • Send /proactive (no args) → shows current on/off status
  • Web UI debug panel still shows all proactive sessions as before
  • Non-proactive system events with thread_id="default" still do not reach Matrix

Note: Proactive sessions run in full mode and can take autonomous actions (spawn workers, create tasks, etc.) — not just summarise. Users opted in will see the LLM-processed result of whatever the session did.

🤖 Generated with Claude Code

Proactive sessions were completing successfully but their results were
silently dropped at the Matrix routing layer (broadcast() skipped any
thread_id == "default"). Users never saw proactive check-ins.

The fix threads an is_proactive flag from sub_session._report() through
the LLM queue item to the broadcast closure, which then delivers to all
rooms that have opted in via the new /proactive slash command.

Changes:
- thread_config: add proactive_enabled per-thread field (default: False)
  and get_proactive_thread_ids() helper
- slash_commands: add /proactive [on|off] command to opt rooms in/out
- sub_session: tag results from [PROACTIVE] objectives with is_proactive
- llm_thread: add is_proactive to _QueueItem, forward through
  enqueue_system_event() and broadcast call
- main: broadcast() delivers to all opted-in rooms when is_proactive=True
- docs + config.yaml.example: document feature as experimental

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 25, 2026 08:06
Copy link
Copy Markdown
Contributor

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 fixes delivery of proactive sub-session results by introducing an explicit “opt-in per room/thread” routing path, so proactive outputs (which otherwise originate from thread_id="default") can be fanned out to opted-in Matrix/Signal rooms without leaking to arbitrary “last active” rooms.

Changes:

  • Add per-thread proactive_enabled config and a helper to list opted-in thread IDs.
  • Add /proactive [on|off] slash command to toggle opt-in for a room and report status.
  • Thread an is_proactive flag from sub-session reporting → LLM queue → broadcast(), and fan out proactive results to opted-in rooms.

Reviewed changes

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

Show a summary per file
File Description
wintermute/main.py Adds is_proactive to broadcast() and proactive fan-out routing to opted-in rooms
wintermute/interfaces/slash_commands.py Adds /proactive command for opt-in/out and lists it in /commands
wintermute/infra/thread_config.py Introduces proactive_enabled and get_proactive_thread_ids()
wintermute/core/sub_session.py Tags sub-session reports as proactive based on objective prefix
wintermute/core/llm_thread.py Adds is_proactive to queue items and forwards it through system-event broadcasting
docs/configuration.md Documents opt-in delivery and deprecates proactive_target_thread_id
config.yaml.example Updates comments to reflect new opt-in behavior

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

Comment thread wintermute/main.py Outdated
Comment thread docs/configuration.md Outdated
Comment thread wintermute/interfaces/slash_commands.py Outdated
Comment thread wintermute/core/sub_session.py Outdated
Comment thread wintermute/main.py Outdated
- slash_commands: use exact token match instead of startswith to avoid
  false matches like /proactiveXYZ
- sub_session: add explicit is_proactive flag to TaskNode/SubSessionState
  instead of parsing objective string prefix (brittle coupling removed)
- scheduler: pass is_proactive=True when spawning proactive sessions
- main: fan-out sends now run concurrently via asyncio.gather; web SSE
  broadcast moved outside the loop so it fires once regardless of how
  many rooms are opted in
- docs: clarify that proactive_target_thread_id != "default" bypasses
  room opt-in fan-out entirely (previous wording was misleading)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

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

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


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

Comment thread wintermute/main.py
Comment on lines 589 to +608
if wi and thread_id:
await wi.broadcast(text, thread_id, reasoning=reasoning)
# Proactive results: deliver to all rooms that have opted in via /proactive on.
# This fires only when thread_id == "default" (proactive sessions have no specific
# target room), ensuring regular messages are never duplicated.
if is_proactive and thread_id == "default":
opted_in = thread_config_mgr.get_proactive_thread_ids()
if opted_in:
async def _send_to_room(tid: str) -> None:
try:
if si and tid.startswith("sig_"):
await si.send_message(text, tid)
elif mx and not tid.startswith("web_") and not tid.startswith("sig_"):
await mx.send_message(text, tid)
except Exception: # noqa: BLE001
logger.warning("Failed to deliver proactive message to %s", tid)

await asyncio.gather(*(_send_to_room(tid) for tid in opted_in))
if wi:
await wi.broadcast(text, thread_id, reasoning=reasoning)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

broadcast() currently calls wi.broadcast() twice for proactive messages (is_proactive=True with thread_id == "default"): once unconditionally for any thread_id and again inside the proactive fan-out block. This will duplicate proactive results in the web UI. Consider broadcasting to the web interface only once (e.g., skip the first wi.broadcast when is_proactive and thread_id == "default", or remove the second call and instead add web-thread fan-out if needed).

Copilot uses AI. Check for mistakes.
Comment thread wintermute/main.py
Comment on lines +603 to +608
except Exception: # noqa: BLE001
logger.warning("Failed to deliver proactive message to %s", tid)

await asyncio.gather(*(_send_to_room(tid) for tid in opted_in))
if wi:
await wi.broadcast(text, thread_id, reasoning=reasoning)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Rooms opting in from the web UI will set proactive_enabled on web_... thread IDs, and get_proactive_thread_ids() will return them, but the proactive fan-out explicitly skips web_ IDs and never calls wi.broadcast() for those target thread IDs. Either filter out web_ thread IDs from proactive opt-in (e.g., reject /proactive on for web threads or exclude them in get_proactive_thread_ids()), or add a wi.broadcast(text, tid, ...) branch so web threads can actually receive proactive messages.

Suggested change
except Exception: # noqa: BLE001
logger.warning("Failed to deliver proactive message to %s", tid)
await asyncio.gather(*(_send_to_room(tid) for tid in opted_in))
if wi:
await wi.broadcast(text, thread_id, reasoning=reasoning)
elif wi and tid.startswith("web_"):
await wi.broadcast(text, tid, reasoning=reasoning)
except Exception: # noqa: BLE001
logger.warning("Failed to deliver proactive message to %s", tid)
await asyncio.gather(*(_send_to_room(tid) for tid in opted_in))

Copilot uses AI. Check for mistakes.
Comment thread config.yaml.example
Comment on lines +637 to +640
# proactive_target_thread_id: "default" # Deprecated: use /proactive on in each room instead
# Proactive delivery (experimental): use the /proactive on slash command in any Matrix/Signal
# room to opt that room in to receiving proactive check-in results. Multiple rooms can be
# opted in independently. Use /proactive off to remove a room. No config change required.
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

proactive_target_thread_id is still read by the scheduler and, per the docs in this PR, can be used to force delivery to a specific thread ID (bypassing room opt-in). Labeling it as "Deprecated" here is misleading; consider rewording to indicate it’s optional/advanced and that /proactive on is the recommended default behavior when it remains set to "default".

Suggested change
# proactive_target_thread_id: "default" # Deprecated: use /proactive on in each room instead
# Proactive delivery (experimental): use the /proactive on slash command in any Matrix/Signal
# room to opt that room in to receiving proactive check-in results. Multiple rooms can be
# opted in independently. Use /proactive off to remove a room. No config change required.
# proactive_target_thread_id: "default" # Advanced/optional: keep "default" to respect room-level /proactive on opt-in (recommended), or set a specific thread ID to force delivery there.
# Proactive delivery (experimental): use the /proactive on slash command in any Matrix/Signal
# room to opt that room in to receiving proactive check-in results. Multiple rooms can be
# opted in independently. Use /proactive off to remove a room. No config change required for
# the recommended default behavior (proactive_target_thread_id = "default").

Copilot uses AI. Check for mistakes.
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.

2 participants