Follow-ups deliberately left open by #150 (the Slack ticket mirror). All three were raised by the CR rounds on that PR and verified; none blocks the mirror shipping inert.
1. A duplicate reply job re-posts its message
handleSlackMirror makes the TicketExternalLink row the idempotent identity of the thread, so two jobs can no longer open rival threads. It does not dedupe reply posts: if a reply job is retried after its postMessage succeeded (a job timeout that does not cancel the handler, a throw in reportProgress, a re-thrown transient error), the same message is posted into the thread again.
Fixing this needs a per-message marker — e.g. a mirroredAt column on Message, or a metadata entry on the link row keyed by message id — checked before posting. That is a schema decision, which is why it was not smuggled into #150.
packages/outpost/queue/src/handlers/slack-mirror.ts — the idempotency docblock states this limitation explicitly.
2. truncate splits surrogate pairs
value.slice(0, max - 1) can cut an astral character (emoji, many CJK extension chars) in half, producing a lone surrogate in the Slack payload. Use Array.from / Intl.Segmenter or a code-point-aware slice.
3. A new WebClient per job
buildPoster constructs a fresh @slack/web-api client on every job. At mirror volumes this is minor, but it discards connection reuse and the client's rate-limit state, which is precisely the state you want shared across posts. Cache one client per token.
Found by: CR rounds 1-2 on #150.
Follow-ups deliberately left open by #150 (the Slack ticket mirror). All three were raised by the CR rounds on that PR and verified; none blocks the mirror shipping inert.
1. A duplicate reply job re-posts its message
handleSlackMirrormakes theTicketExternalLinkrow the idempotent identity of the thread, so two jobs can no longer open rival threads. It does not dedupe reply posts: if areplyjob is retried after itspostMessagesucceeded (a job timeout that does not cancel the handler, a throw inreportProgress, a re-thrown transient error), the same message is posted into the thread again.Fixing this needs a per-message marker — e.g. a
mirroredAtcolumn onMessage, or ametadataentry on the link row keyed by message id — checked before posting. That is a schema decision, which is why it was not smuggled into #150.packages/outpost/queue/src/handlers/slack-mirror.ts— the idempotency docblock states this limitation explicitly.2.
truncatesplits surrogate pairsvalue.slice(0, max - 1)can cut an astral character (emoji, many CJK extension chars) in half, producing a lone surrogate in the Slack payload. UseArray.from/Intl.Segmenteror a code-point-aware slice.3. A new
WebClientper jobbuildPosterconstructs a fresh@slack/web-apiclient on every job. At mirror volumes this is minor, but it discards connection reuse and the client's rate-limit state, which is precisely the state you want shared across posts. Cache one client per token.Found by: CR rounds 1-2 on #150.