@ouro.bot/cli 0.1.0-alpha.729 → 0.1.0-alpha.730

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,6 +72,7 @@ const pending_1 = require("../../mind/pending");
72
72
  const prompt_1 = require("../../mind/prompt");
73
73
  const mcp_manager_1 = require("../../repertoire/mcp-manager");
74
74
  const runtime_1 = require("../../nerves/runtime");
75
+ const session_transaction_1 = require("../../mind/session-transaction");
75
76
  const orientation_frame_1 = require("../../heart/orientation-frame");
76
77
  const proactive_content_guard_1 = require("../proactive-content-guard");
77
78
  const bluebubbles_meta_guard_1 = require("../bluebubbles-meta-guard");
@@ -306,6 +307,7 @@ const defaultDeps = {
306
307
  bbFailoverStates.set(sessionKey, structuredClone(state));
307
308
  },
308
309
  recordProcessed: processed_log_1.recordProcessedBlueBubblesMessage,
310
+ withSessionTurnLease: session_transaction_1.withSessionTurnLease,
309
311
  };
310
312
  const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
311
313
  const BLUEBUBBLES_RECOVERY_PASS_DELAY_MS = 1_000;
@@ -1513,551 +1515,560 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
1513
1515
  const replyTarget = createReplyTargetController(event);
1514
1516
  const friendId = context.friend.id;
1515
1517
  const sessPath = resolvedDeps.sessionPath(friendId, "bluebubbles", event.chat.sessionKey);
1516
- try {
1517
- (0, session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
1518
- }
1519
- catch (error) {
1520
- (0, runtime_1.emitNervesEvent)({
1521
- level: "warn",
1522
- component: "senses",
1523
- event: "senses.bluebubbles_thread_lane_cleanup_error",
1524
- message: "failed to inspect obsolete bluebubbles thread-lane sessions",
1525
- meta: {
1526
- sessionPath: sessPath,
1527
- reason: error instanceof Error ? error.message : String(error),
1528
- },
1529
- });
1530
- }
1531
- // Pre-load session inside the canonical turn lock so same-chat
1532
- // deliveries cannot race on stale trunk state.
1533
- const existing = resolvedDeps.loadSession(sessPath);
1534
- const sessionMessages = existing?.messages && existing.messages.length > 0
1535
- ? structuredClone(existing.messages)
1536
- : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
1537
- // Record EARLY for audit and crash recovery. This is capture truth, not
1538
- // a claim that the agent turn completed successfully.
1539
- const inboundSource = (options.semanticRecovery || source !== "webhook")
1540
- && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
1541
- ? "recovery-bootstrap"
1542
- : source;
1543
- (0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, inboundSource);
1544
- if (inboundSource === "recovery-bootstrap") {
1545
- (0, runtime_1.emitNervesEvent)({
1546
- component: "senses",
1547
- event: "senses.bluebubbles_recovery_skip",
1548
- message: "skipped bluebubbles recovery because the session already contains the message text",
1549
- meta: {
1550
- messageGuid: event.messageGuid,
1551
- sessionKey: event.chat.sessionKey,
1552
- source,
1553
- },
1554
- });
1555
- (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, inboundSource, "session-bootstrap");
1556
- return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
1557
- }
1558
- // The inbound event is canonical truth as soon as its canonical lane is
1559
- // reserved. Persist it before any supersedable orientation/provider work.
1560
- const priorMessages = sessionMessages;
1561
- const userMessage = {
1562
- role: "user",
1563
- content: buildInboundContent(event),
1564
- };
1565
- resolvedDeps.saveSession(sessPath, [...structuredClone(sessionMessages), structuredClone(userMessage)], existing?.lastUsage, existing?.state);
1566
- if (!await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
1567
- || !(0, latest_turn_1.isCurrent)(options.latestTurnCapability)) {
1568
- return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
1569
- }
1570
- options.lifecycleSignal?.throwIfAborted();
1571
- const liveTurnId = (0, active_turns_1.beginBlueBubblesActiveTurn)(agentName, event);
1572
- activeTurnId = liveTurnId;
1573
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
1574
- if (event.chat.isGroup) {
1575
- await (0, friends_1.upsertGroupContextParticipants)({
1576
- store,
1577
- participants: event.chat.participantHandles.map((externalId) => ({
1578
- provider: "imessage-handle",
1579
- externalId,
1580
- })),
1581
- groupExternalId: resolveGroupExternalId(event),
1582
- });
1583
- }
1584
- // Fetch the text of the message being replied to (if this is a threaded reply)
1585
- const threadGuid = event.threadOriginatorGuid?.trim();
1586
- let repliedToText = null;
1587
- if (threadGuid) {
1588
- repliedToText = await client.getMessageText(threadGuid).catch(() => null);
1589
- (0, runtime_1.emitNervesEvent)({
1590
- component: "senses",
1591
- event: "senses.bluebubbles_reply_context",
1592
- message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
1593
- meta: { threadGuid, hasText: !!repliedToText },
1594
- });
1595
- }
1596
- // Shutdown can land during MCP/group/reply-context awaits above. Recheck
1597
- // before the remaining synchronous setup installs its lifecycle listener.
1598
- options.lifecycleSignal?.throwIfAborted();
1599
- const orientationFrame = (0, orientation_frame_1.buildOrientationFrame)({
1600
- channel: "bluebubbles",
1601
- messages: [...priorMessages, userMessage],
1602
- currentUserMessages: [userMessage],
1603
- structuredOutputs: existing?.structuredOutputs ?? [],
1604
- source: buildBlueBubblesOrientationSource(event, priorMessages, options.orientationEvidence, options.orientationConversationKind, repliedToText),
1605
- });
1606
- if (!(0, latest_turn_1.isCurrent)(options.latestTurnCapability)) {
1607
- return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
1608
- }
1609
- const turnStage = createBlueBubblesTurnStage(event.chat.sessionKey);
1610
- let finalTransportInvoked = false;
1611
- let finalTransportAccepted = false;
1612
- const outboundRuntimeConfig = (0, config_1.getBlueBubblesConfig)();
1613
- const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, () => (0, active_turns_1.noteBlueBubblesActiveTurnVisibleActivity)(agentName, liveTurnId), {
1614
- admitOutbound: async () => (!options.lifecycleSignal?.aborted
1615
- && await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
1616
- && !options.lifecycleSignal?.aborted),
1617
- isOutboundCurrent: () => (!options.lifecycleSignal?.aborted
1618
- && (0, latest_turn_1.isCurrent)(options.latestTurnCapability)),
1619
- isOutboundAdmittedNow: () => (!options.lifecycleSignal?.aborted
1620
- && (0, latest_turn_1.isDeliveryAdmittedNow)(options.latestTurnCapability)),
1621
- onFinalTransportInvoked: () => {
1622
- finalTransportInvoked = true;
1623
- },
1624
- onFinalTransportAccepted: () => {
1625
- finalTransportAccepted = true;
1626
- },
1627
- durableOutbound: {
1628
- agentRoot: (0, identity_1.getAgentRoot)(),
1629
- idempotencyKey: blueBubblesInboundReplyIdempotencyKey(options.currentIngressEvidence.captureKeyHash),
1630
- attachment: {
1631
- serverUrl: outboundRuntimeConfig.serverUrl,
1632
- accountId: outboundRuntimeConfig.accountId,
1633
- },
1634
- },
1635
- });
1636
- const controller = new AbortController();
1637
- const lifecycleSignal = options.lifecycleSignal;
1638
- let rejectLifecycle;
1639
- const lifecyclePromise = new Promise((_resolve, reject) => {
1640
- rejectLifecycle = reject;
1641
- });
1642
- let resolveSupersession;
1643
- const supersessionPromise = new Promise((resolve) => {
1644
- resolveSupersession = resolve;
1645
- });
1646
- const cancelForSupersession = () => {
1647
- callbacks.cancelOutbound("superseded");
1648
- if (!finalTransportInvoked) {
1649
- controller.abort(options.latestTurnCapability.signal.reason);
1650
- resolveSupersession({
1651
- handled: true,
1652
- notifiedAgent: false,
1653
- kind: event.kind,
1654
- reason: "superseded",
1655
- });
1656
- }
1657
- };
1658
- options.latestTurnCapability.signal.addEventListener("abort", cancelForSupersession, { once: true });
1659
- const cancelForShutdown = () => {
1660
- callbacks.cancelOutbound("shutdown");
1661
- if (!finalTransportInvoked) {
1662
- const reason = lifecycleSignal.reason;
1663
- controller.abort(reason);
1664
- rejectLifecycle(reason);
1665
- }
1666
- };
1667
- lifecycleSignal?.addEventListener("abort", cancelForShutdown, { once: true });
1668
- let timeoutTimer;
1669
- let timeoutPromise;
1670
- let resolveTimeout;
1671
- let recoveryTimedOut = false;
1672
- // BB-specific tool context wrappers
1673
- const summarize = (0, core_1.createSummarize)("human");
1674
- const bbCapabilities = (0, friends_1.getChannelCapabilities)("bluebubbles");
1675
- const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
1676
- // Buffer terminal errors so failover can suppress them.
1677
- // If failover produces a message, the buffered error is skipped.
1678
- // If failover doesn't fire, the buffered error is replayed.
1679
- let bufferedTerminalError = null;
1680
- /* v8 ignore start -- failover-aware error buffering @preserve */
1681
- const failoverAwareCallbacks = {
1682
- ...callbacks,
1683
- onError(error, severity) {
1684
- if (severity === "terminal") {
1685
- bufferedTerminalError = error;
1686
- return;
1687
- }
1688
- callbacks.onError(error, severity);
1689
- },
1690
- };
1691
- /* v8 ignore stop */
1692
- try {
1693
- // ── Compute trust gate context for group/acquaintance rules ─────
1694
- // These store reads can block on filesystem I/O. Race them against the
1695
- // runtime lifecycle before constructing the shared pipeline so a closed
1696
- // worker cannot begin new durable or outward work after the read settles.
1697
- const trustContext = await Promise.race([
1698
- Promise.all([
1699
- checkGroupHasFamilyMember(store, event),
1700
- event.chat.isGroup
1701
- ? Promise.resolve(false)
1702
- : checkHasExistingGroupWithFamily(store, context.friend),
1703
- ]),
1704
- lifecyclePromise,
1705
- supersessionPromise.then(() => null),
1706
- ]);
1707
- if (trustContext === null) {
1708
- return {
1709
- handled: true,
1710
- notifiedAgent: false,
1711
- kind: event.kind,
1712
- reason: "superseded",
1713
- };
1518
+ return await resolvedDeps.withSessionTurnLease(sessPath, async (sessionTurnLease) => {
1519
+ try {
1520
+ (0, session_cleanup_1.findObsoleteBlueBubblesThreadSessions)(sessPath);
1714
1521
  }
1715
- const [groupHasFamilyMember, hasExistingGroupWithFamily] = trustContext;
1716
- lifecycleSignal?.throwIfAborted();
1717
- // ── Call shared pipeline ────────────────────────────────────────
1718
- const timeoutMs = options.timeoutMs ?? BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS;
1719
- timeoutPromise = new Promise((resolve) => {
1720
- resolveTimeout = resolve;
1721
- });
1722
- timeoutTimer = setTimeout(() => {
1723
- const reason = new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
1724
- recoveryTimedOut = true;
1725
- if (!finalTransportInvoked) {
1726
- resolveTimeout?.({
1727
- handled: true,
1728
- notifiedAgent: false,
1729
- kind: event.kind,
1730
- reason: "turn_timeout",
1731
- });
1732
- }
1733
- callbacks.cancelOutbound("turn_timeout");
1734
- (0, latest_turn_1.cancel)(options.latestTurnCapability, "turn_timeout");
1735
- if (!finalTransportInvoked)
1736
- controller.abort(reason);
1522
+ catch (error) {
1737
1523
  (0, runtime_1.emitNervesEvent)({
1738
1524
  level: "warn",
1739
1525
  component: "senses",
1740
- event: "senses.bluebubbles_turn_timeout",
1741
- message: "bluebubbles turn timed out",
1526
+ event: "senses.bluebubbles_thread_lane_cleanup_error",
1527
+ message: "failed to inspect obsolete bluebubbles thread-lane sessions",
1742
1528
  meta: {
1743
- messageGuid: event.messageGuid,
1744
- sessionKey: event.chat.sessionKey,
1745
- source,
1746
- timeoutMs,
1529
+ sessionPath: sessPath,
1530
+ reason: error instanceof Error ? error.message : String(error),
1747
1531
  },
1748
1532
  });
1533
+ }
1534
+ // Pre-load session inside the canonical turn lock so same-chat
1535
+ // deliveries cannot race on stale trunk state.
1536
+ const existing = resolvedDeps.loadSession(sessPath);
1537
+ const sessionMessages = existing?.messages && existing.messages.length > 0
1538
+ ? structuredClone(existing.messages)
1539
+ : [{ role: "system", content: (0, prompt_1.flattenSystemPrompt)(await resolvedDeps.buildSystem("bluebubbles", {}, context)) }];
1540
+ // Record EARLY for audit and crash recovery. This is capture truth, not
1541
+ // a claim that the agent turn completed successfully.
1542
+ const inboundSource = (options.semanticRecovery || source !== "webhook")
1543
+ && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)
1544
+ ? "recovery-bootstrap"
1545
+ : source;
1546
+ (0, inbound_log_1.recordBlueBubblesInbound)(agentName, event, inboundSource);
1547
+ if (inboundSource === "recovery-bootstrap") {
1749
1548
  (0, runtime_1.emitNervesEvent)({
1750
- level: "warn",
1751
1549
  component: "senses",
1752
- event: "senses.bluebubbles_timeout_notice_suppressed",
1753
- message: "bluebubbles timeout notice suppressed from iMessage",
1550
+ event: "senses.bluebubbles_recovery_skip",
1551
+ message: "skipped bluebubbles recovery because the session already contains the message text",
1754
1552
  meta: {
1755
1553
  messageGuid: event.messageGuid,
1756
1554
  sessionKey: event.chat.sessionKey,
1757
1555
  source,
1758
- timeoutMs,
1759
1556
  },
1760
1557
  });
1761
- }, timeoutMs);
1762
- /* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
1763
- if (typeof timeoutTimer.unref === "function") {
1764
- timeoutTimer.unref();
1558
+ (0, processed_log_1.recordProcessedBlueBubblesMessage)(agentName, event, inboundSource, "session-bootstrap");
1559
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
1765
1560
  }
1766
- let preparedBlueBubblesContext = {
1767
- messages: [],
1768
- contextPacketIds: [],
1561
+ // The inbound event is canonical truth as soon as its canonical lane is
1562
+ // reserved. Persist it before any supersedable orientation/provider work.
1563
+ const priorMessages = sessionMessages;
1564
+ const userMessage = {
1565
+ role: "user",
1566
+ content: buildInboundContent(event),
1769
1567
  };
1770
- const turnPromise = (0, pipeline_1.handleInboundTurn)({
1568
+ resolvedDeps.saveSession(sessPath, [...structuredClone(sessionMessages), structuredClone(userMessage)], existing?.lastUsage, existing?.state);
1569
+ if (!await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
1570
+ || !(0, latest_turn_1.isCurrent)(options.latestTurnCapability)) {
1571
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
1572
+ }
1573
+ options.lifecycleSignal?.throwIfAborted();
1574
+ const liveTurnId = (0, active_turns_1.beginBlueBubblesActiveTurn)(agentName, event);
1575
+ activeTurnId = liveTurnId;
1576
+ const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
1577
+ if (event.chat.isGroup) {
1578
+ await (0, friends_1.upsertGroupContextParticipants)({
1579
+ store,
1580
+ participants: event.chat.participantHandles.map((externalId) => ({
1581
+ provider: "imessage-handle",
1582
+ externalId,
1583
+ })),
1584
+ groupExternalId: resolveGroupExternalId(event),
1585
+ });
1586
+ }
1587
+ // Fetch the text of the message being replied to (if this is a threaded reply)
1588
+ const threadGuid = event.threadOriginatorGuid?.trim();
1589
+ let repliedToText = null;
1590
+ if (threadGuid) {
1591
+ repliedToText = await client.getMessageText(threadGuid).catch(() => null);
1592
+ (0, runtime_1.emitNervesEvent)({
1593
+ component: "senses",
1594
+ event: "senses.bluebubbles_reply_context",
1595
+ message: repliedToText ? "fetched replied-to message text" : "could not fetch replied-to message text",
1596
+ meta: { threadGuid, hasText: !!repliedToText },
1597
+ });
1598
+ }
1599
+ // Shutdown can land during MCP/group/reply-context awaits above. Recheck
1600
+ // before the remaining synchronous setup installs its lifecycle listener.
1601
+ options.lifecycleSignal?.throwIfAborted();
1602
+ const orientationFrame = (0, orientation_frame_1.buildOrientationFrame)({
1771
1603
  channel: "bluebubbles",
1772
- sessionKey: event.chat.sessionKey,
1773
- capabilities: bbCapabilities,
1774
- messages: [userMessage],
1775
- continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
1776
- friendResolver: { resolve: () => Promise.resolve(context) },
1777
- sessionLoader: {
1778
- loadOrCreate: () => Promise.resolve({
1779
- messages: sessionMessages,
1780
- sessionPath: sessPath,
1781
- state: existing?.state,
1782
- events: existing?.events,
1783
- structuredOutputs: existing?.structuredOutputs,
1784
- }),
1604
+ messages: [...priorMessages, userMessage],
1605
+ currentUserMessages: [userMessage],
1606
+ structuredOutputs: existing?.structuredOutputs ?? [],
1607
+ source: buildBlueBubblesOrientationSource(event, priorMessages, options.orientationEvidence, options.orientationConversationKind, repliedToText),
1608
+ });
1609
+ if (!(0, latest_turn_1.isCurrent)(options.latestTurnCapability)) {
1610
+ return { handled: true, notifiedAgent: false, kind: event.kind, reason: "superseded" };
1611
+ }
1612
+ const turnStage = createBlueBubblesTurnStage(event.chat.sessionKey);
1613
+ let finalTransportInvoked = false;
1614
+ let finalTransportAccepted = false;
1615
+ const outboundRuntimeConfig = (0, config_1.getBlueBubblesConfig)();
1616
+ const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, () => (0, active_turns_1.noteBlueBubblesActiveTurnVisibleActivity)(agentName, liveTurnId), {
1617
+ admitOutbound: async () => (!options.lifecycleSignal?.aborted
1618
+ && await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
1619
+ && !options.lifecycleSignal?.aborted),
1620
+ isOutboundCurrent: () => (!options.lifecycleSignal?.aborted
1621
+ && (0, latest_turn_1.isCurrent)(options.latestTurnCapability)),
1622
+ isOutboundAdmittedNow: () => (!options.lifecycleSignal?.aborted
1623
+ && (0, latest_turn_1.isDeliveryAdmittedNow)(options.latestTurnCapability)),
1624
+ onFinalTransportInvoked: () => {
1625
+ finalTransportInvoked = true;
1785
1626
  },
1786
- pendingDir,
1787
- friendStore: store,
1788
- provider: "imessage-handle",
1789
- externalId: event.sender.externalId,
1790
- isGroupChat: event.chat.isGroup,
1791
- groupHasFamilyMember,
1792
- hasExistingGroupWithFamily,
1793
- enforceTrustGate: trust_gate_1.enforceTrustGate,
1794
- drainPending: pending_1.drainPending,
1795
- drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
1796
- prepareRunAgentOptions: async ({ messages = [], currentUserMessages = [], runAgentOptions }) => {
1797
- preparedBlueBubblesContext = await buildBlueBubblesContextMessages({
1798
- agentName,
1799
- agentRoot: (0, identity_1.getAgentRoot)(),
1800
- client,
1801
- event,
1802
- knownMessages: messages,
1627
+ onFinalTransportAccepted: () => {
1628
+ finalTransportAccepted = true;
1629
+ },
1630
+ durableOutbound: {
1631
+ agentRoot: (0, identity_1.getAgentRoot)(),
1632
+ idempotencyKey: blueBubblesInboundReplyIdempotencyKey(options.currentIngressEvidence.captureKeyHash),
1633
+ attachment: {
1634
+ serverUrl: outboundRuntimeConfig.serverUrl,
1635
+ accountId: outboundRuntimeConfig.accountId,
1636
+ },
1637
+ },
1638
+ });
1639
+ const controller = new AbortController();
1640
+ const lifecycleSignal = options.lifecycleSignal;
1641
+ let rejectLifecycle;
1642
+ const lifecyclePromise = new Promise((_resolve, reject) => {
1643
+ rejectLifecycle = reject;
1644
+ });
1645
+ let resolveSupersession;
1646
+ const supersessionPromise = new Promise((resolve) => {
1647
+ resolveSupersession = resolve;
1648
+ });
1649
+ const cancelForSupersession = () => {
1650
+ callbacks.cancelOutbound("superseded");
1651
+ if (!finalTransportInvoked) {
1652
+ controller.abort(options.latestTurnCapability.signal.reason);
1653
+ resolveSupersession({
1654
+ handled: true,
1655
+ notifiedAgent: false,
1656
+ kind: event.kind,
1657
+ reason: "superseded",
1803
1658
  });
1804
- const currentUserMessage = currentUserMessages.findLast((message) => message.role === "user")
1805
- ?? messages.findLast((message) => message.role === "user");
1806
- if (!currentUserMessage)
1807
- return undefined;
1808
- Object.freeze(currentUserMessage);
1809
- const { verifiedPredecessorMessage } = preparedBlueBubblesContext;
1659
+ }
1660
+ };
1661
+ options.latestTurnCapability.signal.addEventListener("abort", cancelForSupersession, { once: true });
1662
+ const cancelForShutdown = () => {
1663
+ callbacks.cancelOutbound("shutdown");
1664
+ if (!finalTransportInvoked) {
1665
+ const reason = lifecycleSignal.reason;
1666
+ controller.abort(reason);
1667
+ rejectLifecycle(reason);
1668
+ }
1669
+ };
1670
+ lifecycleSignal?.addEventListener("abort", cancelForShutdown, { once: true });
1671
+ let timeoutTimer;
1672
+ let timeoutPromise;
1673
+ let resolveTimeout;
1674
+ let recoveryTimedOut = false;
1675
+ let activeRunTurn = null;
1676
+ // BB-specific tool context wrappers
1677
+ const summarize = (0, core_1.createSummarize)("human");
1678
+ const bbCapabilities = (0, friends_1.getChannelCapabilities)("bluebubbles");
1679
+ const pendingDir = (0, pending_1.getPendingDir)(resolvedDeps.getAgentName(), friendId, "bluebubbles", event.chat.sessionKey);
1680
+ // Buffer terminal errors so failover can suppress them.
1681
+ // If failover produces a message, the buffered error is skipped.
1682
+ // If failover doesn't fire, the buffered error is replayed.
1683
+ let bufferedTerminalError = null;
1684
+ /* v8 ignore start -- failover-aware error buffering @preserve */
1685
+ const failoverAwareCallbacks = {
1686
+ ...callbacks,
1687
+ onError(error, severity) {
1688
+ if (severity === "terminal") {
1689
+ bufferedTerminalError = error;
1690
+ return;
1691
+ }
1692
+ callbacks.onError(error, severity);
1693
+ },
1694
+ };
1695
+ /* v8 ignore stop */
1696
+ try {
1697
+ // ── Compute trust gate context for group/acquaintance rules ─────
1698
+ // These store reads can block on filesystem I/O. Race them against the
1699
+ // runtime lifecycle before constructing the shared pipeline so a closed
1700
+ // worker cannot begin new durable or outward work after the read settles.
1701
+ const trustContext = await Promise.race([
1702
+ Promise.all([
1703
+ checkGroupHasFamilyMember(store, event),
1704
+ event.chat.isGroup
1705
+ ? Promise.resolve(false)
1706
+ : checkHasExistingGroupWithFamily(store, context.friend),
1707
+ ]),
1708
+ lifecyclePromise,
1709
+ supersessionPromise.then(() => null),
1710
+ ]);
1711
+ if (trustContext === null) {
1810
1712
  return {
1811
- ...(preparedBlueBubblesContext.contextPacketIds.length > 0 ? {
1812
- contextPacketIds: [
1813
- ...(runAgentOptions.contextPacketIds ?? []),
1814
- ...preparedBlueBubblesContext.contextPacketIds,
1815
- ],
1816
- } : {}),
1817
- ...(preparedBlueBubblesContext.messages.length > 0 ? {
1818
- promptOnlyEvidenceMessages: preparedBlueBubblesContext.messages,
1819
- } : {}),
1820
- requiredPromptEvidence: {
1821
- currentUserMessage,
1822
- ...(verifiedPredecessorMessage ? { verifiedPredecessorMessage } : {}),
1823
- },
1713
+ handled: true,
1714
+ notifiedAgent: false,
1715
+ kind: event.kind,
1716
+ reason: "superseded",
1824
1717
  };
1825
- },
1826
- runAgent: async (msgs, cb, channel, sig, opts) => {
1827
- const { codingFeedback: _omittedCodingFeedback, ...safeToolContext } = opts?.toolContext ?? {};
1828
- const requiredCurrent = opts?.requiredPromptEvidence?.currentUserMessage;
1829
- const providerMessages = insertEphemeralContextMessages(msgs.map((message) => message === requiredCurrent ? message : structuredClone(message)), [
1830
- ...preparedBlueBubblesContext.messages,
1831
- ...(preparedBlueBubblesContext.verifiedPredecessorMessage
1832
- ? [preparedBlueBubblesContext.verifiedPredecessorMessage]
1833
- : []),
1834
- ], requiredCurrent);
1835
- let generatedMessages = [];
1836
- const agentResult = await resolvedDeps.runAgent(providerMessages, cb, channel, sig, {
1837
- ...opts,
1838
- captureGeneratedMessages: (generated) => {
1839
- opts?.captureGeneratedMessages?.(generated);
1840
- generatedMessages = structuredClone(generated);
1718
+ }
1719
+ const [groupHasFamilyMember, hasExistingGroupWithFamily] = trustContext;
1720
+ lifecycleSignal?.throwIfAborted();
1721
+ // ── Call shared pipeline ────────────────────────────────────────
1722
+ const timeoutMs = options.timeoutMs ?? BLUEBUBBLES_LIVE_TURN_TIMEOUT_MS;
1723
+ timeoutPromise = new Promise((resolve) => {
1724
+ resolveTimeout = resolve;
1725
+ });
1726
+ timeoutTimer = setTimeout(() => {
1727
+ const reason = new BlueBubblesRecoveryTurnTimeoutError(timeoutMs);
1728
+ recoveryTimedOut = true;
1729
+ if (!finalTransportInvoked) {
1730
+ resolveTimeout?.({
1731
+ handled: true,
1732
+ notifiedAgent: false,
1733
+ kind: event.kind,
1734
+ reason: "turn_timeout",
1735
+ });
1736
+ }
1737
+ callbacks.cancelOutbound("turn_timeout");
1738
+ (0, latest_turn_1.cancel)(options.latestTurnCapability, "turn_timeout");
1739
+ if (!finalTransportInvoked)
1740
+ controller.abort(reason);
1741
+ (0, runtime_1.emitNervesEvent)({
1742
+ level: "warn",
1743
+ component: "senses",
1744
+ event: "senses.bluebubbles_turn_timeout",
1745
+ message: "bluebubbles turn timed out",
1746
+ meta: {
1747
+ messageGuid: event.messageGuid,
1748
+ sessionKey: event.chat.sessionKey,
1749
+ source,
1750
+ timeoutMs,
1841
1751
  },
1842
- toolContext: {
1843
- /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
1844
- signin: async () => undefined,
1845
- ...safeToolContext,
1846
- summarize,
1847
- bluebubblesReplyTarget: {
1848
- setSelection: (selection) => replyTarget.setSelection(selection),
1849
- },
1752
+ });
1753
+ (0, runtime_1.emitNervesEvent)({
1754
+ level: "warn",
1755
+ component: "senses",
1756
+ event: "senses.bluebubbles_timeout_notice_suppressed",
1757
+ message: "bluebubbles timeout notice suppressed from iMessage",
1758
+ meta: {
1759
+ messageGuid: event.messageGuid,
1760
+ sessionKey: event.chat.sessionKey,
1761
+ source,
1762
+ timeoutMs,
1850
1763
  },
1851
1764
  });
1852
- if (generatedMessages.length > 0) {
1853
- msgs.push(...generatedMessages);
1854
- }
1855
- return agentResult;
1856
- },
1857
- postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
1858
- const prepared = resolvedDeps.postTurnTrim(turnMessages, usage, hooks);
1859
- turnStage.postTurn = {
1860
- sessionPath: sessionPathArg,
1861
- prepared,
1862
- usage,
1863
- state,
1864
- };
1865
- },
1866
- accumulateFriendTokens: async (tokenStore, tokenFriendId, usage) => {
1867
- turnStage.tokens = {
1868
- store: tokenStore,
1869
- friendId: tokenFriendId,
1870
- usage,
1871
- };
1872
- },
1873
- signal: controller.signal,
1874
- runAgentOptions: {
1875
- mcpManager,
1876
- orientationFrame,
1877
- toolContext: {
1878
- signin: async () => undefined,
1879
- agentRoot: (0, identity_1.getAgentRoot)(),
1880
- currentIngressEvidence: options.currentIngressEvidence,
1765
+ }, timeoutMs);
1766
+ /* v8 ignore next -- timer handles expose unref only in some runtimes @preserve */
1767
+ if (typeof timeoutTimer.unref === "function") {
1768
+ timeoutTimer.unref();
1769
+ }
1770
+ let preparedBlueBubblesContext = {
1771
+ messages: [],
1772
+ contextPacketIds: [],
1773
+ };
1774
+ const turnPromise = (0, pipeline_1.handleInboundTurn)({
1775
+ channel: "bluebubbles",
1776
+ sessionKey: event.chat.sessionKey,
1777
+ capabilities: bbCapabilities,
1778
+ messages: [userMessage],
1779
+ continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
1780
+ friendResolver: { resolve: () => Promise.resolve(context) },
1781
+ sessionLoader: {
1782
+ loadOrCreate: () => Promise.resolve({
1783
+ messages: sessionMessages,
1784
+ sessionPath: sessPath,
1785
+ state: existing?.state,
1786
+ events: existing?.events,
1787
+ structuredOutputs: existing?.structuredOutputs,
1788
+ }),
1881
1789
  },
1882
- },
1883
- callbacks: failoverAwareCallbacks,
1884
- failoverState: turnStage.failoverState,
1885
- });
1886
- /* v8 ignore start -- detached late-rejection telemetry is asserted in timeout tests, but V8 does not reliably attribute Promise.catch callbacks @preserve */
1887
- void turnPromise
1888
- .catch((error) => {
1889
- if (!recoveryTimedOut)
1890
- return;
1891
- (0, runtime_1.emitNervesEvent)({
1892
- level: "warn",
1893
- component: "senses",
1894
- event: "senses.bluebubbles_recovery_error",
1895
- message: "bluebubbles recovery turn rejected after timeout",
1896
- meta: {
1897
- messageGuid: event.messageGuid,
1898
- sessionKey: event.chat.sessionKey,
1899
- source,
1900
- reason: error instanceof Error ? error.message : String(error),
1790
+ pendingDir,
1791
+ friendStore: store,
1792
+ provider: "imessage-handle",
1793
+ externalId: event.sender.externalId,
1794
+ isGroupChat: event.chat.isGroup,
1795
+ groupHasFamilyMember,
1796
+ hasExistingGroupWithFamily,
1797
+ enforceTrustGate: trust_gate_1.enforceTrustGate,
1798
+ drainPending: pending_1.drainPending,
1799
+ drainDeferredReturns: (deferredFriendId) => (0, pending_1.drainDeferredReturns)(resolvedDeps.getAgentName(), deferredFriendId),
1800
+ prepareRunAgentOptions: async ({ messages = [], currentUserMessages = [], runAgentOptions }) => {
1801
+ preparedBlueBubblesContext = await buildBlueBubblesContextMessages({
1802
+ agentName,
1803
+ agentRoot: (0, identity_1.getAgentRoot)(),
1804
+ client,
1805
+ event,
1806
+ knownMessages: messages,
1807
+ });
1808
+ const currentUserMessage = currentUserMessages.findLast((message) => message.role === "user")
1809
+ ?? messages.findLast((message) => message.role === "user");
1810
+ if (!currentUserMessage)
1811
+ return undefined;
1812
+ Object.freeze(currentUserMessage);
1813
+ const { verifiedPredecessorMessage } = preparedBlueBubblesContext;
1814
+ return {
1815
+ ...(preparedBlueBubblesContext.contextPacketIds.length > 0 ? {
1816
+ contextPacketIds: [
1817
+ ...(runAgentOptions.contextPacketIds ?? []),
1818
+ ...preparedBlueBubblesContext.contextPacketIds,
1819
+ ],
1820
+ } : {}),
1821
+ ...(preparedBlueBubblesContext.messages.length > 0 ? {
1822
+ promptOnlyEvidenceMessages: preparedBlueBubblesContext.messages,
1823
+ } : {}),
1824
+ requiredPromptEvidence: {
1825
+ currentUserMessage,
1826
+ ...(verifiedPredecessorMessage ? { verifiedPredecessorMessage } : {}),
1827
+ },
1828
+ };
1829
+ },
1830
+ runAgent: async (msgs, cb, channel, sig, opts) => {
1831
+ const { codingFeedback: _omittedCodingFeedback, ...safeToolContext } = opts?.toolContext ?? {};
1832
+ const requiredCurrent = opts?.requiredPromptEvidence?.currentUserMessage;
1833
+ const providerMessages = insertEphemeralContextMessages(msgs.map((message) => message === requiredCurrent ? message : structuredClone(message)), [
1834
+ ...preparedBlueBubblesContext.messages,
1835
+ ...(preparedBlueBubblesContext.verifiedPredecessorMessage
1836
+ ? [preparedBlueBubblesContext.verifiedPredecessorMessage]
1837
+ : []),
1838
+ ], requiredCurrent);
1839
+ let generatedMessages = [];
1840
+ const agentResult = await resolvedDeps.runAgent(providerMessages, cb, channel, sig, {
1841
+ ...opts,
1842
+ captureGeneratedMessages: (generated) => {
1843
+ opts?.captureGeneratedMessages?.(generated);
1844
+ generatedMessages = structuredClone(generated);
1845
+ },
1846
+ toolContext: {
1847
+ /* v8 ignore next -- default no-op signin; pipeline provides the real one @preserve */
1848
+ signin: async () => undefined,
1849
+ ...safeToolContext,
1850
+ summarize,
1851
+ bluebubblesReplyTarget: {
1852
+ setSelection: (selection) => replyTarget.setSelection(selection),
1853
+ },
1854
+ },
1855
+ });
1856
+ if (generatedMessages.length > 0) {
1857
+ msgs.push(...generatedMessages);
1858
+ }
1859
+ return agentResult;
1860
+ },
1861
+ postTurn: (turnMessages, sessionPathArg, usage, hooks, state) => {
1862
+ const prepared = resolvedDeps.postTurnTrim(turnMessages, usage, hooks);
1863
+ turnStage.postTurn = {
1864
+ sessionPath: sessionPathArg,
1865
+ prepared,
1866
+ usage,
1867
+ state,
1868
+ };
1869
+ },
1870
+ accumulateFriendTokens: async (tokenStore, tokenFriendId, usage) => {
1871
+ turnStage.tokens = {
1872
+ store: tokenStore,
1873
+ friendId: tokenFriendId,
1874
+ usage,
1875
+ };
1876
+ },
1877
+ signal: controller.signal,
1878
+ runAgentOptions: {
1879
+ mcpManager,
1880
+ orientationFrame,
1881
+ toolContext: {
1882
+ signin: async () => undefined,
1883
+ agentRoot: (0, identity_1.getAgentRoot)(),
1884
+ currentIngressEvidence: options.currentIngressEvidence,
1885
+ },
1901
1886
  },
1887
+ callbacks: failoverAwareCallbacks,
1888
+ sessionTurnLease,
1889
+ failoverState: turnStage.failoverState,
1902
1890
  });
1903
- });
1904
- /* v8 ignore stop */
1905
- const runTurn = (async () => {
1906
- const result = await turnPromise;
1907
- if (recoveryTimedOut && !finalTransportInvoked) {
1891
+ /* v8 ignore start -- detached late-rejection telemetry is asserted in timeout tests, but V8 does not reliably attribute Promise.catch callbacks @preserve */
1892
+ void turnPromise
1893
+ .catch((error) => {
1894
+ if (!recoveryTimedOut)
1895
+ return;
1908
1896
  (0, runtime_1.emitNervesEvent)({
1909
1897
  level: "warn",
1910
1898
  component: "senses",
1911
- event: "senses.bluebubbles_timed_out_turn_suppressed",
1912
- message: "suppressed late bluebubbles turn result after timeout",
1899
+ event: "senses.bluebubbles_recovery_error",
1900
+ message: "bluebubbles recovery turn rejected after timeout",
1913
1901
  meta: {
1914
1902
  messageGuid: event.messageGuid,
1915
1903
  sessionKey: event.chat.sessionKey,
1916
1904
  source,
1917
- reason: "turn completed after its final-transport boundary closed",
1905
+ reason: error instanceof Error ? error.message : String(error),
1918
1906
  },
1919
1907
  });
1920
- return {
1921
- handled: true,
1922
- notifiedAgent: false,
1923
- kind: event.kind,
1924
- reason: "turn_timeout",
1925
- };
1926
- }
1927
- /* v8 ignore start -- failover display + error replay @preserve */
1928
- if (result.failoverMessage) {
1929
- callbacks.onClearText?.();
1930
- callbacks.onTextChunk(result.failoverMessage);
1931
- bufferedTerminalError = null;
1932
- }
1933
- else if (bufferedTerminalError) {
1934
- callbacks.onError(bufferedTerminalError, "terminal");
1935
- bufferedTerminalError = null;
1936
- }
1908
+ });
1937
1909
  /* v8 ignore stop */
1938
- // ── Handle gate result ────────────────────────────────────────
1939
- let processedOutcome = "turn-complete";
1940
- if (!result.gateResult.allowed) {
1941
- callbacks.onClearText?.();
1942
- if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
1943
- callbacks.onTextChunk(result.gateResult.autoReply);
1910
+ const runTurn = (async () => {
1911
+ const result = await turnPromise;
1912
+ if (recoveryTimedOut && !finalTransportInvoked) {
1913
+ (0, runtime_1.emitNervesEvent)({
1914
+ level: "warn",
1915
+ component: "senses",
1916
+ event: "senses.bluebubbles_timed_out_turn_suppressed",
1917
+ message: "suppressed late bluebubbles turn result after timeout",
1918
+ meta: {
1919
+ messageGuid: event.messageGuid,
1920
+ sessionKey: event.chat.sessionKey,
1921
+ source,
1922
+ reason: "turn completed after its final-transport boundary closed",
1923
+ },
1924
+ });
1925
+ return {
1926
+ handled: true,
1927
+ notifiedAgent: false,
1928
+ kind: event.kind,
1929
+ reason: "turn_timeout",
1930
+ };
1944
1931
  }
1945
- processedOutcome = "trust-gated";
1946
- }
1947
- let delivery;
1948
- try {
1949
- delivery = await callbacks.flush({ signal: controller.signal });
1950
- }
1951
- catch (error) {
1932
+ /* v8 ignore start -- failover display + error replay @preserve */
1933
+ if (result.failoverMessage) {
1934
+ callbacks.onClearText?.();
1935
+ callbacks.onTextChunk(result.failoverMessage);
1936
+ bufferedTerminalError = null;
1937
+ }
1938
+ else if (bufferedTerminalError) {
1939
+ callbacks.onError(bufferedTerminalError, "terminal");
1940
+ bufferedTerminalError = null;
1941
+ }
1942
+ /* v8 ignore stop */
1943
+ // ── Handle gate result ────────────────────────────────────────
1944
+ let processedOutcome = "turn-complete";
1945
+ if (!result.gateResult.allowed) {
1946
+ callbacks.onClearText?.();
1947
+ if ("autoReply" in result.gateResult && result.gateResult.autoReply) {
1948
+ callbacks.onTextChunk(result.gateResult.autoReply);
1949
+ }
1950
+ processedOutcome = "trust-gated";
1951
+ }
1952
+ let delivery;
1953
+ try {
1954
+ delivery = await callbacks.flush({ signal: controller.signal });
1955
+ }
1956
+ catch (error) {
1957
+ (0, runtime_1.emitNervesEvent)({
1958
+ level: "warn",
1959
+ component: "senses",
1960
+ event: "senses.bluebubbles_final_transport_failed",
1961
+ message: "bluebubbles final transport failed after invocation",
1962
+ meta: {
1963
+ messageGuid: event.messageGuid,
1964
+ sessionKey: event.chat.sessionKey,
1965
+ reason: error instanceof Error ? error.message : String(error),
1966
+ },
1967
+ });
1968
+ return {
1969
+ handled: true,
1970
+ notifiedAgent: false,
1971
+ kind: event.kind,
1972
+ reason: "delivery_failed",
1973
+ };
1974
+ }
1975
+ if (delivery.status !== "accepted" || !finalTransportAccepted) {
1976
+ return {
1977
+ handled: true,
1978
+ notifiedAgent: false,
1979
+ kind: event.kind,
1980
+ reason: !(0, latest_turn_1.isCurrent)(options.latestTurnCapability)
1981
+ ? "superseded"
1982
+ : "no_visible_reply",
1983
+ };
1984
+ }
1985
+ // Acceptance is the visibility boundary. Publish the terminal dedupe
1986
+ // receipt first, then project the private generated tail while this
1987
+ // chat's canonical lock remains held. Even if receipt publication
1988
+ // fails, make the best possible canonical projection before surfacing
1989
+ // the retained-claim failure. Ancillary accounting follows both.
1990
+ let receiptFailed = false;
1991
+ let receiptFailure;
1992
+ try {
1993
+ options.publishAcceptedReceipt();
1994
+ }
1995
+ catch (error) {
1996
+ receiptFailed = true;
1997
+ receiptFailure = error;
1998
+ }
1999
+ persistAcceptedBlueBubblesProjection({
2000
+ stage: turnStage,
2001
+ deps: resolvedDeps,
2002
+ event,
2003
+ });
2004
+ if (receiptFailed)
2005
+ throw receiptFailure;
2006
+ promoteAcceptedBlueBubblesAncillaryState({
2007
+ stage: turnStage,
2008
+ deps: resolvedDeps,
2009
+ agentName,
2010
+ event,
2011
+ source,
2012
+ processedOutcome,
2013
+ });
1952
2014
  (0, runtime_1.emitNervesEvent)({
1953
- level: "warn",
1954
2015
  component: "senses",
1955
- event: "senses.bluebubbles_final_transport_failed",
1956
- message: "bluebubbles final transport failed after invocation",
2016
+ event: "senses.bluebubbles_turn_end",
2017
+ message: "bluebubbles event handled",
1957
2018
  meta: {
1958
2019
  messageGuid: event.messageGuid,
2020
+ kind: event.kind,
1959
2021
  sessionKey: event.chat.sessionKey,
1960
- reason: error instanceof Error ? error.message : String(error),
1961
2022
  },
1962
2023
  });
1963
2024
  return {
1964
2025
  handled: true,
1965
- notifiedAgent: false,
1966
- kind: event.kind,
1967
- reason: "delivery_failed",
1968
- };
1969
- }
1970
- if (delivery.status !== "accepted" || !finalTransportAccepted) {
1971
- return {
1972
- handled: true,
1973
- notifiedAgent: false,
2026
+ notifiedAgent: true,
1974
2027
  kind: event.kind,
1975
- reason: !(0, latest_turn_1.isCurrent)(options.latestTurnCapability)
1976
- ? "superseded"
1977
- : "no_visible_reply",
1978
2028
  };
1979
- }
1980
- // Acceptance is the visibility boundary. Publish the terminal dedupe
1981
- // receipt first, then project the private generated tail while this
1982
- // chat's canonical lock remains held. Even if receipt publication
1983
- // fails, make the best possible canonical projection before surfacing
1984
- // the retained-claim failure. Ancillary accounting follows both.
1985
- let receiptFailed = false;
1986
- let receiptFailure;
1987
- try {
1988
- options.publishAcceptedReceipt();
1989
- }
1990
- catch (error) {
1991
- receiptFailed = true;
1992
- receiptFailure = error;
1993
- }
1994
- persistAcceptedBlueBubblesProjection({
1995
- stage: turnStage,
1996
- deps: resolvedDeps,
1997
- event,
1998
- });
1999
- if (receiptFailed)
2000
- throw receiptFailure;
2001
- promoteAcceptedBlueBubblesAncillaryState({
2002
- stage: turnStage,
2003
- deps: resolvedDeps,
2004
- agentName,
2005
- event,
2006
- source,
2007
- processedOutcome,
2008
- });
2009
- (0, runtime_1.emitNervesEvent)({
2010
- component: "senses",
2011
- event: "senses.bluebubbles_turn_end",
2012
- message: "bluebubbles event handled",
2013
- meta: {
2014
- messageGuid: event.messageGuid,
2015
- kind: event.kind,
2016
- sessionKey: event.chat.sessionKey,
2017
- },
2018
- });
2019
- return {
2020
- handled: true,
2021
- notifiedAgent: true,
2022
- kind: event.kind,
2023
- };
2024
- })();
2025
- /* v8 ignore start -- detached post-timeout suppression telemetry @preserve */
2026
- void runTurn.catch((error) => {
2027
- if (!recoveryTimedOut)
2028
- return;
2029
- (0, runtime_1.emitNervesEvent)({
2030
- level: "warn",
2031
- component: "senses",
2032
- event: "senses.bluebubbles_timed_out_turn_suppressed",
2033
- message: "suppressed late bluebubbles turn result after timeout",
2034
- meta: {
2035
- messageGuid: event.messageGuid,
2036
- sessionKey: event.chat.sessionKey,
2037
- source,
2038
- reason: error instanceof Error ? error.message : String(error),
2039
- },
2029
+ })();
2030
+ activeRunTurn = runTurn;
2031
+ /* v8 ignore start -- detached post-timeout suppression telemetry @preserve */
2032
+ void runTurn.catch((error) => {
2033
+ if (!recoveryTimedOut)
2034
+ return;
2035
+ (0, runtime_1.emitNervesEvent)({
2036
+ level: "warn",
2037
+ component: "senses",
2038
+ event: "senses.bluebubbles_timed_out_turn_suppressed",
2039
+ message: "suppressed late bluebubbles turn result after timeout",
2040
+ meta: {
2041
+ messageGuid: event.messageGuid,
2042
+ sessionKey: event.chat.sessionKey,
2043
+ source,
2044
+ reason: error instanceof Error ? error.message : String(error),
2045
+ },
2046
+ });
2040
2047
  });
2041
- });
2042
- /* v8 ignore stop */
2043
- return await Promise.race([runTurn, timeoutPromise, supersessionPromise, lifecyclePromise]);
2044
- }
2045
- finally {
2046
- // If a terminal error was buffered and never replayed (e.g., handleInboundTurn threw),
2047
- // replay it now so the user still sees the error.
2048
- /* v8 ignore start -- error replay on throw: tested via BB error test @preserve */
2049
- if (bufferedTerminalError) {
2050
- callbacks.onError(bufferedTerminalError, "terminal");
2051
- bufferedTerminalError = null;
2048
+ /* v8 ignore stop */
2049
+ return await Promise.race([runTurn, timeoutPromise, supersessionPromise, lifecyclePromise]);
2052
2050
  }
2053
- /* v8 ignore stop */
2054
- clearTimeout(timeoutTimer);
2055
- options.latestTurnCapability.signal.removeEventListener("abort", cancelForSupersession);
2056
- lifecycleSignal?.removeEventListener("abort", cancelForShutdown);
2057
- (0, active_turns_1.finishBlueBubblesActiveTurn)(agentName, liveTurnId);
2058
- activeTurnId = null;
2059
- await callbacks.finish({ timeoutMs: BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS });
2060
- }
2051
+ finally {
2052
+ // A timed-out/superseded provider may ignore abort briefly. Never release
2053
+ // the cross-process session lease until that work has actually quiesced.
2054
+ if (activeRunTurn)
2055
+ await activeRunTurn.catch(() => undefined);
2056
+ // If a terminal error was buffered and never replayed (e.g., handleInboundTurn threw),
2057
+ // replay it now so the user still sees the error.
2058
+ /* v8 ignore start -- error replay on throw: tested via BB error test @preserve */
2059
+ if (bufferedTerminalError) {
2060
+ callbacks.onError(bufferedTerminalError, "terminal");
2061
+ bufferedTerminalError = null;
2062
+ }
2063
+ /* v8 ignore stop */
2064
+ clearTimeout(timeoutTimer);
2065
+ options.latestTurnCapability.signal.removeEventListener("abort", cancelForSupersession);
2066
+ lifecycleSignal?.removeEventListener("abort", cancelForShutdown);
2067
+ (0, active_turns_1.finishBlueBubblesActiveTurn)(agentName, liveTurnId);
2068
+ activeTurnId = null;
2069
+ await callbacks.finish({ timeoutMs: BLUEBUBBLES_CALLBACK_CLEANUP_TIMEOUT_MS });
2070
+ }
2071
+ });
2061
2072
  });
2062
2073
  }
2063
2074
  finally {