(turn: IncomingTurn)
| 373 | function makeSink(adapter: PlatformAdapter): IngressSink { |
| 374 | return { |
| 375 | async onTurn(turn: IncomingTurn) { |
| 376 | const lockKey = `turn:${turn.conversationKey}`; |
| 377 | const acquired = await backend.lock.acquire(lockKey, { |
| 378 | ttlMs: cfg.lockTtl ?? 60_000, |
| 379 | }); |
| 380 | |
| 381 | if (!acquired) { |
| 382 | const decision = |
| 383 | typeof cfg.onLockConflict === "function" |
| 384 | ? await cfg.onLockConflict( |
| 385 | turn.conversationKey, |
| 386 | msgFromTurn(turn), |
| 387 | ) |
| 388 | : (cfg.onLockConflict ?? "drop"); |
| 389 | if (decision === "drop") return; // discard overlapping turn |
| 390 | // "force": proceed WITHOUT a lock token. Does NOT cancel the |
| 391 | // in-flight handler — cooperative cancellation is a future extension. |
| 392 | } |
| 393 | |
| 394 | try { |
| 395 | // Dedup AFTER acquiring the lock: a turn dropped on lock-conflict must NOT burn its |
| 396 | // eventId, so Slack's retry can still be processed once the lock frees. (A handler |
| 397 | // that throws still leaves its event marked seen — dedup drops duplicate DELIVERIES, |
| 398 | // it is not retry-of-failed-turns.) |
| 399 | if (turn.eventId) { |
| 400 | const dupKey = `evt:${adapter.platform}:${turn.eventId}`; |
| 401 | try { |
| 402 | if (await backend.dedup.seen(dupKey, cfg.dedupTtl ?? 300_000)) |
| 403 | return; |
| 404 | } catch (err) { |
| 405 | console.warn( |
| 406 | `[bot] dedup check failed for ${adapter.platform}; processing without dedup`, |
| 407 | err, |
| 408 | ); |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | // Resolve cross-platform identity key (if configured) and stamp it on |
| 413 | // the message so handlers and transcript storage can use it. Done |
| 414 | // BEFORE makeThread so the thread carries the userKey + message for |
| 415 | // the transcript auto-bridge (runAgent({ transcript: true })). |
| 416 | let userKey: string | undefined; |
| 417 | if (cfg.identity) { |
| 418 | try { |
| 419 | const resolved = await cfg.identity({ |
| 420 | adapter: adapter.platform, |
| 421 | author: turn.user ?? { id: "" }, |
| 422 | message: msgFromTurn(turn), |
| 423 | }); |
| 424 | userKey = resolved ?? undefined; |
| 425 | } catch (err) { |
| 426 | console.warn( |
| 427 | `[bot] identity resolution failed for ${adapter.platform}; continuing without userKey`, |
| 428 | err, |
| 429 | ); |
| 430 | } |
| 431 | } |
| 432 | const message: IncomingMessage = { ...msgFromTurn(turn), userKey }; |
no test coverage detected
searching dependent graphs…