@ouro.bot/cli 0.1.0-alpha.262 → 0.1.0-alpha.264

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,18 @@
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.264",
6
+ "changes": [
7
+ "fix(bluebubbles): dedup `updated-message` webhooks BEFORE running repair+hydrate+VLM. BlueBubbles routinely sends a `new-message` webhook for a fresh message, then follows up seconds later with one or more `updated-message` webhooks for delivery/read status. The BB sense's `repairEvent` path promotes updated-message events with recoverable content back to `message` kind, which re-runs the full hydration pipeline \u2014 including a second MiniMax VLM describe call on the same image. Verified live on 2026-04-08T00:58Z: two sequential VLM describes for attachment guid 317E37EB-..., 13.7s + 14.0s each, for the exact same 291KB JPEG, triggered by a `new-message` followed 3s later by an `updated-message` for the same guid. The downstream `handleBlueBubblesNormalizedEvent` dedup check was firing correctly but too late (after the expensive VLM round-trip). Fix: add a pre-repair dedup check in `handleBlueBubblesEvent` that consults the inbound sidecar by messageGuid and short-circuits before calling `client.repairEvent(...)`. New nerves event `senses.bluebubbles_repair_skipped_duplicate` at warn level for observability."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.263",
12
+ "changes": [
13
+ "fix(tui): arrow keys char-by-char (Ink 3.2 key.meta bug), Option+Arrow word-jump via CSI sequences."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.262",
6
18
  "changes": [
@@ -859,7 +859,43 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
859
859
  async function handleBlueBubblesEvent(payload, deps = {}) {
860
860
  const resolvedDeps = { ...defaultDeps, ...deps };
861
861
  const client = resolvedDeps.createClient();
862
- const event = await client.repairEvent((0, model_1.normalizeBlueBubblesEvent)(payload));
862
+ const normalized = (0, model_1.normalizeBlueBubblesEvent)(payload);
863
+ // Pre-repair dedup: if we've already processed this messageGuid, skip the
864
+ // repair+hydrate path entirely. Applies to BOTH `kind: "message"` AND
865
+ // `kind: "mutation"` events — BlueBubbles often sends a `new-message`
866
+ // webhook for a fresh message AND one or more follow-up `updated-message`
867
+ // webhooks for delivery/read status. The mutation path (inside repairEvent)
868
+ // can promote an updated-message back to a message if it has recoverable
869
+ // content, which then re-runs the full VLM-describe pipeline on the same
870
+ // attachment.
871
+ //
872
+ // Without this early check, we paid DOUBLE latency and double tokens on
873
+ // every image-bearing message. Verified live on 2026-04-08T00:58Z: two
874
+ // sequential VLM describes for attachment guid 317E37EB-..., 13.7s +
875
+ // 14.0s each, for the exact same 291KB JPEG — triggered by a sequence of
876
+ // `new-message` followed ~3s later by `updated-message` for the same guid.
877
+ //
878
+ // We still route the skip through `handleBlueBubblesNormalizedEvent` so
879
+ // the downstream `already_processed` path fires its observability events
880
+ // and the caller sees a consistent return shape.
881
+ const agentName = resolvedDeps.getAgentName();
882
+ if (normalized.messageGuid
883
+ && (0, inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, normalized.chat.sessionKey, normalized.messageGuid)) {
884
+ (0, runtime_1.emitNervesEvent)({
885
+ level: "warn",
886
+ component: "senses",
887
+ event: "senses.bluebubbles_repair_skipped_duplicate",
888
+ message: "skipped repair+hydrate for already-processed bluebubbles messageGuid",
889
+ meta: {
890
+ messageGuid: normalized.messageGuid,
891
+ sessionKey: normalized.chat.sessionKey,
892
+ eventType: normalized.eventType,
893
+ normalizedKind: normalized.kind,
894
+ },
895
+ });
896
+ return handleBlueBubblesNormalizedEvent(normalized, resolvedDeps, "webhook");
897
+ }
898
+ const event = await client.repairEvent(normalized);
863
899
  return handleBlueBubblesNormalizedEvent(event, resolvedDeps, "webhook");
864
900
  }
865
901
  function countPendingRecoveryCandidates(agentName) {
@@ -119,6 +119,12 @@ function classifyEscapeSequence(inputChar) {
119
119
  // End: \x1b[F or \x1b[4~
120
120
  if (seq === "F" || seq === "4~")
121
121
  return "end";
122
+ // Option+Left: \x1b[1;3D (CSI modified arrow with Alt modifier)
123
+ if (seq === "1;3D")
124
+ return "word-left";
125
+ // Option+Right: \x1b[1;3C
126
+ if (seq === "1;3C")
127
+ return "word-right";
122
128
  // PageUp: \x1b[5~, PageDown: \x1b[6~
123
129
  if (seq === "5~" || seq === "6~")
124
130
  return "ignore";
@@ -324,39 +324,20 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
324
324
  historyIdx.current = -1;
325
325
  return;
326
326
  }
327
- // Left/right arrow: move cursor
327
+ // Left/right arrow: move cursor (char by char, token-aware)
328
+ // NOTE: Ink 3.2 bug — key.meta is ALWAYS true for arrow keys because all
329
+ // arrows start with \x1b and Ink sets meta=true for any ESC-prefixed input.
330
+ // Word-jump is handled via Meta+B/F (emacs bindings) instead.
328
331
  if (key.leftArrow) {
329
- if (key.meta) {
330
- // Option+Left: jump to previous word boundary
331
- const before = inputRef.current.slice(0, cursorRef.current);
332
- const match = before.match(/(?:^|\s)\S+\s*$/);
333
- const newPos = match ? cursorRef.current - match[0].length + (match[0].startsWith(" ") ? 1 : 0) : 0;
334
- cursorRef.current = Math.max(0, newPos);
335
- setCursorPos(cursorRef.current);
336
- }
337
- else {
338
- // Token-aware: hop over image ref chips
339
- const chipStart = (0, image_ref_navigation_1.imageRefEndingAt)(inputRef.current, cursorRef.current);
340
- cursorRef.current = chipStart !== undefined ? chipStart : Math.max(0, cursorRef.current - 1);
341
- setCursorPos(cursorRef.current);
342
- }
332
+ const chipStart = (0, image_ref_navigation_1.imageRefEndingAt)(inputRef.current, cursorRef.current);
333
+ cursorRef.current = chipStart !== undefined ? chipStart : Math.max(0, cursorRef.current - 1);
334
+ setCursorPos(cursorRef.current);
343
335
  return;
344
336
  }
345
337
  if (key.rightArrow) {
346
- if (key.meta) {
347
- // Option+Right: jump to next word boundary
348
- const after = inputRef.current.slice(cursorRef.current);
349
- const match = after.match(/^\s*\S+/);
350
- const newPos = match ? cursorRef.current + match[0].length : inputRef.current.length;
351
- cursorRef.current = Math.min(inputRef.current.length, newPos);
352
- setCursorPos(cursorRef.current);
353
- }
354
- else {
355
- // Token-aware: hop over image ref chips
356
- const chipEnd = (0, image_ref_navigation_1.imageRefStartingAt)(inputRef.current, cursorRef.current);
357
- cursorRef.current = chipEnd !== undefined ? chipEnd : Math.min(inputRef.current.length, cursorRef.current + 1);
358
- setCursorPos(cursorRef.current);
359
- }
338
+ const chipEnd = (0, image_ref_navigation_1.imageRefStartingAt)(inputRef.current, cursorRef.current);
339
+ cursorRef.current = chipEnd !== undefined ? chipEnd : Math.min(inputRef.current.length, cursorRef.current + 1);
340
+ setCursorPos(cursorRef.current);
360
341
  return;
361
342
  }
362
343
  // Up/Down: queue pop takes priority over history
@@ -542,22 +523,38 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
542
523
  // ─── Non-kill/non-yank keystroke resets ────────────────────────
543
524
  killRing.resetAccumulation();
544
525
  killRing.resetYankState();
526
+ // Detect raw escape sequences — Ink 3.2 strips \x1b prefix and sets key.meta,
527
+ // so we re-prepend it for classification when meta is set and inputChar starts with [
528
+ const escInput = key.meta && inputChar.startsWith("[") ? "\x1b" + inputChar : inputChar;
529
+ const escClass = (0, input_keys_1.classifyEscapeSequence)(escInput);
530
+ if (escClass === "home") {
531
+ cursorRef.current = (0, input_keys_1.handleHome)(inputRef.current, cursorRef.current);
532
+ setCursorPos(cursorRef.current);
533
+ return;
534
+ }
535
+ if (escClass === "end") {
536
+ cursorRef.current = (0, input_keys_1.handleEnd)(inputRef.current, cursorRef.current);
537
+ setCursorPos(cursorRef.current);
538
+ return;
539
+ }
540
+ if (escClass === "word-left") {
541
+ const before = inputRef.current.slice(0, cursorRef.current);
542
+ const match = before.match(/(?:^|\s)\S+\s*$/);
543
+ cursorRef.current = match ? Math.max(0, cursorRef.current - match[0].length + (match[0].startsWith(" ") ? 1 : 0)) : 0;
544
+ setCursorPos(cursorRef.current);
545
+ return;
546
+ }
547
+ if (escClass === "word-right") {
548
+ const after = inputRef.current.slice(cursorRef.current);
549
+ const match = after.match(/^\s*\S+/);
550
+ cursorRef.current = match ? Math.min(inputRef.current.length, cursorRef.current + match[0].length) : inputRef.current.length;
551
+ setCursorPos(cursorRef.current);
552
+ return;
553
+ }
554
+ if (escClass === "ignore")
555
+ return;
545
556
  // Regular character: insert at cursor position
546
557
  if (!key.ctrl && !key.meta && inputChar) {
547
- // Detect raw escape sequences before inserting text
548
- const escClass = (0, input_keys_1.classifyEscapeSequence)(inputChar);
549
- if (escClass === "home") {
550
- cursorRef.current = (0, input_keys_1.handleHome)(inputRef.current, cursorRef.current);
551
- setCursorPos(cursorRef.current);
552
- return;
553
- }
554
- if (escClass === "end") {
555
- cursorRef.current = (0, input_keys_1.handleEnd)(inputRef.current, cursorRef.current);
556
- setCursorPos(cursorRef.current);
557
- return;
558
- }
559
- if (escClass === "ignore")
560
- return; // PageUp/PageDown/mouse wheel fallback
561
558
  const before = inputRef.current.slice(0, cursorRef.current);
562
559
  const after = inputRef.current.slice(cursorRef.current);
563
560
  updateInput(before + inputChar + after, cursorRef.current + 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.262",
3
+ "version": "0.1.0-alpha.264",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",