diff --git a/README.md b/README.md index df30c74..41f4900 100644 --- a/README.md +++ b/README.md @@ -68,10 +68,15 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.shre.pulse-bridge.pli only (the first `@handle` in a message that matches a workspace agent triggers an AI reply). There is no user mention/notification hook, so needs-you escalations are the literal text `@nir NEEDS YOU: …`. -- **Mention trap defence:** all ledger-derived text has `@` neutralised - with a zero-width space so an `@ellie` in an item detail can never - trigger an AI reply in the feed. Only the bridge's own `@nir` (which - matches no agent, hence inert) is emitted raw. +- **Mention path is broken on this instance (found during smoke):** ANY + `@handle` in a message makes `POST …/messages` **500 after the row is + inserted** — the mention agent-lookup queries a nonexistent `url_key` + column on `agents` (`comms.ts` ~line 226; error in mib007 stderr log). + Retrying such a failure duplicates the message. The bridge therefore + (a) zwsp-neutralises **every** `@` it emits, including its own + `@nir` escalation (renders identically in the UI), and (b) treats an + HTTP error on a *comment* as non-retryable (`DROP … comment not + retried` in the log) — comments are best-effort; posts still retry. - Updates for items that pre-date the bridge and were never seeded (closed before first run) are skipped with a log line — there is no post to comment on. diff --git a/bridge.py b/bridge.py index dc73fb9..1fe9cf4 100644 --- a/bridge.py +++ b/bridge.py @@ -115,8 +115,11 @@ def neutralize(text: str) -> str: """Insert a zero-width space after '@' in ledger-derived text. mib007's comms route treats the FIRST '@handle' in a message as an agent - mention and fires an AI reply into the thread (e.g. @ellie). Ledger text - must never trigger that; only the bridge's own deliberate mention may. + mention: a matching agent fires an AI reply into the thread, and on this + instance ANY mention 500s after the insert (the agent lookup references a + nonexistent url_key column). So no bridge-emitted text may ever contain a + bare '@handle' — including the bridge's own needs-you escalation, which is + emitted zwsp-neutralised and renders identically in the UI. """ return (text or "").replace("@", "@​") @@ -151,8 +154,8 @@ def update_body(rec: dict) -> str: stage = rec.get("stage") note = neutralize(rec.get("note", "")) if kind == "needs-you": - lines.append(f"@{MENTION} NEEDS YOU: this item is now waiting on a human." - + (f" — {note}" if note else "")) + lines.append(neutralize(f"@{MENTION}") + " NEEDS YOU: this item is now" + " waiting on a human." + (f" — {note}" if note else "")) note = "" # already included elif kind == "failed": where = f" at {stage}" if stage else "" @@ -249,7 +252,14 @@ def comment(state: dict, ledger_id: str, content: str, what: str) -> None: if not post_id: log(f"SKIP {what} {ledger_id[:8]}: no mapped post (predates bridge)") return - msg = send_message(ensure_channel(state), content, thread_id=post_id) + try: + msg = send_message(ensure_channel(state), content, thread_id=post_id) + except urllib.error.HTTPError as e: + # mib007 comms can 500 AFTER the insert (proven: mention-path crash), + # so retrying a failed comment risks duplicates. Comments are + # best-effort: log and move on instead of blocking the checkpoint. + log(f"DROP {what} {ledger_id[:8]}: HTTP {e.code} (comment not retried)") + return log(f"{what:6} {ledger_id[:8]} -> comment {msg['id']}")