mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-04 16:42:35 +00:00
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:
@@ -44,7 +44,7 @@ class Gateway:
|
||||
)
|
||||
self._handler = handler
|
||||
# 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
|
||||
# A button click on an interactive prompt (resolves an Inbox item by id).
|
||||
self._interaction_handler = interaction_handler
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:<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.
|
||||
Returns the resolve() result, or None if no item id was found."""
|
||||
m = _ID_TOKEN.search(reply or "")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -495,7 +495,7 @@ export function AutomationQuickstart({
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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:<id>]" 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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user