@ouro.bot/cli 0.1.0-alpha.568 → 0.1.0-alpha.569

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,14 @@
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.569",
6
+ "changes": [
7
+ "BlueBubbles ordinary chat turns no longer trigger external-state proof retry loops merely because the inbound text populated currentObligation.",
8
+ "BlueBubbles now suppresses duplicate normalized outward text within a single inbound turn across flush and flushNow delivery paths, preventing retry loops from sending repeated iMessages.",
9
+ "Regression coverage locks the 2026-05-08 duplicate outward-send incident while preserving distinct same-turn replies and per-turn re-sends."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.568",
6
14
  "changes": [
@@ -296,7 +296,7 @@ function isExternalStateQuery(toolName, args) {
296
296
  const cmd = String(args.command ?? "");
297
297
  return /\bgh\s+(pr|run|api|issue)\b/.test(cmd) || /\bnpm\s+(view|info|show)\b/.test(cmd);
298
298
  }
299
- function getSettleRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp, _delegationDecision, sawSendMessageSelf, sawPonder, _sawQuerySession, currentObligation, innerJob, sawExternalStateQuery) {
299
+ function getSettleRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollowUp, _delegationDecision, sawSendMessageSelf, sawPonder, _sawQuerySession, currentObligation, innerJob, _sawExternalStateQuery) {
300
300
  // Delegation adherence removed: the delegation decision is surfaced in the
301
301
  // system prompt as a suggestion. Hard-gating settle caused infinite
302
302
  // rejection loops where the agent couldn't respond to the user at all.
@@ -317,10 +317,6 @@ function getSettleRetryError(mustResolveBeforeHandoff, intent, sawSteeringFollow
317
317
  if (mustResolveBeforeHandoff && intent === "complete" && currentObligation && !sawSteeringFollowUp) {
318
318
  return "you still owe the live session a visible return on this work. don't end the turn yet — continue until you've brought back the external-state update, or use intent=blocked with the concrete blocker.";
319
319
  }
320
- // 6. External-state grounding: obligation + complete requires fresh external verification
321
- if (intent === "complete" && currentObligation && !sawExternalStateQuery && !sawSteeringFollowUp) {
322
- return "you're claiming this work is complete, but the external state hasn't been verified this turn. ground your claim with a fresh check (gh pr view, npm view, gh run view, etc.) before calling settle.";
323
- }
324
320
  return null;
325
321
  }
326
322
  function upsertSystemPrompt(messages, systemText) {
@@ -458,6 +458,15 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
458
458
  let queue = Promise.resolve();
459
459
  let lastVisibleActivityMs = Date.now();
460
460
  let silenceWatchdog = null;
461
+ // Per-turn outward-send dedupe. A single createBlueBubblesCallbacks lifetime
462
+ // serves one inbound turn, so collapsing identical outward bodies inside
463
+ // this closure is scoped tightly: each fresh inbound turn starts with an
464
+ // empty set. Mid-turn settle/proof retry loops historically caused the
465
+ // agent to re-emit the same answer through multiple speak calls and a
466
+ // final flush, surfacing as 4 near-identical iMessages from one ask
467
+ // (2026-05-08 06:18 incident). The guard lets the engine retry harmlessly
468
+ // without duplicating outward delivery to the friend.
469
+ const sentOutwardTextNorms = new Set();
461
470
  function enqueue(operation, task) {
462
471
  queue = queue.then(task).catch((error) => {
463
472
  (0, runtime_1.emitNervesEvent)({
@@ -491,6 +500,26 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
491
500
  lastVisibleActivityMs = Date.now();
492
501
  onVisibleActivity?.();
493
502
  }
503
+ function isDuplicateOutwardText(trimmed) {
504
+ const norm = trimmed.replace(/\s+/g, " ").trim().toLowerCase();
505
+ if (sentOutwardTextNorms.has(norm))
506
+ return true;
507
+ sentOutwardTextNorms.add(norm);
508
+ return false;
509
+ }
510
+ function emitDuplicateOutwardSuppressed(site, messageLength) {
511
+ (0, runtime_1.emitNervesEvent)({
512
+ level: "warn",
513
+ component: "senses",
514
+ event: "bluebubbles.duplicate_outward_suppressed",
515
+ message: "suppressed near-identical outward send within single inbound turn",
516
+ meta: {
517
+ site,
518
+ chatGuid: chat.chatGuid ?? null,
519
+ messageLength,
520
+ },
521
+ });
522
+ }
494
523
  function stopSilenceWatchdog() {
495
524
  if (silenceWatchdog === null)
496
525
  return;
@@ -628,6 +657,10 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
628
657
  });
629
658
  return;
630
659
  }
660
+ if (isDuplicateOutwardText(trimmed)) {
661
+ emitDuplicateOutwardSuppressed("flushNow", trimmed.length);
662
+ return;
663
+ }
631
664
  await client.sendText({
632
665
  chat,
633
666
  text: trimmed,
@@ -673,6 +706,10 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
673
706
  });
674
707
  return;
675
708
  }
709
+ if (isDuplicateOutwardText(trimmed)) {
710
+ emitDuplicateOutwardSuppressed("flush", trimmed.length);
711
+ return;
712
+ }
676
713
  await client.sendText({
677
714
  chat,
678
715
  text: trimmed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.568",
3
+ "version": "0.1.0-alpha.569",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",