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.
This commit is contained in:
Rohit C Prasad
2026-07-23 07:43:24 -07:00
committed by Rohit P
parent 1032e3a664
commit 71b0df8ea2
13 changed files with 30 additions and 21 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ class Gateway:
) )
self._handler = handler self._handler = handler
# Tried before the handler: if an inbound message is an Inbox reply (carries an # Tried before the handler: if an inbound message is an Inbox reply (carries an
# [ocw:<id>] token), it resolves the item and is consumed — not routed as a new turn. # [ow:<id>] token), it resolves the item and is consumed — not routed as a new turn.
self._reply_resolver = reply_resolver self._reply_resolver = reply_resolver
# A button click on an interactive prompt (resolves an Inbox item by id). # A button click on an interactive prompt (resolves an Inbox item by id).
self._interaction_handler = interaction_handler self._interaction_handler = interaction_handler
+1 -1
View File
@@ -329,7 +329,7 @@ class SlackRelayAdapter(BasePlatformAdapter):
return return
channel = mapped.source.chat_id # bare channel id before qualification channel = mapped.source.chat_id # bare channel id before qualification
# Resolve friendly names with THIS workspace's bot token (cached per team), # 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. # not raw U…/C… ids. Best-effort: ids fall through on failure.
if not mapped.source.user_name: if not mapped.source.user_name:
mapped.source.user_name = await self._display_name( mapped.source.user_name = await self._display_name(
+1 -1
View File
@@ -82,7 +82,7 @@ def _send_slack(
return SendResult(True, message_id=data.get("ts")) return SendResult(True, message_id=data.get("ts"))
err = data.get("error") or "slack send failed" err = data.get("error") or "slack send failed"
if err == "not_in_channel": 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) return SendResult(False, error=err)
+2 -2
View File
@@ -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. 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 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 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 from __future__ import annotations
@@ -157,7 +157,7 @@ def list_channels(
refresh: bool = False, refresh: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Channels the token can see: all public ones, private only where the bot """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) token = _bot_token(secrets, team_id)
if not token: if not token:
return {"ok": False, "error": "workspace not connected"} return {"ok": False, "error": "workspace not connected"}
+1 -1
View File
@@ -106,7 +106,7 @@ def _resolve_slack_channel(
chat_id = str(c["id"]) if team == "default" else f"{team}/{c['id']}" chat_id = str(c["id"]) if team == "default" else f"{team}/{c['id']}"
if not c.get("is_member"): if not c.get("is_member"):
return None, ( 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" "in Slack, then retry"
) )
return chat_id, None return chat_id, None
+6 -5
View File
@@ -19,9 +19,10 @@ from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional
DEFAULT_INBOX = "default" DEFAULT_INBOX = "default"
_ID_TOKEN = re.compile( # Embeds the item id in a delivered message. Emitted as [ow:…] since the bot's rebrand
r"\[ocw:([0-9a-f]{6,})\]" # to OpenWorker (2026-07-22); the legacy [ocw:…] spelling stays parseable so replies to
) # embeds the item id in a delivered message # messages sent before the rename still resolve.
_ID_TOKEN = re.compile(r"\[o(?:c)?w:([0-9a-f]{6,})\]")
@dataclass @dataclass
@@ -111,7 +112,7 @@ def deliver(item, binding: InboxBinding, sender: Optional[Sender]) -> bool:
channel message was sent.""" channel message was sent."""
if not binding.channel or sender is None: if not binding.channel or sender is None:
return False 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) sender(binding.channel, binding.target, text)
return True return True
@@ -121,7 +122,7 @@ def resolve_from_reply(
) -> Optional[bool]: ) -> Optional[bool]:
"""Correlate an inbound channel reply to its item (by the embedded id) and resolve it. """Correlate an inbound channel reply to its item (by the embedded id) and resolve it.
Looks for the ``[ocw:<id>]`` token and an allow/deny intent; falls back to treating the whole Looks for the ``[ow:<id>]`` 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. 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.""" Returns the resolve() result, or None if no item id was found."""
m = _ID_TOKEN.search(reply or "") m = _ID_TOKEN.search(reply or "")
+1 -1
View File
@@ -2,7 +2,7 @@
When an Inbox item is mirrored to a channel, discrete choices (approve/deny, an ask_user option) 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 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). messaging (the user opens the app for those).
Provider-agnostic: a `Button` is `(label, value)`; each adapter renders it natively (Slack Block Provider-agnostic: a `Button` is `(label, value)`; each adapter renders it natively (Slack Block
+1 -1
View File
@@ -1,6 +1,6 @@
"""Mention-thread → session map for the Slack mention router (UX-DECISIONS §31). """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 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 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 (``"slack:C0123:1700….000100"``; relay: ``"slack:T…/C…:ts"``) byte-identical to
+1 -1
View File
@@ -10,7 +10,7 @@ busy→steer / idle→background-turn path as self-wake — no live socket requi
gateway's `format_target` / `parse_target`. gateway's `format_target` / `parse_target`.
NOTE: this is *not* Inbox routing. Routing mirrors an agent's approvals/questions OUT to a NOTE: this is *not* Inbox routing. Routing mirrors an agent's approvals/questions OUT to a
DM/channel (requestreply, `[ocw:id]`-correlated); a subscription brings a channel's messages IN DM/channel (requestreply, `[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 (broadcast). Keep them on different channels pointing your Inbox at a channel you also subscribe
to conflates the two directions. to conflates the two directions.
""" """
+1 -1
View File
@@ -1472,7 +1472,7 @@ export interface SlackMember {
} }
// One channel from the workspace roster. Private channels appear only where the // 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 { export interface SlackChannelEntry {
id: string; id: string;
name: string; name: string;
@@ -495,7 +495,7 @@ export function AutomationQuickstart({
/> />
</div> </div>
<p className="text-[11px] text-warnInk mt-1"> <p className="text-[11px] text-warnInk mt-1">
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.
</p> </p>
</> </>
)} )}
+12 -4
View File
@@ -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 deliver(item, routing.binding_for("ops"), sender) is True
assert sent["channel"] == "slack" and sent["target"] == "#ops" 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): 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): def test_inbound_reply_resolves_correct_item(tmp_path):
store = InboxStore(tmp_path / "inbox.json") store = InboxStore(tmp_path / "inbox.json")
item = store.add_approval("s1", "Deploy?", inbox="ops") item = store.add_approval("s1", "Deploy?", inbox="ops")
# An inbound "approve [ocw:<id>]" resolves exactly that item. # Current token spelling…
ok = resolve_from_reply(f"approve [ocw:{item.id}]", store.resolve) ok = resolve_from_reply(f"approve [ow:{item.id}]", store.resolve)
assert ok is True assert ok is True
assert store.get(item.id).resolution == "allow" 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): def test_inbound_freetext_answer_to_question(tmp_path):
store = InboxStore(tmp_path / "inbox.json") store = InboxStore(tmp_path / "inbox.json")
q = store.add_question("s1", "Which region?") 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" assert res is True and store.get(q.id).resolution == "us-east-1"
def test_reply_without_token_is_ignored(tmp_path): def test_reply_without_token_is_ignored(tmp_path):
store = InboxStore(tmp_path / "inbox.json") store = InboxStore(tmp_path / "inbox.json")
assert resolve_from_reply("random chatter", store.resolve) is None 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"
+1 -1
View File
@@ -173,7 +173,7 @@ def test_unknown_ambiguous_and_not_member_names_error_actionably(tmp_path, monke
}, },
) )
secrets.delete("slack:team:T2") 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): def test_send_file_resolves_names_too(tmp_path, monkeypatch):