@ouro.bot/cli 0.1.0-alpha.954 → 0.1.0-alpha.955
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
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.955",
|
|
6
|
+
"changes": [
|
|
7
|
+
"iMessage no longer loses a reply that fails to send. When Slugger's reply times out or otherwise fails at BlueBubbles, your message stays pending; about ten minutes later a recovery pass checks the chat, and if the reply did not arrive after all, Slugger answers again. The message is marked handled only once a reply goes through. After three failed sends it is given up on with an error in the logs, so one stuck message cannot use up the day's recovery budget. If Slugger had already said something partway through that turn, the message is not retried, so you never see that part twice."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.954",
|
|
6
12
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.955</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -106,6 +106,11 @@ const BLUEBUBBLES_VISIBLE_OUTBOUND_STATUSES = new Set([
|
|
|
106
106
|
"local-visible",
|
|
107
107
|
"delivered",
|
|
108
108
|
]);
|
|
109
|
+
// A reply whose final send keeps failing stays pending for recovery this many
|
|
110
|
+
// times, then gets a terminal receipt so one message cannot drain the budget.
|
|
111
|
+
const BLUEBUBBLES_REPLY_DELIVERY_MAX_ATTEMPTS = 3;
|
|
112
|
+
// A timed-out send may still land, so wait before checking the chat and retrying.
|
|
113
|
+
const BLUEBUBBLES_REPLY_RETRY_DELAY_MS = 10 * 60_000;
|
|
109
114
|
exports.BLUEBUBBLES_SEMANTIC_GENERATION_RETENTION_MS = 15 * 60_000;
|
|
110
115
|
function pruneBlueBubblesSemanticObservationGenerations(nowMs = Date.now()) {
|
|
111
116
|
let pruned = 0;
|
|
@@ -1622,8 +1627,12 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1622
1627
|
const turnStage = createBlueBubblesTurnStage(event.chat.sessionKey);
|
|
1623
1628
|
let finalTransportInvoked = false;
|
|
1624
1629
|
let finalTransportAccepted = false;
|
|
1630
|
+
let spokeBeforeFinalTransport = false;
|
|
1625
1631
|
const outboundRuntimeConfig = (0, config_1.getBlueBubblesConfig)();
|
|
1626
|
-
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, () =>
|
|
1632
|
+
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, () => {
|
|
1633
|
+
spokeBeforeFinalTransport = true;
|
|
1634
|
+
(0, active_turns_1.noteBlueBubblesActiveTurnVisibleActivity)(agentName, liveTurnId);
|
|
1635
|
+
}, {
|
|
1627
1636
|
admitOutbound: async () => (!options.lifecycleSignal?.aborted
|
|
1628
1637
|
&& await (0, latest_turn_1.awaitDeliveryAdmission)(options.latestTurnCapability)
|
|
1629
1638
|
&& !options.lifecycleSignal?.aborted),
|
|
@@ -1976,6 +1985,18 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1976
1985
|
reason: error instanceof Error ? error.message : String(error),
|
|
1977
1986
|
},
|
|
1978
1987
|
});
|
|
1988
|
+
if (spokeBeforeFinalTransport) {
|
|
1989
|
+
// The friend already saw part of this turn; a retry would repeat it.
|
|
1990
|
+
const failed = (0, outbound_state_1.readBlueBubblesOutboundRecordByIdempotencyKey)((0, identity_1.getAgentRoot)(), blueBubblesInboundReplyIdempotencyKey(options.currentIngressEvidence.captureKeyHash));
|
|
1991
|
+
if (failed) {
|
|
1992
|
+
(0, outbound_state_1.markBlueBubblesOutboundPendingManualVerification)({
|
|
1993
|
+
agentRoot: (0, identity_1.getAgentRoot)(),
|
|
1994
|
+
recordId: failed.recordId,
|
|
1995
|
+
decidedAt: new Date().toISOString(),
|
|
1996
|
+
reason: "mid-turn speech was visible before the final send failed",
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1979
2000
|
return {
|
|
1980
2001
|
handled: true,
|
|
1981
2002
|
notifiedAgent: false,
|
|
@@ -2406,6 +2427,18 @@ function writeCapturedBlueBubblesHandled(agentName, capture, outcome, detailCode
|
|
|
2406
2427
|
function blueBubblesInboundReplyIdempotencyKey(captureKeyHash) {
|
|
2407
2428
|
return `bluebubbles-inbound-reply:${captureKeyHash}`;
|
|
2408
2429
|
}
|
|
2430
|
+
async function blueBubblesFailedReplyIsVisible(deps, chat, record) {
|
|
2431
|
+
const since = Date.parse(record.createdAt);
|
|
2432
|
+
const recent = await deps.createClient().listRecentMessages?.({
|
|
2433
|
+
chatGuid: chat.chatGuid,
|
|
2434
|
+
limit: 25,
|
|
2435
|
+
}) ?? [];
|
|
2436
|
+
return recent.some((message) => (message.kind === "message"
|
|
2437
|
+
&& message.fromMe
|
|
2438
|
+
&& message.timestamp >= since
|
|
2439
|
+
&& message.chat.chatGuid === chat.chatGuid
|
|
2440
|
+
&& (0, outbound_state_1.blueBubblesOutboundTextMatches)(record, message.text)));
|
|
2441
|
+
}
|
|
2409
2442
|
function blueBubblesRecoveredOutboundOutcome(record) {
|
|
2410
2443
|
return BLUEBUBBLES_VISIBLE_OUTBOUND_STATUSES.has(record.status)
|
|
2411
2444
|
? "message_completed"
|
|
@@ -2520,7 +2553,13 @@ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, so
|
|
|
2520
2553
|
return result;
|
|
2521
2554
|
}
|
|
2522
2555
|
const durableOutbound = (0, outbound_state_1.readBlueBubblesOutboundRecordByIdempotencyKey)((0, identity_1.getAgentRoot)(), blueBubblesInboundReplyIdempotencyKey(captured.capture.keyHash));
|
|
2523
|
-
|
|
2556
|
+
const retryFailedReply = durableOutbound?.status === "failed"
|
|
2557
|
+
&& (durableOutbound.failedAttempts ?? 0) < BLUEBUBBLES_REPLY_DELIVERY_MAX_ATTEMPTS;
|
|
2558
|
+
if (retryFailedReply
|
|
2559
|
+
&& Date.now() < Date.parse(durableOutbound.failedAt ?? durableOutbound.updatedAt) + BLUEBUBBLES_REPLY_RETRY_DELAY_MS) {
|
|
2560
|
+
return { handled: true, notifiedAgent: false, kind: captured.normalized.kind, reason: "delivery_failed" };
|
|
2561
|
+
}
|
|
2562
|
+
if (durableOutbound && !retryFailedReply) {
|
|
2524
2563
|
const outcome = blueBubblesRecoveredOutboundOutcome(durableOutbound);
|
|
2525
2564
|
(0, runtime_1.emitNervesEvent)({
|
|
2526
2565
|
level: "warn",
|
|
@@ -2627,11 +2666,74 @@ async function handleCapturedBlueBubblesSemanticEvent(captured, resolvedDeps, so
|
|
|
2627
2666
|
orientationConversationKind: canonicalEvent.chat.isGroup ? "group" : "one_to_one",
|
|
2628
2667
|
publishAcceptedReceipt,
|
|
2629
2668
|
};
|
|
2669
|
+
let failedReplyVisible = false;
|
|
2670
|
+
if (retryFailedReply) {
|
|
2671
|
+
try {
|
|
2672
|
+
failedReplyVisible = await blueBubblesFailedReplyIsVisible(resolvedDeps, canonicalEvent.chat, durableOutbound);
|
|
2673
|
+
}
|
|
2674
|
+
catch (error) {
|
|
2675
|
+
// Without the chat history a retry could reply twice; try again later.
|
|
2676
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2677
|
+
level: "warn",
|
|
2678
|
+
component: "senses",
|
|
2679
|
+
event: "senses.bluebubbles_reply_retry_deferred",
|
|
2680
|
+
message: "deferred a bluebubbles reply retry because recent chat messages could not be read",
|
|
2681
|
+
meta: {
|
|
2682
|
+
messageGuid: captured.normalized.messageGuid,
|
|
2683
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2684
|
+
},
|
|
2685
|
+
});
|
|
2686
|
+
return { handled: true, notifiedAgent: false, kind: captured.normalized.kind, reason: "delivery_failed" };
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
if (failedReplyVisible) {
|
|
2690
|
+
// The send that timed out reached the chat after all; do not reply twice.
|
|
2691
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2692
|
+
level: "warn",
|
|
2693
|
+
component: "senses",
|
|
2694
|
+
event: "senses.bluebubbles_inbound_reply_recovery_suppressed",
|
|
2695
|
+
message: "suppressed bluebubbles reply retry because a reply is already in the chat",
|
|
2696
|
+
meta: { messageGuid: captured.normalized.messageGuid, recordId: durableOutbound.recordId },
|
|
2697
|
+
});
|
|
2698
|
+
publishTerminalReceipt("message_completed", "reply_visible_after_failed_send");
|
|
2699
|
+
semanticHandlingCompleted = true;
|
|
2700
|
+
return { handled: true, notifiedAgent: false, kind: captured.normalized.kind, reason: "already_processed" };
|
|
2701
|
+
}
|
|
2630
2702
|
result = await handleBlueBubblesNormalizedEvent(canonicalEvent, resolvedDeps, source, capturedOptions);
|
|
2631
2703
|
// A budget or storm-breaker block defers the message; it was never seen.
|
|
2632
2704
|
// Leave it pending so a later recovery pass retries it once the block lifts.
|
|
2633
2705
|
if (result.reason === "autonomy_budget_blocked")
|
|
2634
2706
|
return result;
|
|
2707
|
+
if (result.reason === "delivery_failed") {
|
|
2708
|
+
// The reply never reached the chat. Leave the message pending so a later
|
|
2709
|
+
// recovery pass sends it, until the per-message attempt cap is reached.
|
|
2710
|
+
let failed = (0, outbound_state_1.readBlueBubblesOutboundRecordByIdempotencyKey)((0, identity_1.getAgentRoot)(), blueBubblesInboundReplyIdempotencyKey(captured.capture.keyHash));
|
|
2711
|
+
if (failed?.status === "failed") {
|
|
2712
|
+
if (retryFailedReply && failed.failedAttempts === durableOutbound.failedAttempts) {
|
|
2713
|
+
// The retry failed before it reached the transport; count it anyway.
|
|
2714
|
+
failed = (0, outbound_state_1.markBlueBubblesOutboundFailed)({
|
|
2715
|
+
agentRoot: (0, identity_1.getAgentRoot)(),
|
|
2716
|
+
recordId: failed.recordId,
|
|
2717
|
+
failedAt: new Date().toISOString(),
|
|
2718
|
+
reason: "retry failed before the final transport",
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
/* v8 ignore next -- a failed record re-read after a send always carries its count @preserve */
|
|
2722
|
+
const failedAttempts = failed.failedAttempts ?? 0;
|
|
2723
|
+
if (failedAttempts < BLUEBUBBLES_REPLY_DELIVERY_MAX_ATTEMPTS)
|
|
2724
|
+
return result;
|
|
2725
|
+
(0, runtime_1.emitNervesEvent)({
|
|
2726
|
+
level: "error",
|
|
2727
|
+
component: "senses",
|
|
2728
|
+
event: "senses.bluebubbles_reply_delivery_abandoned",
|
|
2729
|
+
message: "gave up sending a bluebubbles reply after repeated transport failures",
|
|
2730
|
+
meta: { messageGuid: captured.normalized.messageGuid, failedAttempts },
|
|
2731
|
+
});
|
|
2732
|
+
publishTerminalReceipt("message_observed", "reply_delivery_retries_exhausted");
|
|
2733
|
+
semanticHandlingCompleted = true;
|
|
2734
|
+
return result;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2635
2737
|
handledOutcome = result.notifiedAgent ? "message_completed" : "message_observed";
|
|
2636
2738
|
}
|
|
2637
2739
|
if (result.notifiedAgent) {
|
|
@@ -37,6 +37,7 @@ exports.BLUEBUBBLES_OUTBOUND_STATE_POLICY_VERSION = void 0;
|
|
|
37
37
|
exports.blueBubblesOutboundRecordPath = blueBubblesOutboundRecordPath;
|
|
38
38
|
exports.buildBlueBubblesRouteProof = buildBlueBubblesRouteProof;
|
|
39
39
|
exports.buildBlueBubblesAttachmentProof = buildBlueBubblesAttachmentProof;
|
|
40
|
+
exports.blueBubblesOutboundTextMatches = blueBubblesOutboundTextMatches;
|
|
40
41
|
exports.readBlueBubblesOutboundRecord = readBlueBubblesOutboundRecord;
|
|
41
42
|
exports.readBlueBubblesOutboundRecordByIdempotencyKey = readBlueBubblesOutboundRecordByIdempotencyKey;
|
|
42
43
|
exports.reserveBlueBubblesOutbound = reserveBlueBubblesOutbound;
|
|
@@ -139,6 +140,10 @@ function buildRecord(input) {
|
|
|
139
140
|
contentStored: false,
|
|
140
141
|
};
|
|
141
142
|
}
|
|
143
|
+
/** True when `text` is the reply this record last tried to send. */
|
|
144
|
+
function blueBubblesOutboundTextMatches(record, text) {
|
|
145
|
+
return hash(normalizeText(text)) === record.textHash;
|
|
146
|
+
}
|
|
142
147
|
function writeJsonAtomic(filePath, value) {
|
|
143
148
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
144
149
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -240,6 +245,26 @@ function reserveBlueBubblesOutbound(input) {
|
|
|
240
245
|
const existing = readBlueBubblesOutboundRecord(input.agentRoot, record.recordId);
|
|
241
246
|
if (!existing)
|
|
242
247
|
throw new Error(`bluebubbles outbound reservation is unreadable: ${record.recordId}`);
|
|
248
|
+
if (existing.status === "failed"
|
|
249
|
+
&& existing.routeProof.routeHash === record.routeProof.routeHash
|
|
250
|
+
&& existing.attachmentProof.identityHash === record.attachmentProof.identityHash) {
|
|
251
|
+
// A failed send to the same chat is retryable, possibly with new text:
|
|
252
|
+
// reserve again, keeping the first attempt's time and the failure history.
|
|
253
|
+
const retry = writeBlueBubblesOutboundRecord(input.agentRoot, {
|
|
254
|
+
...record,
|
|
255
|
+
createdAt: existing.createdAt,
|
|
256
|
+
...(existing.failedAttempts ? { failedAttempts: existing.failedAttempts } : {}),
|
|
257
|
+
...(existing.failedAt ? { failedAt: existing.failedAt } : {}),
|
|
258
|
+
...(existing.failureReason ? { failureReason: existing.failureReason } : {}),
|
|
259
|
+
});
|
|
260
|
+
(0, runtime_1.emitNervesEvent)({
|
|
261
|
+
component: "senses",
|
|
262
|
+
event: "senses.bluebubbles_outbound_reserved",
|
|
263
|
+
message: "reserved bluebubbles outbound idempotency key again after a failed send",
|
|
264
|
+
meta: { recordId: retry.recordId, textLength: retry.textLength, failedAttempts: retry.failedAttempts ?? 0 },
|
|
265
|
+
});
|
|
266
|
+
return { status: "reserved", record: retry };
|
|
267
|
+
}
|
|
243
268
|
if (identityMatches(existing, record)) {
|
|
244
269
|
emitDuplicate(existing);
|
|
245
270
|
return { status: "duplicate", record: existing };
|
|
@@ -295,9 +320,11 @@ function markBlueBubblesOutboundDelivered(input) {
|
|
|
295
320
|
});
|
|
296
321
|
}
|
|
297
322
|
function markBlueBubblesOutboundFailed(input) {
|
|
323
|
+
const failedAttempts = (readBlueBubblesOutboundRecord(input.agentRoot, input.recordId)?.failedAttempts ?? 0) + 1;
|
|
298
324
|
return updateStatus(input.agentRoot, input.recordId, "failed", input.failedAt, {
|
|
299
325
|
failedAt: input.failedAt,
|
|
300
326
|
failureReason: input.reason.slice(0, 500),
|
|
327
|
+
failedAttempts,
|
|
301
328
|
});
|
|
302
329
|
}
|
|
303
330
|
function markBlueBubblesOutboundPendingManualVerification(input) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.955",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.955",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|