@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.
- package/changelog.json +7 -0
- package/dist/heart/approval-store.js +791 -0
- package/dist/heart/core.js +177 -0
- package/dist/heart/session-events.js +167 -0
- package/dist/heart/tool-approval.js +339 -0
- package/dist/mind/context.js +80 -67
- package/dist/mind/session-transaction.js +379 -0
- package/dist/repertoire/stripe-client.js +2 -2
- package/dist/repertoire/tool-arguments.js +104 -0
- package/dist/repertoire/tools-shell.js +13 -0
- package/dist/repertoire/tools.js +4 -0
- package/dist/senses/bluebubbles/index.js +504 -493
- package/dist/senses/cli.js +234 -214
- package/dist/senses/pipeline.js +23 -0
- package/dist/senses/private-runtime.js +330 -325
- package/dist/senses/shared-turn.js +176 -169
- package/dist/senses/teams.js +175 -163
- package/package.json +2 -1
|
@@ -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
|
-
|
|
1517
|
-
|
|
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
|
-
|
|
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.
|
|
1741
|
-
message: "bluebubbles
|
|
1526
|
+
event: "senses.bluebubbles_thread_lane_cleanup_error",
|
|
1527
|
+
message: "failed to inspect obsolete bluebubbles thread-lane sessions",
|
|
1742
1528
|
meta: {
|
|
1743
|
-
|
|
1744
|
-
|
|
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.
|
|
1753
|
-
message: "bluebubbles
|
|
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
|
-
|
|
1762
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
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
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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.
|
|
1912
|
-
message: "
|
|
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:
|
|
1905
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1918
1906
|
},
|
|
1919
1907
|
});
|
|
1920
|
-
|
|
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
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
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.
|
|
1956
|
-
message: "bluebubbles
|
|
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:
|
|
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
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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 {
|