@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
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
|
};
|