@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 +1 -1
- package/src/commands/agent.js +31 -1
- package/src/lib/enforcement.js +42 -7
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -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.
|
|
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: {
|
package/src/lib/enforcement.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
823
|
+
if (undelivered(response)) return nonDelivery(response, messages, chunks.length);
|
|
789
824
|
messages += 1;
|
|
790
825
|
}
|
|
791
826
|
return { mode: 'split-fallback', messages };
|