From 71b0df8ea220ca0b81ec77172dd1c842e631d218 Mon Sep 17 00:00:00 2001 From: Rohit C Prasad Date: Wed, 22 Jul 2026 23:32:14 -0700 Subject: [PATCH] Rename bot references from ocw to OpenWorker Correlation token now emits [ow:id]; legacy [ocw:id] replies still parse. Invite hints and docstrings say @OpenWorker. --- coworker/connectors/gateway.py | 2 +- coworker/connectors/relay_client.py | 2 +- coworker/connectors/senders.py | 2 +- coworker/connectors/slack_directory.py | 4 ++-- coworker/connectors/tools.py | 2 +- coworker/inbox_routing.py | 11 ++++++----- coworker/interactions.py | 2 +- coworker/mentions.py | 2 +- coworker/subscriptions.py | 2 +- surfaces/gui/src/api.ts | 2 +- .../gui/src/components/AutomationQuickstart.tsx | 2 +- tests/test_inbox_routing.py | 16 ++++++++++++---- tests/test_send_target_resolution.py | 2 +- 13 files changed, 30 insertions(+), 21 deletions(-) diff --git a/coworker/connectors/gateway.py b/coworker/connectors/gateway.py index e7e8baea..0946feb4 100644 --- a/coworker/connectors/gateway.py +++ b/coworker/connectors/gateway.py @@ -44,7 +44,7 @@ class Gateway: ) self._handler = handler # Tried before the handler: if an inbound message is an Inbox reply (carries an - # [ocw:] token), it resolves the item and is consumed — not routed as a new turn. + # [ow:] token), it resolves the item and is consumed — not routed as a new turn. self._reply_resolver = reply_resolver # A button click on an interactive prompt (resolves an Inbox item by id). self._interaction_handler = interaction_handler diff --git a/coworker/connectors/relay_client.py b/coworker/connectors/relay_client.py index 25e490ec..24de2793 100644 --- a/coworker/connectors/relay_client.py +++ b/coworker/connectors/relay_client.py @@ -329,7 +329,7 @@ class SlackRelayAdapter(BasePlatformAdapter): return channel = mapped.source.chat_id # bare channel id before qualification # Resolve friendly names with THIS workspace's bot token (cached per team), - # mirroring the Socket-Mode adapter — so cards read "@ocw"/"Rohit"/"#ocw-test" + # mirroring the Socket-Mode adapter — so cards read "@OpenWorker"/"Rohit"/"#ocw-test" # not raw U…/C… ids. Best-effort: ids fall through on failure. if not mapped.source.user_name: mapped.source.user_name = await self._display_name( diff --git a/coworker/connectors/senders.py b/coworker/connectors/senders.py index 17a4c146..9ca9ff8e 100644 --- a/coworker/connectors/senders.py +++ b/coworker/connectors/senders.py @@ -82,7 +82,7 @@ def _send_slack( return SendResult(True, message_id=data.get("ts")) err = data.get("error") or "slack send failed" if err == "not_in_channel": - err = "not_in_channel — invite @ocw to the channel in Slack, then retry" + err = "not_in_channel — invite @OpenWorker to the channel in Slack, then retry" return SendResult(False, error=err) diff --git a/coworker/connectors/slack_directory.py b/coworker/connectors/slack_directory.py index 1da9a6ce..9f3b21a5 100644 --- a/coworker/connectors/slack_directory.py +++ b/coworker/connectors/slack_directory.py @@ -10,7 +10,7 @@ Slack API notes: `users.list` is Tier-2 (~20 req/min) and Slack's own guidance is to cache it — one paginated sweep per workspace per TTL, filtered locally. Private channels only appear where the bot is a MEMBER (API constraint — the GUI words it honestly); public channels carry `is_member` so the picker can -hint "invite @ocw in Slack" instead of silently failing to listen. +hint "invite @OpenWorker in Slack" instead of silently failing to listen. """ from __future__ import annotations @@ -157,7 +157,7 @@ def list_channels( refresh: bool = False, ) -> dict[str, Any]: """Channels the token can see: all public ones, private only where the bot - is a member. `is_member` lets the GUI hint "invite @ocw" for the rest.""" + is a member. `is_member` lets the GUI hint "invite @OpenWorker" for the rest.""" token = _bot_token(secrets, team_id) if not token: return {"ok": False, "error": "workspace not connected"} diff --git a/coworker/connectors/tools.py b/coworker/connectors/tools.py index 135589ba..8b2f4ce4 100644 --- a/coworker/connectors/tools.py +++ b/coworker/connectors/tools.py @@ -106,7 +106,7 @@ def _resolve_slack_channel( chat_id = str(c["id"]) if team == "default" else f"{team}/{c['id']}" if not c.get("is_member"): return None, ( - f"found #{query}, but the bot isn't a member — invite @ocw to #{query} " + f"found #{query}, but the bot isn't a member — invite @OpenWorker to #{query} " "in Slack, then retry" ) return chat_id, None diff --git a/coworker/inbox_routing.py b/coworker/inbox_routing.py index 51f7501d..ef115e95 100644 --- a/coworker/inbox_routing.py +++ b/coworker/inbox_routing.py @@ -19,9 +19,10 @@ from pathlib import Path from typing import Callable, Optional DEFAULT_INBOX = "default" -_ID_TOKEN = re.compile( - r"\[ocw:([0-9a-f]{6,})\]" -) # embeds the item id in a delivered message +# Embeds the item id in a delivered message. Emitted as [ow:…] since the bot's rebrand +# to OpenWorker (2026-07-22); the legacy [ocw:…] spelling stays parseable so replies to +# messages sent before the rename still resolve. +_ID_TOKEN = re.compile(r"\[o(?:c)?w:([0-9a-f]{6,})\]") @dataclass @@ -111,7 +112,7 @@ def deliver(item, binding: InboxBinding, sender: Optional[Sender]) -> bool: channel message was sent.""" if not binding.channel or sender is None: return False - text = f"{item.title}\n{item.body}\n[ocw:{item.id}]".strip() + text = f"{item.title}\n{item.body}\n[ow:{item.id}]".strip() sender(binding.channel, binding.target, text) return True @@ -121,7 +122,7 @@ def resolve_from_reply( ) -> Optional[bool]: """Correlate an inbound channel reply to its item (by the embedded id) and resolve it. - Looks for the ``[ocw:]`` token and an allow/deny intent; falls back to treating the whole + Looks for the ``[ow:]`` token (or legacy ``[ocw:…]``) and an allow/deny intent; falls back to treating the whole message as a free-text answer. ``resolve(item_id, resolution)`` is the InboxStore.resolve. Returns the resolve() result, or None if no item id was found.""" m = _ID_TOKEN.search(reply or "") diff --git a/coworker/interactions.py b/coworker/interactions.py index 659d4c57..b8a93960 100644 --- a/coworker/interactions.py +++ b/coworker/interactions.py @@ -2,7 +2,7 @@ When an Inbox item is mirrored to a channel, discrete choices (approve/deny, an ask_user option) render as **buttons**. The item id rides in each button's value, so a click resolves the exact -item — no `[ocw:id]`-in-reply fragility, no thread tracking. Free-text answers aren't offered over +item — no `[ow:id]`-in-reply fragility, no thread tracking. Free-text answers aren't offered over messaging (the user opens the app for those). Provider-agnostic: a `Button` is `(label, value)`; each adapter renders it natively (Slack Block diff --git a/coworker/mentions.py b/coworker/mentions.py index b7a6d360..cd4a8717 100644 --- a/coworker/mentions.py +++ b/coworker/mentions.py @@ -1,6 +1,6 @@ """Mention-thread → session map for the Slack mention router (UX-DECISIONS §31). -When @ocw is tagged in a channel with no subscribed session, the router spawns a +When @OpenWorker is tagged in a channel with no subscribed session, the router spawns a coworker session that OWNS that thread and replies into it. This store is the dedupe map: one durable record per thread, keyed by the thread target string (``"slack:C0123:1700….000100"``; relay: ``"slack:T…/C…:ts"``) — byte-identical to diff --git a/coworker/subscriptions.py b/coworker/subscriptions.py index 4a286553..f8b5d33a 100644 --- a/coworker/subscriptions.py +++ b/coworker/subscriptions.py @@ -10,7 +10,7 @@ busy→steer / idle→background-turn path as self-wake — no live socket requi gateway's `format_target` / `parse_target`. NOTE: this is *not* Inbox routing. Routing mirrors an agent's approvals/questions OUT to a -DM/channel (request↔reply, `[ocw:id]`-correlated); a subscription brings a channel's messages IN +DM/channel (request↔reply, `[ow:id]`-correlated); a subscription brings a channel's messages IN (broadcast). Keep them on different channels — pointing your Inbox at a channel you also subscribe to conflates the two directions. """ diff --git a/surfaces/gui/src/api.ts b/surfaces/gui/src/api.ts index b1589719..842d7082 100644 --- a/surfaces/gui/src/api.ts +++ b/surfaces/gui/src/api.ts @@ -1472,7 +1472,7 @@ export interface SlackMember { } // One channel from the workspace roster. Private channels appear only where the -// bot is a member (Slack API constraint); is_member=false → "invite @ocw" hint. +// bot is a member (Slack API constraint); is_member=false → "invite @OpenWorker" hint. export interface SlackChannelEntry { id: string; name: string; diff --git a/surfaces/gui/src/components/AutomationQuickstart.tsx b/surfaces/gui/src/components/AutomationQuickstart.tsx index 6ff846be..e7ac533e 100644 --- a/surfaces/gui/src/components/AutomationQuickstart.tsx +++ b/surfaces/gui/src/components/AutomationQuickstart.tsx @@ -495,7 +495,7 @@ export function AutomationQuickstart({ />

- The bot must be a member of the channel — invite @ocw in Slack if it isn't. + The bot must be a member of the channel — invite @OpenWorker in Slack if it isn't.

)} diff --git a/tests/test_inbox_routing.py b/tests/test_inbox_routing.py index 6790cb00..fb3377ed 100644 --- a/tests/test_inbox_routing.py +++ b/tests/test_inbox_routing.py @@ -46,7 +46,7 @@ def test_deliver_to_channel_embeds_item_id(tmp_path): assert deliver(item, routing.binding_for("ops"), sender) is True assert sent["channel"] == "slack" and sent["target"] == "#ops" - assert f"[ocw:{item.id}]" in sent["text"] + assert f"[ow:{item.id}]" in sent["text"] # rebrand: emits [ow:…] since 2026-07-22 def test_in_app_only_binding_delivers_nothing(tmp_path): @@ -64,8 +64,8 @@ def test_in_app_only_binding_delivers_nothing(tmp_path): def test_inbound_reply_resolves_correct_item(tmp_path): store = InboxStore(tmp_path / "inbox.json") item = store.add_approval("s1", "Deploy?", inbox="ops") - # An inbound "approve [ocw:]" resolves exactly that item. - ok = resolve_from_reply(f"approve [ocw:{item.id}]", store.resolve) + # Current token spelling… + ok = resolve_from_reply(f"approve [ow:{item.id}]", store.resolve) assert ok is True assert store.get(item.id).resolution == "allow" @@ -73,10 +73,18 @@ def test_inbound_reply_resolves_correct_item(tmp_path): def test_inbound_freetext_answer_to_question(tmp_path): store = InboxStore(tmp_path / "inbox.json") q = store.add_question("s1", "Which region?") - res = resolve_from_reply(f"us-east-1 [ocw:{q.id}]", store.resolve) + res = resolve_from_reply(f"us-east-1 [ow:{q.id}]", store.resolve) assert res is True and store.get(q.id).resolution == "us-east-1" def test_reply_without_token_is_ignored(tmp_path): store = InboxStore(tmp_path / "inbox.json") assert resolve_from_reply("random chatter", store.resolve) is None + + +def test_inbound_legacy_ocw_token_still_resolves(tmp_path): + """Replies to messages sent BEFORE the @OpenWorker rename carry [ocw:…] — must keep working.""" + store = InboxStore(tmp_path / "inbox.json") + item = store.add_approval("s1", "Deploy?", inbox="ops") + assert resolve_from_reply(f"deny [ocw:{item.id}]", store.resolve) is True + assert store.get(item.id).resolution == "deny" diff --git a/tests/test_send_target_resolution.py b/tests/test_send_target_resolution.py index 6166484f..645f27a0 100644 --- a/tests/test_send_target_resolution.py +++ b/tests/test_send_target_resolution.py @@ -173,7 +173,7 @@ def test_unknown_ambiguous_and_not_member_names_error_actionably(tmp_path, monke }, ) secrets.delete("slack:team:T2") - assert "invite @ocw" in tool("slack:#private-ops", "Hi")["error"] + assert "invite @OpenWorker" in tool("slack:#private-ops", "Hi")["error"] def test_send_file_resolves_names_too(tmp_path, monkeypatch):