@commonlyai/cli 0.1.176 → 0.1.177
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 +20 -10
- package/src/lib/claim-outcome.js +11 -3
- package/src/lib/poller.js +3 -1
- package/src/lib/webhook-server.js +24 -3
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
import { getToken, resolveInstanceUrl } from '../lib/config.js';
|
|
33
33
|
import { startPoller, terminalDeliveryAckError } from '../lib/poller.js';
|
|
34
34
|
import { formatDigestFrame } from '../lib/digest-frame.js';
|
|
35
|
-
import { startWebhookServer, forwardToLocalWebhook } from '../lib/webhook-server.js';
|
|
35
|
+
import { startWebhookServer, forwardToLocalWebhook, logOnlyHandler } from '../lib/webhook-server.js';
|
|
36
36
|
import { getAdapter, listAdapterNames } from '../lib/adapters/index.js';
|
|
37
37
|
import {
|
|
38
38
|
getSession,
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
formatPodFocusFrame,
|
|
77
77
|
readPodFocus,
|
|
78
78
|
} from '../lib/pod-focus.js';
|
|
79
|
-
import { claimReleaseFor, ackResultFor } from '../lib/claim-outcome.js';
|
|
79
|
+
import { claimReleaseFor, ackResultFor, EMPTY_OUTPUT } from '../lib/claim-outcome.js';
|
|
80
80
|
import { prepareCommitAttribution } from '../lib/commit-attribution.js';
|
|
81
81
|
import { detectBwrap } from '../lib/sandbox/bwrap.js';
|
|
82
82
|
import { resolvePublicSandboxMode } from '../lib/sandbox/mode.js';
|
|
@@ -2214,6 +2214,13 @@ export const performRun = ({
|
|
|
2214
2214
|
},
|
|
2215
2215
|
};
|
|
2216
2216
|
}
|
|
2217
|
+
// Empty output is not the sentinel. A seat that answered `NO_REPLY` read
|
|
2218
|
+
// its batch and chose silence; one that printed nothing may have crashed,
|
|
2219
|
+
// timed out or lost its route without saying so. The bare `no_action`
|
|
2220
|
+
// stays the sentinel's alone, because a digest counts as read only on
|
|
2221
|
+
// that (sable, #2753). Release, hand-off and spawn counting treat
|
|
2222
|
+
// `empty-output` exactly as they treat a bare decline.
|
|
2223
|
+
if (!delivered && !replyText) return { outcome: 'no_action', reason: EMPTY_OUTPUT };
|
|
2217
2224
|
return { outcome: delivered ? 'posted' : 'no_action' };
|
|
2218
2225
|
};
|
|
2219
2226
|
|
|
@@ -2681,9 +2688,15 @@ export const performRun = ({
|
|
|
2681
2688
|
for (const entry of actionEntries) entry.result = turnResult;
|
|
2682
2689
|
// A digest was context in this successful waking turn, never an
|
|
2683
2690
|
// independent action. If the waking work stood down or the provider
|
|
2684
|
-
// refused the batch, leave it unread for the next real turn.
|
|
2691
|
+
// refused the batch, leave it unread for the next real turn. A reply
|
|
2692
|
+
// the server skipped as `silent_or_empty` is the seat choosing silence
|
|
2693
|
+
// (the server's answer to an intentional NO_REPLY), so it read the
|
|
2694
|
+
// digest. Empty output (`empty-output`) is not a choice anyone can see:
|
|
2695
|
+
// a broken seat produces it every turn, and reading on it would drain
|
|
2696
|
+
// the backlog that seat never saw.
|
|
2685
2697
|
if (turnResult?.outcome === 'posted'
|
|
2686
|
-
|| (turnResult?.outcome === 'no_action'
|
|
2698
|
+
|| (turnResult?.outcome === 'no_action'
|
|
2699
|
+
&& (!turnResult?.reason || turnResult.reason === 'silent_or_empty'))) {
|
|
2687
2700
|
for (const entry of activeEntries) {
|
|
2688
2701
|
if (entry.event.deliveryMode === 'digest') {
|
|
2689
2702
|
entry.result = { outcome: 'no_action', reason: 'digest-read' };
|
|
@@ -2914,7 +2927,8 @@ export const performRun = ({
|
|
|
2914
2927
|
}
|
|
2915
2928
|
const batchSpawned = batch.entries.some((entry) => (
|
|
2916
2929
|
entry.result?.outcome === 'posted'
|
|
2917
|
-
|| (entry.result?.outcome === 'no_action'
|
|
2930
|
+
|| (entry.result?.outcome === 'no_action'
|
|
2931
|
+
&& (!entry.result?.reason || entry.result.reason === EMPTY_OUTPUT))
|
|
2918
2932
|
));
|
|
2919
2933
|
if (batchSpawned) {
|
|
2920
2934
|
consecutiveSpawnFailures = 0;
|
|
@@ -3462,11 +3476,7 @@ Docs:
|
|
|
3462
3476
|
port,
|
|
3463
3477
|
path: opts.path,
|
|
3464
3478
|
secret: opts.secret,
|
|
3465
|
-
onEvent:
|
|
3466
|
-
// Developer's agent code handles this — just ack locally
|
|
3467
|
-
console.log(`[local] ${event.type} from pod ${event.podId}`);
|
|
3468
|
-
return { outcome: 'acknowledged' };
|
|
3469
|
-
},
|
|
3479
|
+
onEvent: logOnlyHandler,
|
|
3470
3480
|
onReady: (url) => console.log(`Local server ready: ${url}`),
|
|
3471
3481
|
});
|
|
3472
3482
|
|
package/src/lib/claim-outcome.js
CHANGED
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
* finite. On any other wake it is terminal. The routing is
|
|
21
21
|
* the kernel's: this module only names the outcome.
|
|
22
22
|
* - `declined` — a silent, normally completed human broadcast. The agent
|
|
23
|
-
* chose not to answer,
|
|
24
|
-
* remaining original
|
|
23
|
+
* chose not to answer, or printed nothing (`empty-output`),
|
|
24
|
+
* so the kernel hands it to exactly one remaining original
|
|
25
|
+
* listener.
|
|
25
26
|
* - `completed` — everything else that produced a result: a posted reply, or
|
|
26
27
|
* a skip with a reason (`no-prompt`, `claim-held`,
|
|
27
28
|
* `duplicate-delivery`) where nothing was refused.
|
|
@@ -43,6 +44,13 @@ export const REFUSAL_REASONS = ['upstream-refused', 'cascade-cap', 'delivery-ref
|
|
|
43
44
|
/** What an unclassifiable refusal is recorded as, rather than a fourth class. */
|
|
44
45
|
export const DEFAULT_REFUSAL_REASON = 'delivery-refused';
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The reason a turn that printed nothing carries. It releases like a bare
|
|
49
|
+
* decline, so a human's message still reaches another listener, but it is not
|
|
50
|
+
* the `NO_REPLY` sentinel and does not mark a digest read.
|
|
51
|
+
*/
|
|
52
|
+
export const EMPTY_OUTPUT = 'empty-output';
|
|
53
|
+
|
|
46
54
|
/**
|
|
47
55
|
* The range the kernel accepts for a refusal's `status` — its
|
|
48
56
|
* `MIN_UPSTREAM_STATUS` / `MAX_UPSTREAM_STATUS` in `messageClaimService`.
|
|
@@ -93,7 +101,7 @@ export const claimReleaseFor = (event, turnResult) => {
|
|
|
93
101
|
if (event?.type === 'message.posted'
|
|
94
102
|
&& event.payload?.senderIsHuman === true
|
|
95
103
|
&& turnResult.outcome === 'no_action'
|
|
96
|
-
&& !turnResult.reason) {
|
|
104
|
+
&& (!turnResult.reason || turnResult.reason === EMPTY_OUTPUT)) {
|
|
97
105
|
return { outcome: 'declined' };
|
|
98
106
|
}
|
|
99
107
|
return { outcome: 'completed' };
|
package/src/lib/poller.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { createClient } from './api.js';
|
|
10
10
|
import { formatDigestFrame } from './digest-frame.js';
|
|
11
|
+
import { EMPTY_OUTPUT } from './claim-outcome.js';
|
|
11
12
|
|
|
12
13
|
// Delivery failures that must stop a consumer rather than fall through to the
|
|
13
14
|
// normal at-least-once retry path. A stale delivery belongs to a replacement
|
|
@@ -80,7 +81,8 @@ export const startPoller = ({
|
|
|
80
81
|
} : event;
|
|
81
82
|
let result = { outcome: 'no_action' };
|
|
82
83
|
try {
|
|
83
|
-
|
|
84
|
+
// A handler that returns nothing said nothing: not a read of the digest.
|
|
85
|
+
result = (await onEvent(forwarded)) || { outcome: 'no_action', reason: EMPTY_OUTPUT };
|
|
84
86
|
} catch (handlerErr) {
|
|
85
87
|
result = { outcome: 'error', reason: handlerErr.message };
|
|
86
88
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { createServer } from 'http';
|
|
10
10
|
import { createHmac } from 'crypto';
|
|
11
|
+
import { EMPTY_OUTPUT } from './claim-outcome.js';
|
|
11
12
|
|
|
12
13
|
export const startWebhookServer = ({
|
|
13
14
|
port = 3001,
|
|
@@ -47,9 +48,10 @@ export const startWebhookServer = ({
|
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
// A handler that returns nothing said nothing (#2753).
|
|
52
|
+
let result = { outcome: 'no_action', reason: EMPTY_OUTPUT };
|
|
51
53
|
try {
|
|
52
|
-
result = (await onEvent(event)) || { outcome: '
|
|
54
|
+
result = (await onEvent(event)) || { outcome: 'no_action', reason: EMPTY_OUTPUT };
|
|
53
55
|
} catch (err) {
|
|
54
56
|
result = { outcome: 'error', reason: err.message };
|
|
55
57
|
}
|
|
@@ -67,6 +69,17 @@ export const startWebhookServer = ({
|
|
|
67
69
|
});
|
|
68
70
|
});
|
|
69
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The handler `agent connect` serves on its own port. It only logs what
|
|
74
|
+
* arrives, and no agent has read the event, so it states no outcome a read
|
|
75
|
+
* would count as: the wake still settles, and the pod digest stays unread
|
|
76
|
+
* (Sable, #2753).
|
|
77
|
+
*/
|
|
78
|
+
export const logOnlyHandler = async (event) => {
|
|
79
|
+
console.log(`[local] ${event.type} from pod ${event.podId}`);
|
|
80
|
+
return { outcome: 'no_action', reason: EMPTY_OUTPUT };
|
|
81
|
+
};
|
|
82
|
+
|
|
70
83
|
/**
|
|
71
84
|
* Forward a polled event to a local webhook server.
|
|
72
85
|
* Used by `agent connect` to bridge polling → local HTTP.
|
|
@@ -84,5 +97,13 @@ export const forwardToLocalWebhook = async (event, webhookUrl, secret = null) =>
|
|
|
84
97
|
|
|
85
98
|
const res = await fetch(webhookUrl, { method: 'POST', headers, body: payload });
|
|
86
99
|
if (!res.ok) throw new Error(`Local webhook returned HTTP ${res.status}`);
|
|
87
|
-
|
|
100
|
+
// Only a stated outcome counts. An empty or non-JSON body, or one without
|
|
101
|
+
// an outcome, is a handler that said nothing, the same as a CLI turn that
|
|
102
|
+
// printed nothing, so it must not read the pod digest (Rhea, #2753).
|
|
103
|
+
let body = null;
|
|
104
|
+
try { body = await res.json(); } catch { body = null; }
|
|
105
|
+
if (!body || typeof body !== 'object' || typeof body.outcome !== 'string') {
|
|
106
|
+
return { outcome: 'no_action', reason: EMPTY_OUTPUT };
|
|
107
|
+
}
|
|
108
|
+
return body;
|
|
88
109
|
};
|