harden against mib007 mention-path 500 (insert-then-error)
Smoke test found: any @handle in a comms message 500s AFTER the row is inserted (agents.url_key column missing on this instance), so a retry duplicates the message. Neutralise ALL '@' the bridge emits (incl. its own @nir escalation, zwsp renders identically) and make comment HTTP errors non-retryable best-effort drops; posts still retry. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YECpkAwQUwgu7NVy91R8fW
This commit is contained in:
co-authored by
Claude Fable 5
parent
9a862bee2b
commit
1a3ab878b4
@@ -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
|
only (the first `@handle` in a message that matches a workspace agent
|
||||||
triggers an AI reply). There is no user mention/notification hook, so
|
triggers an AI reply). There is no user mention/notification hook, so
|
||||||
needs-you escalations are the literal text `@nir NEEDS YOU: …`.
|
needs-you escalations are the literal text `@nir NEEDS YOU: …`.
|
||||||
- **Mention trap defence:** all ledger-derived text has `@` neutralised
|
- **Mention path is broken on this instance (found during smoke):** ANY
|
||||||
with a zero-width space so an `@ellie` in an item detail can never
|
`@handle` in a message makes `POST …/messages` **500 after the row is
|
||||||
trigger an AI reply in the feed. Only the bridge's own `@nir` (which
|
inserted** — the mention agent-lookup queries a nonexistent `url_key`
|
||||||
matches no agent, hence inert) is emitted raw.
|
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
|
- 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
|
(closed before first run) are skipped with a log line — there is no
|
||||||
post to comment on.
|
post to comment on.
|
||||||
|
|||||||
@@ -115,8 +115,11 @@ def neutralize(text: str) -> str:
|
|||||||
"""Insert a zero-width space after '@' in ledger-derived text.
|
"""Insert a zero-width space after '@' in ledger-derived text.
|
||||||
|
|
||||||
mib007's comms route treats the FIRST '@handle' in a message as an agent
|
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
|
mention: a matching agent fires an AI reply into the thread, and on this
|
||||||
must never trigger that; only the bridge's own deliberate mention may.
|
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("@", "@")
|
return (text or "").replace("@", "@")
|
||||||
|
|
||||||
@@ -151,8 +154,8 @@ def update_body(rec: dict) -> str:
|
|||||||
stage = rec.get("stage")
|
stage = rec.get("stage")
|
||||||
note = neutralize(rec.get("note", ""))
|
note = neutralize(rec.get("note", ""))
|
||||||
if kind == "needs-you":
|
if kind == "needs-you":
|
||||||
lines.append(f"@{MENTION} NEEDS YOU: this item is now waiting on a human."
|
lines.append(neutralize(f"@{MENTION}") + " NEEDS YOU: this item is now"
|
||||||
+ (f" — {note}" if note else ""))
|
" waiting on a human." + (f" — {note}" if note else ""))
|
||||||
note = "" # already included
|
note = "" # already included
|
||||||
elif kind == "failed":
|
elif kind == "failed":
|
||||||
where = f" at {stage}" if stage else ""
|
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:
|
if not post_id:
|
||||||
log(f"SKIP {what} {ledger_id[:8]}: no mapped post (predates bridge)")
|
log(f"SKIP {what} {ledger_id[:8]}: no mapped post (predates bridge)")
|
||||||
return
|
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']}")
|
log(f"{what:6} {ledger_id[:8]} -> comment {msg['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user