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
|
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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 "")
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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 (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
|
(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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user