/v1/subscriptions rows carry channel_name (from the channel buffer's
names map; the address stays the identifier), and every surface that
lists a subscribed channel renders '#ocw-test' with the address as a
tooltip/sub-label: the session drawer's Slack-channels panel, the
composer chip popover, the Messaging-routing table, and the connector
card's Sessions-listening block.
Two ID-only surfaces from the owner's Slack pass:
- Allow-list chips showed 'U07JK68S4BH'. New persisted people directory
(people.json: platform:user_id → display name) noted on every inbound
— authorized or parked — and seeded from already-parked items;
/v1/connectors carries allowed_user_names and backfills recent-sender
names from it. Chips render the name (id in the tooltip).
- The recent-channels dropdown showed only 'slack:C0…' addresses.
ChannelBuffer now records the resolved channel display name
(channels.json grows a names map; the first shipped bare-messages
format still loads) and /v1/channels/recent carries it. The picker
shows '#ocw-test' with the address as a sub-label and filters on
name, address, or last message text.
First contact on Slack took a double-send: the allow-list (closed by
default, correctly) silently DROPPED the first message; the sender only
surfaced under Recent senders and had to message again after being
allowed.
- ParkedStore (parked.json, capped): the gateway's allow-list drop now
parks the message via an on_unauthorized callback instead of losing
it. The connector card resolves each item inline: Allow & deliver
(allow-list the sender and re-inject the original message through the
normal inbound path — buffer + subscriptions, no re-send), Allow
only, or Dismiss.
- Sessions-listening block on the two-way connector card — the
per-connector cut of the global Channel-subscriptions table (which
lives under Messaging routing, where the owner didn't find it) with
inline unsubscribe.
- Gateway hot-reload: connect/disconnect of a messaging connector
refreshes the listeners in-process (a platform socket authenticates
at connect time, so new creds mean reopening that socket — and
nothing else). Pasting tokens now takes effect immediately; before,
the listener only started in the app lifespan, so new tokens
silently did nothing until a sidecar restart.
- Slack setup instructions now name users:read / channels:read /
groups:read (sender + channel display names resolve via these; the
owner's bot lacked them, so senders read 'unknown').
Tests: park→resolve lifecycle incl. allow_deliver reaching subscriber
+ buffer; ParkedStore persistence/cap; refresh_gateway swaps
listeners; 3 e2e flows on the connector card.
Three fixes from the owner's Slack-on-PM setup (2026-07-04):
- The recent-channels suggestion list was an in-memory ring buffer that
emptied on every sidecar restart — useless as a picker source.
ChannelBuffer now persists (best-effort JSON at channels.json) when
given a state_path; catch-up messages survive restarts too.
- The suggestions rode a native <datalist>, which WKWebView (the macOS
desktop shell) doesn't render at all. ChannelPicker now uses a
hand-rolled popover (focus opens, typing filters on address or last
message text, mousedown picks) that behaves the same on Mac, Windows
and web.
- resolve_channel accepts Slack's channel Copy-link URL
(…slack.com/archives/C0123 → slack:C0123, case-normalized) and
rejects a bare #name instead of storing it literally — a literal
#general never matches inbound slack:C… traffic, so it was a dead
subscription. The subscribe route returns a paste-the-ID hint, the
Sources drawer shows it inline, and the misleading '#channel'
placeholder is gone.
Tests: resolve_channel forms, buffer persistence + corrupt-file
startup, route rejection; two new e2e flows (popover, link/name adds)
with backend-parity resolution in the fixture mock.
Any test that built a SessionManager without COWORKER_STATE_DIR read the
developer's machine-global SecretStore — including their real cloud
sign-in, so test session creation emitted REAL coworker_session_created
events to prod (discovered as burst noise in the telemetry table). Six
tests turned out to be riding the developer's connected Slack profile for
inbound-delivery gating; they now connect a fake Slack in their own store.
Side effect: the suite is ~45% faster (no live telemetry threads).
Connector inbound messages carry a display-only source sidecar so the GUI
renders a rich card (logo, channel/person names, ids-on-hover, time) while
the model still receives the framed text. The sidecar is persisted with the
message and stripped unconditionally before the provider feed. Frontend adds
ConnectorMessageCard wired through an extracted, testable itemsFromMessages.
DMs depended on the legacy always-on super-agent (a hidden __superagent__
session + a GUI surface that wasn't reachable from the sidebar). In the durable-
session model that surface is vestigial: a DM should just drive a real session.
DM routing:
- prefs-backed dm_session designation + GET/POST /v1/messaging/dm-route.
- _dispatch_inbound routes a DM to the designated session (delivered like any
background turn, so it streams live via the per-session bus) or, with none set,
parks it as unrouted for visibility.
- GUI 'Direct messages' control in Integrations (pick the session, or park).
Retire the super-agent:
- delete connectors/superagent.py + its exports; remove the SuperAgent build/run,
_sa_* GUI surface, superagent config/status, SUPERAGENT_SESSION_ID, the
/ws/superagent socket + /v1/superagent routes; start_gateway keeps the gateway
+ scheduler only. Scheduled-task 'done' notices now broadcast to the run's own
session view.
- GUI: delete SuperAgentView + the disabled Super-agent manage tab + getSuperagent.
- myhelper persona kept resolvable (persisted sessions may reference it); docstrings
de-super-agent-ified.
- tests: test_dm_routing.py; updated the DM case in test_subscriptions; dropped the
super-agent tests in test_connectors.
Turns the view-only subscriptions surface into a managed one.
- REST: POST /v1/subscriptions (subscribe; resolves a Slack #mention or address),
POST /v1/subscriptions/remove (unsubscribe), GET /v1/channels/recent (the picker's
recently-seen source from the channel buffer).
- ChannelPicker: an input + datalist of recent channels (pick) with free typing
(slack:C0123 or #channel) — the "recent list + type-the-id" picker.
- Global (Integrations): each row gets an unsubscribe ×; an add form (session select
+ ChannelPicker + Subscribe).
- Per-session: the composer-head plug chip opens a popover to add/remove this
session's channels.
Tested (subscribe/unsubscribe/recent endpoints + #mention resolution).
A human-facing view of the agent-driven subscriptions (creation stays via ask_user;
GUI manage comes next).
- Global: a "Channel subscriptions" table in Integrations — each session → channel it
listens to (inbound) and the channel its Inbox routes to (outbound), with a warning
when they collide on one channel. GET /v1/subscriptions (server-joined with session
title/agent + routing), polled.
- Per-session: list_sessions gains `subscriptions`; the composer head shows a plug chip
("📡 N") titled with the channels when the active session listens to any.
Read-only this pass (by design). Next: GUI-managed subscribe/unsubscribe with
a channel picker (recently-seen list + type-the-id). Tested (endpoint + collision flag).
A session can subscribe to a messaging channel and receive its messages — the
inbound counterpart of (outbound) Inbox routing.
- SubscriptionStore: persisted (session_id, channel) records, permanent until the
agent/user unsubscribes or the session is deleted (the one implicit teardown).
Channel = "<platform>:<chat_id>".
- Agent tools (messaging personas): subscribe_channel / unsubscribe_channel /
list_subscriptions / get_channel_messages. The agent bootstraps via ask_user to
learn the channel; subscribe_channel parses a Slack <#id|name> mention or an
address. A guard warns when a subscription collides with an Inbox-routing target.
- Gateway dispatch: token → Inbox (unchanged); else a channel message is buffered
(ring buffer for get_channel_messages) and fanned out to every subscribed session
via manager.deliver_to_session (busy→steer / idle→background turn, shared with
self-wake — no socket needed); a DM with no subscription → the super-agent.
- Loop prevention is free: the Slack adapter already drops bot-self messages, and
one bot identity makes agent-↔-agent loops impossible.
v1 filter = the subscription itself (all of a channel's non-bot messages); mention
/thread filtering deferred. Sessions are durable (never end except on explicit
delete) — documented in MESSAGING-AND-SESSIONS.md along with these decisions.
Tests: store CRUD/persistence, parsing, buffer, tools, and the fan-out dispatch.
Full suite 449 passed (3 pre-existing SDK-import failures unrelated).