fd85e64fe3
## Context
Two assistant paths ended with no visible reaction for the member:
- a mention with no request text (a bare `@twenty`) or an empty DM was
skipped silently
- a follow-up in a thread whose 24-hour subscription window had lapsed
was dropped silently, with no hint that the bot "forgot" the thread
## What this does
**Empty requests get answered.** `parseSlackAssistantRequest` flags an
empty mention/DM with its channel and thread target, and the enqueue
function answers in-thread. A bare mention that starts a fresh thread
(or an empty DM) gets a short markdown list of example asks; a bare
mention inside an existing thread gets a context-aware one-liner
instead. There is no request record to dedupe on for these, so a KV
claim (`slack-empty-request-reply:{channel}:{ts}`, 1-hour expiry) keeps
Slack event redeliveries from double-posting, and the claim is released
if the hint cannot be posted so a redelivery can retry. The hint's
thread is subscribed like any answered thread, so a follow-up question
there is picked up without a re-mention. Empty messages in unmentioned
thread follow-ups still skip silently on purpose.
**Lapsed threads get a nudge.** `is-slack-thread-active.ts` becomes
`get-slack-thread-subscription-state.ts`, a pure read returning `active`
/ `expired` / `none`: an expired-but-present KV key proves the bot used
to follow that thread, while `none` means it never did, so random
threads stay untouched. On `expired` the author of the follow-up gets an
ephemeral (only they see it) asking them to re-mention the bot. The
lapsed key is cleared only after the nudge actually posted, via
`clearLapsedSlackThreadSubscription`, which re-checks that the
subscription was not renewed in the meantime; a failed nudge is retried
on the next follow-up.
Supporting changes: the `slack-post-ephemeral-message` step gains an
optional `parentMessageTimestamp` (type, schema, handler) so the nudge
can post inside the thread, mirroring `slack-post-message`; the enqueue
result shape is extracted into a shared `SlackEventsEnqueueResult` type;
nullish checks use `isDefined` from `twenty-sdk/utils`. README and
SETUP.md behaviour notes describe the new behaviours.
## Demo
https://github.com/user-attachments/assets/03add964-92fb-46e5-aa49-06faad85e18c
<img width="406" height="531" alt="Screenshot 2026-08-06 at 6 34 57 AM"
src="https://github.com/user-attachments/assets/bc001057-8998-4d1f-ac7e-e1ed8b5f3770"
/>