@ouro.bot/cli 0.1.0-alpha.581 → 0.1.0-alpha.582

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,13 @@
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.582",
6
+ "changes": [
7
+ "BlueBubbles now suppresses same-turn near-duplicate outward sends using token/Jaccard similarity, covering rewritten retry answers, repeated status messages, and retry-loop variants without blocking short distinct replies.",
8
+ "BlueBubbles status sends now pass through the same duplicate guard as normal outward text, preserving inner/meta leak protections while reducing repeated iMessage status loops."
9
+ ]
10
+ },
4
11
  {
5
12
  "version": "0.1.0-alpha.581",
6
13
  "changes": [
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.enrichReactionText = enrichReactionText;
37
+ exports.tokenizeForDedupe = tokenizeForDedupe;
38
+ exports.jaccardSimilarity = jaccardSimilarity;
37
39
  exports.createStatusBatcher = createStatusBatcher;
38
40
  exports.createBlueBubblesCallbacks = createBlueBubblesCallbacks;
39
41
  exports.isAgentSelfHandle = isAgentSelfHandle;
@@ -95,6 +97,29 @@ function enrichReactionText(baseText, originalText, maxLen) {
95
97
  : originalText;
96
98
  return `${baseText} to: "${truncated}"`;
97
99
  }
100
+ // ── Near-duplicate outward-text detection ────────────────────────
101
+ // Used by createBlueBubblesCallbacks to collapse mid-turn rephrasings of the
102
+ // same answer/status into a single delivery. Exposed for direct unit testing
103
+ // of the similarity behavior without spinning up the full callbacks closure.
104
+ const NEAR_DUPLICATE_JACCARD_THRESHOLD = 0.7;
105
+ const NEAR_DUPLICATE_MIN_TOKENS = 5;
106
+ function tokenizeForDedupe(text) {
107
+ const matches = text.toLowerCase().match(/[a-z0-9']+/g);
108
+ if (!matches)
109
+ return new Set();
110
+ return new Set(matches);
111
+ }
112
+ function jaccardSimilarity(a, b) {
113
+ if (a.size === 0 || b.size === 0)
114
+ return 0;
115
+ let intersection = 0;
116
+ for (const token of a) {
117
+ if (b.has(token))
118
+ intersection += 1;
119
+ }
120
+ const union = a.size + b.size - intersection;
121
+ return intersection / union;
122
+ }
98
123
  /**
99
124
  * Accumulates status descriptions and debounces them.
100
125
  * If multiple descriptions arrive within `delayMs`, they are joined with ` · `
@@ -466,7 +491,16 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
466
491
  // final flush, surfacing as 4 near-identical iMessages from one ask
467
492
  // (2026-05-08 06:18 incident). The guard lets the engine retry harmlessly
468
493
  // without duplicating outward delivery to the friend.
494
+ //
495
+ // PR #699 used exact whitespace+case-normalized match. Post-#699 evidence
496
+ // (2026-05-09 05:25 UTC, evt-001814 + evt-001818) showed two answers that
497
+ // start "yep — I looked it up… Assuming you mean AMC's The Audacity…" with
498
+ // substantially the same content but slight rephrasing — the LLM rewrites
499
+ // the same answer on a retry/recovery loop and bypasses an exact-match
500
+ // guard. We now also keep the original token sets so a fuzzy (Jaccard)
501
+ // check catches near-duplicates from the same turn.
469
502
  const sentOutwardTextNorms = new Set();
503
+ const sentOutwardTokenSets = [];
470
504
  function enqueue(operation, task) {
471
505
  queue = queue.then(task).catch((error) => {
472
506
  (0, runtime_1.emitNervesEvent)({
@@ -504,7 +538,25 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
504
538
  const norm = trimmed.replace(/\s+/g, " ").trim().toLowerCase();
505
539
  if (sentOutwardTextNorms.has(norm))
506
540
  return true;
541
+ // Fuzzy near-duplicate: when the same answer is rephrased between speak
542
+ // and settle (or across two speak calls in a recovery loop), the exact
543
+ // norm differs but token overlap is very high. We compare against every
544
+ // already-sent body's token set; if any has Jaccard overlap >= the
545
+ // threshold, treat as a duplicate. We require a minimum token count on
546
+ // the new body so single-word replies ("yes", "ok") don't get suppressed
547
+ // against any previous short one with shared tokens.
548
+ const newTokens = tokenizeForDedupe(trimmed);
549
+ if (newTokens.size >= NEAR_DUPLICATE_MIN_TOKENS) {
550
+ for (const prevTokens of sentOutwardTokenSets) {
551
+ if (prevTokens.size < NEAR_DUPLICATE_MIN_TOKENS)
552
+ continue;
553
+ if (jaccardSimilarity(newTokens, prevTokens) >= NEAR_DUPLICATE_JACCARD_THRESHOLD) {
554
+ return true;
555
+ }
556
+ }
557
+ }
507
558
  sentOutwardTextNorms.add(norm);
559
+ sentOutwardTokenSets.push(newTokens);
508
560
  return false;
509
561
  }
510
562
  function emitDuplicateOutwardSuppressed(site, messageLength) {
@@ -540,10 +592,25 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
540
592
  }
541
593
  }
542
594
  function sendStatus(text) {
595
+ const trimmed = text.trim();
596
+ /* v8 ignore next -- defensive guard; current status callers always provide non-empty text @preserve */
597
+ if (!trimmed)
598
+ return;
599
+ // Status surfaces share the per-turn dedupe set so a status-style
600
+ // outward message (tool description, error notice, watchdog ping)
601
+ // can't deliver a near-duplicate of an already-sent answer or status
602
+ // (post-#699 evidence: evt-001820 + evt-001821 + evt-001823 on
603
+ // 2026-05-09 — overlapping status surface updates about the same
604
+ // duplicate issue, which the flushNow/flush-only guard could not catch
605
+ // because sendStatus writes directly via client.sendText).
606
+ if (isDuplicateOutwardText(trimmed)) {
607
+ emitDuplicateOutwardSuppressed("status", trimmed.length);
608
+ return;
609
+ }
543
610
  enqueue("send_status", async () => {
544
611
  await client.sendText({
545
612
  chat,
546
- text,
613
+ text: trimmed,
547
614
  replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
548
615
  });
549
616
  recordVisibleActivity();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.581",
3
+ "version": "0.1.0-alpha.582",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",