@commonlyai/cli 0.1.167 → 0.1.170
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 +26 -9
- package/src/commands/pod.js +13 -6
- package/src/lib/enforcement.js +45 -1
- package/src/lib/pod-catch-up.js +19 -3
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -2035,15 +2035,32 @@ export const performRun = ({
|
|
|
2035
2035
|
} else {
|
|
2036
2036
|
// ADR-018 length gate: the tone contract is enforced here, where the
|
|
2037
2037
|
// wrapper is the one posting. Split, attach — never truncate.
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2038
|
+
let delivery;
|
|
2039
|
+
try {
|
|
2040
|
+
delivery = await deliverChatReply({
|
|
2041
|
+
client,
|
|
2042
|
+
podId: eventPodId,
|
|
2043
|
+
text: replyText,
|
|
2044
|
+
limit: chatCharLimit,
|
|
2045
|
+
maxChunks: maxChatChunks,
|
|
2046
|
+
uploadName: `${agentName}-reply-${event._id}.md`,
|
|
2047
|
+
log: (line) => log(`[${event.type}] ${line}`),
|
|
2048
|
+
});
|
|
2049
|
+
} catch (deliveryError) {
|
|
2050
|
+
// TASK-535: every rung threw, so the reply reached nobody. The throw
|
|
2051
|
+
// still leaves the events unacked for redelivery (unchanged), but the
|
|
2052
|
+
// retry report is about the BATCH; this line is about the reply, and
|
|
2053
|
+
// it names where it was going so a lost summary is never silent.
|
|
2054
|
+
const eventCount = Array.isArray(event.payload?.batchEventIds)
|
|
2055
|
+
? event.payload.batchEventIds.length
|
|
2056
|
+
: 1;
|
|
2057
|
+
log(
|
|
2058
|
+
`[${event.type}] ERROR reply NOT delivered to pod ${eventPodId} — `
|
|
2059
|
+
+ `${Buffer.byteLength(replyText)} bytes lost, ${eventCount} event${eventCount === 1 ? '' : 's'} `
|
|
2060
|
+
+ `left unacked for retry: ${deliveryError.message}`,
|
|
2061
|
+
);
|
|
2062
|
+
throw deliveryError;
|
|
2063
|
+
}
|
|
2047
2064
|
if (delivery.skipped) {
|
|
2048
2065
|
// The server answered 200 and created nothing: a duplicate inside its
|
|
2049
2066
|
// dedupe window, a silent body, a heartbeat it keeps. Nothing reached
|
package/src/commands/pod.js
CHANGED
|
@@ -55,6 +55,7 @@ Commonly instance (see: commonly login --help).
|
|
|
55
55
|
pod
|
|
56
56
|
.command('send <podId> <message>')
|
|
57
57
|
.description('Post a message to a pod')
|
|
58
|
+
.option('--payload <json>', 'UI card JSON envelope; the message remains the required text fallback')
|
|
58
59
|
.option('--instance <url>', 'Target Commonly instance')
|
|
59
60
|
.action(async (podId, message, opts) => {
|
|
60
61
|
const token = getToken(opts.instance);
|
|
@@ -63,12 +64,18 @@ Commonly instance (see: commonly login --help).
|
|
|
63
64
|
const client = createClient({ instance: resolveInstanceUrl(opts.instance), token });
|
|
64
65
|
|
|
65
66
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
const body = { content: message };
|
|
68
|
+
if (opts.payload !== undefined) {
|
|
69
|
+
body.payload = JSON.parse(opts.payload);
|
|
70
|
+
body.messageType = 'card';
|
|
71
|
+
}
|
|
72
|
+
// UI cards use the validated agent endpoint only. A validation or
|
|
73
|
+
// authorization refusal must never retry through the human writer.
|
|
74
|
+
const result = await client.post(`/api/agents/runtime/pods/${podId}/messages`, body)
|
|
75
|
+
.catch((error) => {
|
|
76
|
+
if (opts.payload !== undefined) throw error;
|
|
77
|
+
return client.post(`/api/messages/${podId}`, body);
|
|
78
|
+
});
|
|
72
79
|
|
|
73
80
|
console.log(`Sent (${result._id || result.id || 'ok'})`);
|
|
74
81
|
} catch (err) {
|
package/src/lib/enforcement.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* stands the agent down, and both are logged with the reason.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { isUnseenMessagesHold } from './pod-catch-up.js';
|
|
22
|
+
|
|
21
23
|
// Event types whose payload.messageId identifies a claimable trigger message.
|
|
22
24
|
// first_contact is deliberately absent: the welcome wake targets exactly one
|
|
23
25
|
// agent, so there is nothing to contend for. Heartbeats have no message at
|
|
@@ -673,7 +675,49 @@ export const deliverChatReply = async ({
|
|
|
673
675
|
// request, but no message was created. Keep that distinction at the client
|
|
674
676
|
// boundary so every delivery mode shares it rather than treating a resolved
|
|
675
677
|
// promise as proof of a post.
|
|
676
|
-
|
|
678
|
+
//
|
|
679
|
+
// TASK-535 — the send-time freshness hold. The backend answers 409
|
|
680
|
+
// `unseen_messages` when other members posted after this seat's last
|
|
681
|
+
// recorded tail read. A turn that ran ~43 minutes (rhea, 2026-10-25 10:01Z)
|
|
682
|
+
// hit it on the thread headline, the attach card and the split fallback in
|
|
683
|
+
// turn, so the reply was lost and its 10 events re-ran. A retry without an
|
|
684
|
+
// override cannot succeed — every rung posts to the same guarded route.
|
|
685
|
+
//
|
|
686
|
+
// CTO ruling: on the FIRST hold of a delivery, re-read the pod tail once,
|
|
687
|
+
// then post with `continueAnyway: true`, and keep the override for every
|
|
688
|
+
// later post in THIS delivery (remaining chunks, later rungs). The re-read
|
|
689
|
+
// is GET /messages at the route's 50-row cap with no `before`/`threadRootId`
|
|
690
|
+
// — the read that records a seen cursor (AgentSendFreshness.recordRead). It
|
|
691
|
+
// advances the cursor only when the page holds EVERY message since the last
|
|
692
|
+
// recorded read. The wrapper's own post-turn snapshot is that same read at
|
|
693
|
+
// limit 10, which is why a 12-message gap stayed unseen. So a re-read alone
|
|
694
|
+
// passes a normal post only while the gap is ≤ 50 and nothing lands between
|
|
695
|
+
// read and post; the override is what delivers, and the read records what
|
|
696
|
+
// the wrapper saw, exactly as its snapshots already do. One re-read and one
|
|
697
|
+
// override per delivery; a post that still fails throws to the next rung as
|
|
698
|
+
// before. Nothing here loops.
|
|
699
|
+
let freshnessOverride = false;
|
|
700
|
+
const postMessage = async (body) => {
|
|
701
|
+
if (freshnessOverride) return client.post(messagesPath, { ...body, continueAnyway: true });
|
|
702
|
+
try {
|
|
703
|
+
return await client.post(messagesPath, body);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
if (!isUnseenMessagesHold(error)) throw error;
|
|
706
|
+
freshnessOverride = true;
|
|
707
|
+
const unseen = error.body?.unseenCount;
|
|
708
|
+
log(
|
|
709
|
+
`reply held: ${Number.isFinite(unseen) ? unseen : 'some'} unseen message(s) in pod ${podId} `
|
|
710
|
+
+ 'arrived during the turn — re-reading the pod once, then posting with continueAnyway (TASK-535)',
|
|
711
|
+
);
|
|
712
|
+
try {
|
|
713
|
+
await client.get(messagesPath, { limit: 50 });
|
|
714
|
+
} catch (readError) {
|
|
715
|
+
// The re-read is bookkeeping, not permission: the override delivers.
|
|
716
|
+
log(`pod re-read before the continueAnyway post failed (${readError.message}) — posting anyway`);
|
|
717
|
+
}
|
|
718
|
+
return client.post(messagesPath, { ...body, continueAnyway: true });
|
|
719
|
+
}
|
|
720
|
+
};
|
|
677
721
|
const refused = (response, messages, attemptedMessages) => ({
|
|
678
722
|
mode: 'refused',
|
|
679
723
|
messages,
|
package/src/lib/pod-catch-up.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backend's send-time freshness hold (agentSendFreshness.unseenMessages):
|
|
3
|
+
* HTTP 409 with `code: 'unseen_messages'`, surfaced by api.js as
|
|
4
|
+
* `error.status` + `error.body`. Shared by the turn-start probe below and the
|
|
5
|
+
* end-of-turn delivery in enforcement.js, so both read the hold the same way.
|
|
6
|
+
*/
|
|
7
|
+
export const isUnseenMessagesHold = (error) => (
|
|
8
|
+
error?.status === 409 && error?.body?.code === 'unseen_messages'
|
|
9
|
+
);
|
|
10
|
+
|
|
1
11
|
/** Explicit turn-start recovery for history that cannot fit in a tail read. */
|
|
2
12
|
export const catchUpBeforeTurn = async (client, podId) => {
|
|
3
13
|
const base = `/api/agents/runtime/pods/${encodeURIComponent(podId)}`;
|
|
@@ -9,13 +19,19 @@ export const catchUpBeforeTurn = async (client, podId) => {
|
|
|
9
19
|
// Unknown freshness preserves existing turn behavior; the send-time auth
|
|
10
20
|
// and freshness checks still decide whether output may be posted.
|
|
11
21
|
if (error?.status !== 409) return '';
|
|
12
|
-
if (error
|
|
22
|
+
if (!isUnseenMessagesHold(error)) throw error;
|
|
13
23
|
unseenCount = error.body.unseenCount;
|
|
14
24
|
}
|
|
15
25
|
// Routine wakes retain the seat's existing read behavior and prompt size.
|
|
16
26
|
if (!(unseenCount > 50)) return '';
|
|
17
|
-
//
|
|
18
|
-
// supplies fresh conversation
|
|
27
|
+
// Before the turn, the answer to a hold is context, not an override: a
|
|
28
|
+
// catch-up supplies fresh conversation BEFORE the model gets a turn, so
|
|
29
|
+
// nothing here sends with continueAnyway. The one place an already
|
|
30
|
+
// generated reply IS retried with continueAnyway is end-of-turn delivery
|
|
31
|
+
// (deliverChatReply, TASK-535): a turn long enough for the pod to move on
|
|
32
|
+
// otherwise lost its whole reply to this same hold and left its events to
|
|
33
|
+
// re-run. That is a CTO ruling scoped to delivery; it does not license
|
|
34
|
+
// continueAnyway anywhere a fresh read can still reach the model.
|
|
19
35
|
let body;
|
|
20
36
|
try {
|
|
21
37
|
body = await client.post(`${base}/catch-up`, { confirmSkipHistory: true });
|