@ouro.bot/cli 0.1.0-alpha.721 → 0.1.0-alpha.723
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/LICENSE +21 -0
- package/changelog.json +289 -275
- package/dist/heart/daemon/doctor.js +231 -13
- package/dist/heart/daemon/freshness.js +343 -0
- package/dist/heart/orientation-frame.js +82 -11
- package/dist/senses/bluebubbles/client.js +49 -0
- package/dist/senses/bluebubbles/index.js +22 -17
- package/dist/senses/bluebubbles/model.js +74 -8
- package/package.json +1 -1
|
@@ -72,9 +72,17 @@ function extractOrderedListReferents(text) {
|
|
|
72
72
|
}
|
|
73
73
|
return referents;
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Returns the marker that fired, not just a boolean: when the hold blocks an action
|
|
77
|
+
* the agent has to be able to see *which* word triggered it. In the 2026-07-27
|
|
78
|
+
* incident it guessed wrong twice ("it's because of the word 'useful'") because the
|
|
79
|
+
* block message named no trigger.
|
|
80
|
+
*/
|
|
81
|
+
function correctionMarkerIn(text) {
|
|
82
|
+
const phrase = /\b(hang on|wait|actually|not that|not this|wrong|misunderstood|you(?:'re| are) right|correct)\b/i.exec(text);
|
|
83
|
+
if (phrase)
|
|
84
|
+
return phrase[0].toLowerCase();
|
|
85
|
+
return /^\s*no[\s,]/i.test(text) ? "no" : undefined;
|
|
78
86
|
}
|
|
79
87
|
function isTerseReferent(text) {
|
|
80
88
|
const normalized = text.trim().toLowerCase().replace(/[.!?]+$/g, "");
|
|
@@ -93,12 +101,16 @@ function shouldAttachStructuredOutput(combinedSpeech, signals) {
|
|
|
93
101
|
return true;
|
|
94
102
|
return signals.some((signal) => signal === "terse_referent" || signal === "correction_marker");
|
|
95
103
|
}
|
|
96
|
-
function deriveSignals(currentUserSpeech, priorAssistantReferents, latestStructuredOutput) {
|
|
104
|
+
function deriveSignals(currentUserSpeech, priorAssistantReferents, latestStructuredOutput, speechKind) {
|
|
97
105
|
const combined = currentUserSpeech.join("\n").trim();
|
|
98
|
-
|
|
106
|
+
// A reaction is an acknowledgement of an existing message, not new speech. Its
|
|
107
|
+
// text is synthesised by the sense from the message being reacted to, so scanning
|
|
108
|
+
// it would read the *quoted* message's words as the human's correction — and a
|
|
109
|
+
// positive tapback is approval, which must never gate outbound work.
|
|
110
|
+
if (!combined || speechKind === "reaction")
|
|
99
111
|
return [];
|
|
100
112
|
const signals = [];
|
|
101
|
-
if (
|
|
113
|
+
if (correctionMarkerIn(combined))
|
|
102
114
|
signals.push("correction_marker");
|
|
103
115
|
if (priorAssistantReferents.length > 0 && isTerseReferent(combined))
|
|
104
116
|
signals.push("terse_referent");
|
|
@@ -106,15 +118,42 @@ function deriveSignals(currentUserSpeech, priorAssistantReferents, latestStructu
|
|
|
106
118
|
signals.push("structured_referent");
|
|
107
119
|
return [...new Set(signals)];
|
|
108
120
|
}
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
/**
|
|
122
|
+
* The hold exists for corrections that are terse or ambiguous ("no, not that one").
|
|
123
|
+
* A correction word buried in a long, self-contained instruction is neither: on
|
|
124
|
+
* 2026-07-27 the word "actually" — in "Do NOT claim a hotel has AC unless you
|
|
125
|
+
* actually saw evidence" — armed the hold inside a 195-word directive that had zero
|
|
126
|
+
* prior referents and no structured output, and blocked five consecutive
|
|
127
|
+
* `send_message` calls. The same directive was what authorised the send, and with
|
|
128
|
+
* nothing to disambiguate the remedy the block named could not be performed.
|
|
129
|
+
*/
|
|
130
|
+
const TERSE_CORRECTION_MAX_WORDS = 12;
|
|
131
|
+
const CORRECTION_HOLD_CLEARED_BY = "Resolve the referent (orientation_get lists the candidates) or restate the request as a standalone, correction-free instruction. The policy is recomputed from each user turn, so it never carries past this turn.";
|
|
132
|
+
function countWords(text) {
|
|
133
|
+
return text.trim().split(/\s+/).filter(Boolean).length;
|
|
134
|
+
}
|
|
135
|
+
function correctionMarkerArmsHold(combinedSpeech, priorAssistantReferents, latestStructuredOutput) {
|
|
136
|
+
if (countWords(combinedSpeech) <= TERSE_CORRECTION_MAX_WORDS)
|
|
137
|
+
return true;
|
|
138
|
+
// Long speech still holds when there is something concrete to disambiguate.
|
|
139
|
+
return priorAssistantReferents.length > 0 || Boolean(latestStructuredOutput);
|
|
140
|
+
}
|
|
141
|
+
function deriveActionPolicy(signals, combinedSpeech, priorAssistantReferents, latestStructuredOutput) {
|
|
142
|
+
const holdSignals = signals.filter((signal) => signal !== "correction_marker"
|
|
143
|
+
|| correctionMarkerArmsHold(combinedSpeech, priorAssistantReferents, latestStructuredOutput));
|
|
144
|
+
if (holdSignals.length === 0)
|
|
111
145
|
return { mode: "normal" };
|
|
146
|
+
const triggeredBy = holdSignals.includes("correction_marker")
|
|
147
|
+
? correctionMarkerIn(combinedSpeech)
|
|
148
|
+
: undefined;
|
|
112
149
|
return {
|
|
113
150
|
mode: "correction_hold",
|
|
114
|
-
reason:
|
|
151
|
+
reason: holdSignals.includes("terse_referent")
|
|
115
152
|
? "Current user speech appears referent-dependent; inspect orientation before mutating durable state."
|
|
116
153
|
: "Current user speech appears to correct or revise prior understanding; inspect orientation before mutating durable state.",
|
|
117
154
|
blockedMutationKinds: ["durable_state_write", "external_side_effect"],
|
|
155
|
+
...(triggeredBy ? { triggeredBy } : {}),
|
|
156
|
+
clearedBy: CORRECTION_HOLD_CLEARED_BY,
|
|
118
157
|
};
|
|
119
158
|
}
|
|
120
159
|
function buildOrientationFrame(input) {
|
|
@@ -127,12 +166,13 @@ function buildOrientationFrame(input) {
|
|
|
127
166
|
const previousAssistant = latestAssistantBefore(input.messages, derivedCurrent.firstIndex);
|
|
128
167
|
const priorAssistantReferents = extractOrderedListReferents(extractMessageText(previousAssistant));
|
|
129
168
|
const candidateStructuredOutput = latestStructuredOutputFrom(input.structuredOutputs);
|
|
130
|
-
const
|
|
169
|
+
const speechKind = input.speechKind ?? "utterance";
|
|
170
|
+
const signals = deriveSignals(currentUserSpeech, priorAssistantReferents, candidateStructuredOutput, speechKind);
|
|
131
171
|
const combinedSpeech = currentUserSpeech.join("\n").trim();
|
|
132
172
|
const latestStructuredOutput = candidateStructuredOutput && shouldAttachStructuredOutput(combinedSpeech, signals)
|
|
133
173
|
? candidateStructuredOutput
|
|
134
174
|
: undefined;
|
|
135
|
-
const actionPolicy = deriveActionPolicy(signals);
|
|
175
|
+
const actionPolicy = deriveActionPolicy(signals, combinedSpeech, priorAssistantReferents, candidateStructuredOutput);
|
|
136
176
|
const frame = {
|
|
137
177
|
schemaVersion: 1,
|
|
138
178
|
channel: input.channel,
|
|
@@ -142,6 +182,7 @@ function buildOrientationFrame(input) {
|
|
|
142
182
|
signals,
|
|
143
183
|
actionPolicy,
|
|
144
184
|
...(input.source ? { source: input.source } : {}),
|
|
185
|
+
...(speechKind === "utterance" ? {} : { speechKind }),
|
|
145
186
|
};
|
|
146
187
|
(0, runtime_1.emitNervesEvent)({
|
|
147
188
|
component: "engine",
|
|
@@ -155,6 +196,27 @@ function buildOrientationFrame(input) {
|
|
|
155
196
|
policy: actionPolicy.mode,
|
|
156
197
|
},
|
|
157
198
|
});
|
|
199
|
+
if (actionPolicy.mode === "correction_hold") {
|
|
200
|
+
// Distinct + greppable: a hold that blocks an explicitly requested action must
|
|
201
|
+
// leave a trace naming what it blocks and what releases it.
|
|
202
|
+
(0, runtime_1.emitNervesEvent)({
|
|
203
|
+
level: "warn",
|
|
204
|
+
component: "engine",
|
|
205
|
+
event: "orientation.correction_hold_armed",
|
|
206
|
+
message: "orientation correction hold armed",
|
|
207
|
+
meta: {
|
|
208
|
+
channel: input.channel,
|
|
209
|
+
speechKind,
|
|
210
|
+
signals,
|
|
211
|
+
blockedMutationKinds: actionPolicy.blockedMutationKinds,
|
|
212
|
+
triggeredBy: actionPolicy.triggeredBy ?? null,
|
|
213
|
+
referentCount: priorAssistantReferents.length,
|
|
214
|
+
structuredOutput: candidateStructuredOutput?.id ?? null,
|
|
215
|
+
speechWordCount: countWords(combinedSpeech),
|
|
216
|
+
clearedBy: actionPolicy.clearedBy,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
158
220
|
return frame;
|
|
159
221
|
}
|
|
160
222
|
function renderOrientationFrame(frame) {
|
|
@@ -165,6 +227,15 @@ function renderOrientationFrame(frame) {
|
|
|
165
227
|
];
|
|
166
228
|
if (frame.actionPolicy.mode === "correction_hold") {
|
|
167
229
|
lines.push(`policy reason: ${frame.actionPolicy.reason}`);
|
|
230
|
+
lines.push(`policy blocks: ${frame.actionPolicy.blockedMutationKinds.join(", ")}`);
|
|
231
|
+
if (frame.actionPolicy.triggeredBy) {
|
|
232
|
+
lines.push(`policy trigger: correction marker "${frame.actionPolicy.triggeredBy}" in current user speech`);
|
|
233
|
+
}
|
|
234
|
+
if (frame.actionPolicy.clearedBy)
|
|
235
|
+
lines.push(`policy clears when: ${frame.actionPolicy.clearedBy}`);
|
|
236
|
+
}
|
|
237
|
+
if (frame.speechKind) {
|
|
238
|
+
lines.push(`speech kind: ${frame.speechKind}`);
|
|
168
239
|
}
|
|
169
240
|
if (frame.source) {
|
|
170
241
|
lines.push("source:");
|
|
@@ -687,5 +687,54 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
687
687
|
return null;
|
|
688
688
|
}
|
|
689
689
|
},
|
|
690
|
+
async getMessageDetails(messageGuid) {
|
|
691
|
+
const url = buildRepairUrl(config.serverUrl, messageGuid, config.password);
|
|
692
|
+
try {
|
|
693
|
+
const response = await fetch(url, {
|
|
694
|
+
method: "GET",
|
|
695
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
696
|
+
});
|
|
697
|
+
if (!response.ok) {
|
|
698
|
+
(0, runtime_1.emitNervesEvent)({
|
|
699
|
+
level: "warn",
|
|
700
|
+
component: "senses",
|
|
701
|
+
event: "senses.bluebubbles_get_message_details_error",
|
|
702
|
+
message: "failed to fetch message details",
|
|
703
|
+
meta: { messageGuid, status: response.status },
|
|
704
|
+
});
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
const data = extractRepairData(await parseJsonBody(response));
|
|
708
|
+
if (!data) {
|
|
709
|
+
(0, runtime_1.emitNervesEvent)({
|
|
710
|
+
level: "warn",
|
|
711
|
+
component: "senses",
|
|
712
|
+
event: "senses.bluebubbles_get_message_details_error",
|
|
713
|
+
message: "message payload was not a record",
|
|
714
|
+
meta: { messageGuid },
|
|
715
|
+
});
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
const text = typeof data.text === "string" ? data.text.trim() || null : null;
|
|
719
|
+
const fromMe = typeof data.isFromMe === "boolean" ? data.isFromMe : null;
|
|
720
|
+
(0, runtime_1.emitNervesEvent)({
|
|
721
|
+
component: "senses",
|
|
722
|
+
event: "senses.bluebubbles_get_message_details",
|
|
723
|
+
message: "fetched message details by guid",
|
|
724
|
+
meta: { messageGuid, hasText: text !== null, hasAuthorship: fromMe !== null },
|
|
725
|
+
});
|
|
726
|
+
return { text, fromMe };
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
(0, runtime_1.emitNervesEvent)({
|
|
730
|
+
level: "warn",
|
|
731
|
+
component: "senses",
|
|
732
|
+
event: "senses.bluebubbles_get_message_details_error",
|
|
733
|
+
message: "exception fetching message details",
|
|
734
|
+
meta: { messageGuid, reason: error instanceof Error ? error.message : String(error) },
|
|
735
|
+
});
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
},
|
|
690
739
|
};
|
|
691
740
|
}
|
|
@@ -34,7 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.BB_IN_FLIGHT_MAX_AGE_MS = void 0;
|
|
37
|
-
exports.
|
|
37
|
+
exports.resolveReactionTarget = resolveReactionTarget;
|
|
38
38
|
exports.tokenizeForDedupe = tokenizeForDedupe;
|
|
39
39
|
exports.jaccardSimilarity = jaccardSimilarity;
|
|
40
40
|
exports.createStatusBatcher = createStatusBatcher;
|
|
@@ -116,16 +116,19 @@ const bbInFlightMessageClaims = new Map();
|
|
|
116
116
|
* a leaked marker from blocking forward progress for hours.
|
|
117
117
|
*/
|
|
118
118
|
exports.BB_IN_FLIGHT_MAX_AGE_MS = 15 * 60_000;
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
// Resolve the message a reaction points at, so the agent is told *what* was
|
|
120
|
+
// reacted to and *whose* message it was rather than a bare "reacted with love".
|
|
121
|
+
// Prefers the authorship-carrying lookup; falls back to text-only when the client
|
|
122
|
+
// does not provide one. Every failure path returns an explicit "unknown" rather
|
|
123
|
+
// than a value the caller could mistake for a resolved target.
|
|
124
|
+
async function resolveReactionTarget(client, targetMessageGuid) {
|
|
125
|
+
const base = { guid: targetMessageGuid };
|
|
126
|
+
if (client.getMessageDetails) {
|
|
127
|
+
const details = await client.getMessageDetails(targetMessageGuid).catch(() => null);
|
|
128
|
+
return { ...base, text: details?.text ?? null, fromMe: details?.fromMe ?? null };
|
|
129
|
+
}
|
|
130
|
+
const text = await client.getMessageText(targetMessageGuid).catch(() => null);
|
|
131
|
+
return { ...base, text, fromMe: null };
|
|
129
132
|
}
|
|
130
133
|
// ── Near-duplicate outward-text detection ────────────────────────
|
|
131
134
|
// Used by createBlueBubblesCallbacks to collapse mid-turn rephrasings of the
|
|
@@ -1390,13 +1393,14 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1390
1393
|
meta: { threadGuid, hasText: !!repliedToText },
|
|
1391
1394
|
});
|
|
1392
1395
|
}
|
|
1393
|
-
//
|
|
1396
|
+
// Anchor reaction mutations to the message they point at, including whose
|
|
1397
|
+
// message it was. Unresolved targets keep the explicit "unidentified message"
|
|
1398
|
+
// wording that normalizeBlueBubblesEvent already produced.
|
|
1394
1399
|
const isReaction = event.kind === "mutation" && event.mutationType === "reaction";
|
|
1395
|
-
if (isReaction && event.targetMessageGuid) {
|
|
1396
|
-
/* v8 ignore start -- best-effort lookup;
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
event.textForAgent = enrichReactionText(event.textForAgent, originalText, 80);
|
|
1400
|
+
if (isReaction && event.reaction && event.targetMessageGuid) {
|
|
1401
|
+
/* v8 ignore start -- best-effort network lookup; resolveReactionTarget and renderBlueBubblesReactionText are covered by unit tests @preserve */
|
|
1402
|
+
const target = await resolveReactionTarget(client, event.targetMessageGuid);
|
|
1403
|
+
event.textForAgent = (0, model_1.renderBlueBubblesReactionText)(event.reaction, target);
|
|
1400
1404
|
/* v8 ignore stop */
|
|
1401
1405
|
}
|
|
1402
1406
|
// Build inbound user message as user-visible speech only; source/routing facts live in the orientation frame.
|
|
@@ -1411,6 +1415,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source, opt
|
|
|
1411
1415
|
currentUserMessages: [userMessage],
|
|
1412
1416
|
structuredOutputs: existing?.structuredOutputs ?? [],
|
|
1413
1417
|
source: buildBlueBubblesOrientationSource(event, priorMessages, repliedToText),
|
|
1418
|
+
...(isReaction ? { speechKind: "reaction" } : {}),
|
|
1414
1419
|
});
|
|
1415
1420
|
const visibleActivityTurnId = activeTurnId;
|
|
1416
1421
|
const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget, event.chat.isGroup, visibleActivityTurnId
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BlueBubblesIgnoredEventError = void 0;
|
|
4
|
+
exports.describeBlueBubblesReaction = describeBlueBubblesReaction;
|
|
5
|
+
exports.renderBlueBubblesReactionText = renderBlueBubblesReactionText;
|
|
4
6
|
exports.normalizeBlueBubblesEvent = normalizeBlueBubblesEvent;
|
|
5
7
|
const runtime_1 = require("../../nerves/runtime");
|
|
6
8
|
const render_1 = require("../../heart/attachments/render");
|
|
@@ -150,11 +152,70 @@ function formatMessageText(data, attachments) {
|
|
|
150
152
|
return formatAttachmentText(attachments);
|
|
151
153
|
}
|
|
152
154
|
function normalizeReactionName(value) {
|
|
155
|
+
// chat.db stores associated_message_type as an integer; BlueBubbles forwards it
|
|
156
|
+
// as either the integer, its decimal string, or a name ("love", "-love").
|
|
157
|
+
if (typeof value === "number") {
|
|
158
|
+
return Number.isFinite(value) && value !== 0 ? String(value) : undefined;
|
|
159
|
+
}
|
|
153
160
|
if (typeof value !== "string")
|
|
154
161
|
return undefined;
|
|
155
162
|
const trimmed = value.trim();
|
|
156
163
|
return trimmed ? trimmed.toLowerCase() : undefined;
|
|
157
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* iMessage tapbacks: associated_message_type 2000-2005 add a tapback, 3000-3005
|
|
167
|
+
* remove the same tapback. BlueBubbles may send either the code or the name.
|
|
168
|
+
*/
|
|
169
|
+
const REACTION_VOCABULARY = [
|
|
170
|
+
{ code: 2000, name: "love", verb: "loved", noun: "love" },
|
|
171
|
+
{ code: 2001, name: "like", verb: "liked", noun: "like" },
|
|
172
|
+
{ code: 2002, name: "dislike", verb: "disliked", noun: "dislike" },
|
|
173
|
+
{ code: 2003, name: "laugh", verb: "laughed at", noun: "laugh" },
|
|
174
|
+
{ code: 2004, name: "emphasize", verb: "emphasized", noun: "emphasis" },
|
|
175
|
+
{ code: 2005, name: "question", verb: "questioned", noun: "question" },
|
|
176
|
+
];
|
|
177
|
+
const REACTION_EXCERPT_MAX_LENGTH = 80;
|
|
178
|
+
function describeBlueBubblesReaction(raw) {
|
|
179
|
+
const stripped = raw.startsWith("-") ? raw.slice(1) : raw;
|
|
180
|
+
const code = /^\d+$/.test(stripped) ? Number(stripped) : undefined;
|
|
181
|
+
const removal = raw.startsWith("-") || (code !== undefined && code >= 3000 && code < 4000);
|
|
182
|
+
const action = removal ? "remove" : "add";
|
|
183
|
+
const entry = REACTION_VOCABULARY.find((candidate) => candidate.name === stripped || candidate.code === code || candidate.code + 1000 === code);
|
|
184
|
+
if (!entry)
|
|
185
|
+
return { raw, action };
|
|
186
|
+
return { raw, action, verb: entry.verb, noun: entry.noun };
|
|
187
|
+
}
|
|
188
|
+
function reactionExcerpt(text) {
|
|
189
|
+
return text.length > REACTION_EXCERPT_MAX_LENGTH
|
|
190
|
+
? `${text.slice(0, REACTION_EXCERPT_MAX_LENGTH - 3)}...`
|
|
191
|
+
: text;
|
|
192
|
+
}
|
|
193
|
+
function reactionTargetPhrase(target) {
|
|
194
|
+
const text = target.text?.trim();
|
|
195
|
+
if (text) {
|
|
196
|
+
const owner = target.fromMe === true ? "your" : target.fromMe === false ? "their" : "a";
|
|
197
|
+
return `${owner} message: "${reactionExcerpt(text)}"`;
|
|
198
|
+
}
|
|
199
|
+
// Never hand the agent a bare "reacted with love" stub — say plainly that the
|
|
200
|
+
// referent is missing so it can ask instead of guessing what was approved.
|
|
201
|
+
return target.guid
|
|
202
|
+
? `an unidentified message (target guid ${target.guid}; its text could not be resolved)`
|
|
203
|
+
: "an unidentified message (the reaction carried no target message reference)";
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Render a reaction as speech the agent can act on: what the tapback was, whether
|
|
207
|
+
* it was added or removed, and which message it points at.
|
|
208
|
+
*/
|
|
209
|
+
function renderBlueBubblesReactionText(reaction, target) {
|
|
210
|
+
const phrase = reactionTargetPhrase(target);
|
|
211
|
+
if (reaction.action === "remove") {
|
|
212
|
+
const noun = reaction.noun ?? reaction.raw.replace(/^-/, "");
|
|
213
|
+
return `removed their ${noun} reaction from ${phrase}`;
|
|
214
|
+
}
|
|
215
|
+
return reaction.verb
|
|
216
|
+
? `${reaction.verb} ${phrase}`
|
|
217
|
+
: `reacted with ${reaction.raw} to ${phrase}`;
|
|
218
|
+
}
|
|
158
219
|
function stripPartPrefix(guid) {
|
|
159
220
|
if (!guid)
|
|
160
221
|
return undefined;
|
|
@@ -162,10 +223,10 @@ function stripPartPrefix(guid) {
|
|
|
162
223
|
const marker = trimmed.lastIndexOf("/");
|
|
163
224
|
return marker >= 0 ? trimmed.slice(marker + 1) : trimmed;
|
|
164
225
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
226
|
+
// Reactions never reach here: `detectMutationType` returns "reaction" only when a
|
|
227
|
+
// reaction name was decoded, and that same name produces the descriptor the caller
|
|
228
|
+
// renders with `renderBlueBubblesReactionText` instead.
|
|
229
|
+
function buildMutationText(mutationType, data) {
|
|
169
230
|
if (mutationType === "edit") {
|
|
170
231
|
const editedText = readString(data, "text")?.trim() ?? "";
|
|
171
232
|
return editedText ? `edited message: ${editedText}` : "edited a message";
|
|
@@ -228,6 +289,10 @@ function normalizeBlueBubblesEvent(payload) {
|
|
|
228
289
|
const fromMe = readBoolean(data, "isFromMe") ?? false;
|
|
229
290
|
const attachments = extractAttachments(data);
|
|
230
291
|
const reactionName = normalizeReactionName(data.associatedMessageType);
|
|
292
|
+
const reaction = reactionName ? describeBlueBubblesReaction(reactionName) : undefined;
|
|
293
|
+
const targetMessageGuid = reaction
|
|
294
|
+
? stripPartPrefix(readString(data, "associatedMessageGuid"))
|
|
295
|
+
: undefined;
|
|
231
296
|
const mutationType = detectMutationType(eventType, data, reactionName);
|
|
232
297
|
const requiresRepair = (readBoolean(data, "hasPayloadData") ?? false) ||
|
|
233
298
|
attachments.length > 0 ||
|
|
@@ -238,16 +303,17 @@ function normalizeBlueBubblesEvent(payload) {
|
|
|
238
303
|
eventType,
|
|
239
304
|
mutationType,
|
|
240
305
|
messageGuid,
|
|
241
|
-
targetMessageGuid
|
|
242
|
-
? stripPartPrefix(readString(data, "associatedMessageGuid"))
|
|
243
|
-
: undefined,
|
|
306
|
+
targetMessageGuid,
|
|
244
307
|
timestamp,
|
|
245
308
|
fromMe,
|
|
246
309
|
sender,
|
|
247
310
|
chat,
|
|
248
311
|
shouldNotifyAgent: mutationType === "reaction" || mutationType === "edit" || mutationType === "unsend",
|
|
249
|
-
textForAgent:
|
|
312
|
+
textForAgent: reaction
|
|
313
|
+
? renderBlueBubblesReactionText(reaction, { guid: targetMessageGuid })
|
|
314
|
+
: buildMutationText(mutationType, data),
|
|
250
315
|
requiresRepair,
|
|
316
|
+
...(reaction ? { reaction } : {}),
|
|
251
317
|
}
|
|
252
318
|
: {
|
|
253
319
|
kind: "message",
|