@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.176",
3
+ "version": "0.1.177",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -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' && !turnResult?.reason)) {
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' && !entry.result?.reason)
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: async (event) => {
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
 
@@ -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, so the kernel hands it to exactly one
24
- * remaining original listener.
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
- result = (await onEvent(forwarded)) || { outcome: 'no_action' };
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
- let result = { outcome: 'acknowledged' };
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: 'acknowledged' };
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
- try { return await res.json(); } catch { return { outcome: 'acknowledged' }; }
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
  };