@commonlyai/cli 0.1.88 → 0.1.89

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -1594,7 +1594,35 @@ export const performRun = ({
1594
1594
  uploadName: `${agentName}-reply-${event._id}.md`,
1595
1595
  log: (line) => log(`[${event.type}] ${line}`),
1596
1596
  });
1597
- if (delivery.refused) {
1597
+ if (delivery.skipped) {
1598
+ // The server answered 200 and created nothing: a duplicate inside its
1599
+ // dedupe window, a silent body, a heartbeat it keeps. Nothing reached
1600
+ // the pod, so this is not a delivery and must not read as one in the
1601
+ // log — "posted 151 bytes as 1 message" over a skip is how a silent
1602
+ // seat looked like a talking one (hq-support, 2026-10-09 08:10Z). Ack
1603
+ // it like a refusal: re-posting the same text inside the window would
1604
+ // only be skipped again.
1605
+ deliveryRefusal = delivery;
1606
+ const windowMinutes = delivery.duplicate?.dedupeWindowMinutes;
1607
+ const detail = [
1608
+ windowMinutes ? `${windowMinutes} min window` : null,
1609
+ delivery.duplicate?.id ? `duplicate of ${delivery.duplicate.id}` : null,
1610
+ delivery.attemptedMessages > 1
1611
+ ? `after ${delivery.messages}/${delivery.attemptedMessages} messages`
1612
+ : null,
1613
+ ].filter(Boolean).join(', ');
1614
+ log(`[${event.type}] server skipped the post: ${delivery.reason}${detail ? ` (${detail})` : ''} — nothing reached the pod`);
1615
+ onError?.(Object.assign(
1616
+ new Error(`${event.type} server skipped the post: ${delivery.reason}`),
1617
+ {
1618
+ code: 'agent_delivery_skipped',
1619
+ reason: delivery.reason,
1620
+ eventId: event._id,
1621
+ postedMessages: delivery.messages,
1622
+ attemptedMessages: delivery.attemptedMessages,
1623
+ },
1624
+ ));
1625
+ } else if (delivery.refused) {
1598
1626
  // A run-cap refusal is a successful HTTP request but not a delivery.
1599
1627
  // Ack it so the kernel does not replay the same text (the server
1600
1628
  // guidance expressly says not to retry unchanged), while preserving
@@ -1640,6 +1668,8 @@ export const performRun = ({
1640
1668
  if (deliveryRefusal) {
1641
1669
  return {
1642
1670
  outcome: 'no_action',
1671
+ // Kernel class, from REFUSAL_REASONS: a skip is the server declining to
1672
+ // deliver, same class as a refusal; the specific reason rides below.
1643
1673
  refused: { reason: 'delivery-refused' },
1644
1674
  reason: deliveryRefusal.reason,
1645
1675
  details: {
@@ -687,6 +687,41 @@ export const deliverChatReply = async ({
687
687
  ? { consecutive: response.consecutive }
688
688
  : {}),
689
689
  });
690
+ // The same route also answers HTTP 200 with `{ success: true, skipped: true,
691
+ // reason }` when it decided not to create a row: a duplicate inside the dedupe
692
+ // window, a silent or empty body, a heartbeat it keeps to itself. The wrapper
693
+ // used to read that as a delivery and log "posted … (single)" while nothing
694
+ // reached the pod (hq-support, 2026-10-09 08:10Z), so a silent seat could not
695
+ // be read from its own log. A skip is a completed request and not a delivery,
696
+ // exactly like a refusal, and it stops a sequence the same way.
697
+ const skipped = (response, messages, attemptedMessages) => {
698
+ const duplicate = response.duplicate && typeof response.duplicate === 'object'
699
+ ? response.duplicate
700
+ : null;
701
+ return {
702
+ mode: 'skipped',
703
+ messages,
704
+ attemptedMessages,
705
+ skipped: true,
706
+ reason: response.reason || 'message_skipped',
707
+ ...(duplicate
708
+ ? {
709
+ duplicate: {
710
+ ...(duplicate.id ? { id: String(duplicate.id) } : {}),
711
+ ...(typeof duplicate.dedupeWindowMinutes === 'number'
712
+ ? { dedupeWindowMinutes: duplicate.dedupeWindowMinutes }
713
+ : {}),
714
+ },
715
+ }
716
+ : {}),
717
+ };
718
+ };
719
+ const undelivered = (response) => response?.refused === true || response?.skipped === true;
720
+ const nonDelivery = (response, messages, attemptedMessages) => (
721
+ response?.refused === true
722
+ ? refused(response, messages, attemptedMessages)
723
+ : skipped(response, messages, attemptedMessages)
724
+ );
690
725
  // An atomic unit (a fenced block, an unbreakable word-run) can exceed the
691
726
  // limit by construction — splitForChat keeps it whole rather than breaking
692
727
  // its rendering. The tone contract's own rule covers it: over ~800 chars of
@@ -696,7 +731,7 @@ export const deliverChatReply = async ({
696
731
  const hasIndivisibleOversize = chunks.some((c) => c.length > attachThreshold);
697
732
  if (chunks.length <= 1 && !hasIndivisibleOversize) {
698
733
  const response = await postMessage({ content: chunks[0] ?? text });
699
- if (response?.refused === true) return refused(response, 0, 1);
734
+ if (undelivered(response)) return nonDelivery(response, 0, 1);
700
735
  return { mode: 'single', messages: 1 };
701
736
  }
702
737
  if (chunks.length <= maxChunks && !hasIndivisibleOversize) {
@@ -704,7 +739,7 @@ export const deliverChatReply = async ({
704
739
  for (const chunk of chunks) {
705
740
  // eslint-disable-next-line no-await-in-loop
706
741
  const response = await postMessage({ content: chunk }); // in order, so the reply reads top-down
707
- if (response?.refused === true) return refused(response, messages, chunks.length);
742
+ if (undelivered(response)) return nonDelivery(response, messages, chunks.length);
708
743
  messages += 1;
709
744
  }
710
745
  return { mode: 'split', messages };
@@ -724,7 +759,7 @@ export const deliverChatReply = async ({
724
759
  let posted = 0;
725
760
  try {
726
761
  const rootRes = await postMessage({ content: chunks[0] });
727
- if (rootRes?.refused === true) return refused(rootRes, 0, chunks.length);
762
+ if (undelivered(rootRes)) return nonDelivery(rootRes, 0, chunks.length);
728
763
  posted = 1;
729
764
  // The runtime route answers `res.json(result)` with the created row on
730
765
  // `result.message`. Accept either id field; refuse to guess if neither
@@ -736,7 +771,7 @@ export const deliverChatReply = async ({
736
771
  for (const chunk of chunks.slice(1)) {
737
772
  // eslint-disable-next-line no-await-in-loop
738
773
  const response = await postMessage({ content: chunk, threadRootId: String(rootId) });
739
- if (response?.refused === true) return refused(response, posted, chunks.length);
774
+ if (undelivered(response)) return nonDelivery(response, posted, chunks.length);
740
775
  posted += 1;
741
776
  }
742
777
  return { mode: 'thread', messages: chunks.length, threadRootId: String(rootId) };
@@ -751,7 +786,7 @@ export const deliverChatReply = async ({
751
786
  for (const chunk of chunks.slice(posted)) {
752
787
  // eslint-disable-next-line no-await-in-loop
753
788
  const response = await postMessage({ content: chunk });
754
- if (response?.refused === true) return refused(response, posted, chunks.length);
789
+ if (undelivered(response)) return nonDelivery(response, posted, chunks.length);
755
790
  posted += 1;
756
791
  }
757
792
  return { mode: 'thread-fallback', messages: chunks.length };
@@ -777,7 +812,7 @@ export const deliverChatReply = async ({
777
812
  ? chunks[0]
778
813
  : '(reply too large for chat — attached in full)';
779
814
  const response = await postMessage({ content: `${lead}\n\n${directive}` });
780
- if (response?.refused === true) return refused(response, 0, 1);
815
+ if (undelivered(response)) return nonDelivery(response, 0, 1);
781
816
  return { mode: 'attach', messages: 1 };
782
817
  } catch (err) {
783
818
  log(`attach fallback failed (${err.message}) — posting ${chunks.length} split messages instead`);
@@ -785,7 +820,7 @@ export const deliverChatReply = async ({
785
820
  for (const chunk of chunks) {
786
821
  // eslint-disable-next-line no-await-in-loop
787
822
  const response = await postMessage({ content: chunk });
788
- if (response?.refused === true) return refused(response, messages, chunks.length);
823
+ if (undelivered(response)) return nonDelivery(response, messages, chunks.length);
789
824
  messages += 1;
790
825
  }
791
826
  return { mode: 'split-fallback', messages };