@commonlyai/cli 0.1.175 → 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.
@@ -4,6 +4,21 @@ The adapters that run an agent CLI as a supervised seat: one prompt in, one CLI
4
4
 
5
5
  It is private and is not published on its own. `@commonlyai/cli` depends on it as `file:../packages/seats` and lists it in `bundleDependencies`, so it ships inside the CLI tarball.
6
6
 
7
+ ## Release path
8
+
9
+ - A push to `main` touching `cli/` or `packages/seats/` triggers
10
+ [npm publish](../../.github/workflows/npm-publish.yml). If the packed files changed,
11
+ it publishes the next CLI patch with seats bundled ([CLI package](../../cli/package.json)).
12
+ [Deploy Dev](../../.github/workflows/deploy-dev.yml) is manually dispatched and builds service images separately.
13
+ - The publish job assigns the version. PRs leave version lines alone
14
+ ([version guard](../../.github/workflows/package-version-guard.yml), TASK-466).
15
+ - Operator-pinned fleet seats select their CLI path in each seat's local `run.sh` launcher,
16
+ outside this repo. Update that pin and restart the seat to load a release.
17
+ Global CLI installs must be upgraded too; publishing does not update a running seat.
18
+ - Check the running wrapper's CLI path and the live spawn argv to see what it runs
19
+ ([diagnosis guide](../../docs/runbooks/diagnosing-a-silent-seat.md#2-read-the-live-spawn--the-prompt-is-in-argv)).
20
+ The registry describes configuration; it does not prove what was loaded.
21
+
7
22
  **Rule:** nothing here imports Commonly code. An adapter that needs Commonly policy (the memory preamble, the legacy sandbox-trust names, the fleet seat-model switch) takes it as a host object through its factory, for example `createHermesAdapter(host)`. The CLI binds that host in `cli/src/lib/adapters/<name>.js`. `cli/__tests__/seats-package.test.mjs` enforces the import rule and checks that the tarball carries every source file.
8
23
 
9
24
  The extraction plan is D62, extract-in-place.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.175",
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
  };