@cello-protocol/cli 0.0.141 → 0.0.143

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.
@@ -1 +1 @@
1
- {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/hermes/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,w4CA6B9B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,QAkjCjC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,eAAe,60GAiE3B,CAAC"}
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../../src/hermes/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,w4CA6B9B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,QAmmCjC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,eAAe,60GAiE3B,CAAC"}
@@ -150,6 +150,10 @@ CALL_TIMEOUT_SECONDS = 30.0
150
150
  # Server-side wait for the adapter's own cello_receive. Short on purpose: the notification that
151
151
  # triggered it means the content is already durable, so this is a fetch, not a poll.
152
152
  RECEIVE_TIMEOUT_MS = 5000
153
+ # Upper bound on one drain. A conversation that has been away a long time can have a lot
154
+ # queued; handing an agent an unbounded turn is its own failure. Hitting this logs loudly -
155
+ # a silent truncation would read as "the agent saw everything" when it did not.
156
+ MAX_DRAIN_MESSAGES = 25
153
157
  # Matches the daemon IPC server's MAX_BUFFER_SIZE (4 MB).
154
158
  MAX_LINE_BYTES = 4 * 1024 * 1024
155
159
 
@@ -591,45 +595,90 @@ class CelloAdapter(BasePlatformAdapter):
591
595
  notifications per connection (so this is the connection that was woken), and the
592
596
  read-before-send gate (M8C-CURSOR-1) tracks a per-connection cursor - so reading here is
593
597
  exactly what later lets send() through on the same socket.
598
+
599
+ DRAIN, not a single read. cello_receive serves THIS CONNECTION's oldest unread message,
600
+ not the one the notification just announced - the two are the same only when the
601
+ connection is already caught up. On a conversation with any history behind it (which is
602
+ every existing conversation the first time this adapter attaches) the first read returns
603
+ a message from minutes ago, and the agent answers the wrong thing.
604
+
605
+ Worse, it then cannot answer at all: the read-before-send gate refuses a send while
606
+ anything is still unread, so the reply the agent just wrote is REFUSED and lost - the
607
+ agent believes it answered and the peer hears nothing. Observed live 2026-08-07 on
608
+ session 9bc456f6: adapter read seq 0 (five minutes stale), agent replied, two
609
+ session.send.blocked with unreadReceived=1, reply gone.
610
+
611
+ Draining fixes both: the agent sees everything waiting, in order, and the gate is clear
612
+ by the time it answers.
594
613
  """
595
- try:
596
- # An explicit SHORT server-side wait. The daemon's default is 30 s, which is exactly
597
- # CALL_TIMEOUT_SECONDS - so the two would race and the client timeout could fire on a
598
- # call the daemon was about to answer. The content this notification announces is
599
- # already durable, so a long wait buys nothing.
600
- result = await self._call(
601
- "cello_receive",
602
- {"session_id": session_id, "timeout_ms": RECEIVE_TIMEOUT_MS},
603
- timeout=RECEIVE_TIMEOUT_MS / 1000.0 + 5.0,
604
- )
605
- except Exception as exc:
606
- # Name the EXCEPTION TYPE: asyncio.TimeoutError stringifies to "", so "%s" alone
607
- # produced a log line that named a session and no cause at all - pointing the operator
608
- # at the daemon when the fault could be entirely local.
609
- logger.error(
610
- "[cello] Could not fetch content for session %s (%s: %r) - falling back to a wake "
611
- "notice so the agent can still read it through the cello_* MCP tools",
612
- session_id, exc.__class__.__name__, exc,
613
- )
614
- return None
615
- if not isinstance(result, dict) or result.get("ok") is False:
616
- reason = result.get("reason") if isinstance(result, dict) else "malformed_response"
617
- logger.error(
618
- "[cello] cello_receive refused session %s (%s) - falling back to a wake notice",
619
- session_id, reason,
620
- )
621
- return None
622
- content = result.get("content")
623
- if not isinstance(content, str) or not content:
624
- # An 'ok' answer with nothing in it (timed out, or a sibling connection took the
625
- # message). An empty user turn tells the agent nothing; the wake notice at least
626
- # names the session.
614
+ parts: list = []
615
+ for attempt in range(MAX_DRAIN_MESSAGES):
616
+ try:
617
+ # First read waits briefly for the announced message; every later read is
618
+ # non-blocking (timeout_ms 0) because it is only draining what is ALREADY there.
619
+ # An explicit SHORT server-side wait on the first: the daemon's default is 30 s,
620
+ # exactly CALL_TIMEOUT_SECONDS, so the two would race and the client could give up
621
+ # on a call the daemon was about to answer.
622
+ wait_ms = RECEIVE_TIMEOUT_MS if attempt == 0 else 0
623
+ result = await self._call(
624
+ "cello_receive",
625
+ {"session_id": session_id, "timeout_ms": wait_ms},
626
+ timeout=wait_ms / 1000.0 + 5.0,
627
+ )
628
+ except Exception as exc:
629
+ # Name the EXCEPTION TYPE: asyncio.TimeoutError stringifies to "", so "%s" alone
630
+ # produced a log line that named a session and no cause at all - pointing the
631
+ # operator at the daemon when the fault could be entirely local.
632
+ logger.error(
633
+ "[cello] Could not fetch content for session %s (%s: %r) - %s",
634
+ session_id, exc.__class__.__name__, exc,
635
+ "delivering the %d message(s) already read" % len(parts) if parts
636
+ else "falling back to a wake notice so the agent can still read it through "
637
+ "the cello_* MCP tools",
638
+ )
639
+ break
640
+ if not isinstance(result, dict) or result.get("ok") is False:
641
+ reason = result.get("reason") if isinstance(result, dict) else "malformed_response"
642
+ # Only the FIRST read's refusal is a failure to report; a later one just means the
643
+ # drain reached the end (e.g. the session sealed between reads).
644
+ if not parts:
645
+ logger.error(
646
+ "[cello] cello_receive refused session %s (%s) - falling back to a wake "
647
+ "notice", session_id, reason,
648
+ )
649
+ break
650
+ content = result.get("content")
651
+ if not isinstance(content, str) or not content:
652
+ # 'ok' with nothing in it: the queue is empty. On the FIRST read that means the
653
+ # message went to a sibling connection or timed out, and an empty user turn tells
654
+ # the agent nothing - the wake notice at least names the session.
655
+ if not parts:
656
+ logger.warning(
657
+ "[cello] cello_receive returned no content for session %s - falling back "
658
+ "to a wake notice", session_id,
659
+ )
660
+ break
661
+ parts.append(content)
662
+ else:
663
+ # Hit the cap with more possibly waiting. Say so: a silent truncation here reads as
664
+ # "the agent saw everything" when it did not, and the unread tail will keep the
665
+ # read-before-send gate closed.
627
666
  logger.warning(
628
- "[cello] cello_receive returned no content for session %s - falling back to a "
629
- "wake notice", session_id,
667
+ "[cello] Stopped draining session %s at %d messages; any remaining are still "
668
+ "unread and may block this agent's next reply until it catches up",
669
+ session_id, MAX_DRAIN_MESSAGES,
630
670
  )
671
+
672
+ if not parts:
631
673
  return None
632
- return content
674
+ if len(parts) > 1:
675
+ logger.info(
676
+ "[cello] Delivered %d queued messages for session %s as one turn",
677
+ len(parts), session_id,
678
+ )
679
+ # Joined into ONE turn rather than emitted as several events: they share a session, so
680
+ # they share an anchor, and one turn means one reply - which is what the peer expects.
681
+ return "\n\n".join(parts)
633
682
 
634
683
  async def _on_notification(self, frame: Dict[str, Any]) -> None:
635
684
  kind = str(frame.get("notification", ""))
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../src/hermes/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BjC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkjC9C,CAAC;AAEF,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiE9B,CAAC"}
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../../src/hermes/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BjC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmmC9C,CAAC;AAEF,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiE9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cello-protocol/cli",
3
- "version": "0.0.141",
3
+ "version": "0.0.143",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -17,8 +17,8 @@
17
17
  "package.json"
18
18
  ],
19
19
  "dependencies": {
20
- "@cello-protocol/protocol-types": "0.0.47",
21
- "@cello-protocol/daemon": "0.0.137"
20
+ "@cello-protocol/daemon": "0.0.138",
21
+ "@cello-protocol/protocol-types": "0.0.47"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.6.2"