@commonlyai/cli 0.1.202 → 0.1.204
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.
|
@@ -200,7 +200,8 @@ class Commonly:
|
|
|
200
200
|
if consecutive_auth_errors >= max_auth_errors:
|
|
201
201
|
print(
|
|
202
202
|
f"[commonly] runtime token rejected {consecutive_auth_errors}x in a row — exiting. "
|
|
203
|
-
"
|
|
203
|
+
"Check COMMONLY_TOKEN: it must hold the bare cm_agent_... value, not the "
|
|
204
|
+
"KEY=VALUE line from .commonly-env. If it is right, re-issue the token or uninstall the agent.",
|
|
204
205
|
flush=True,
|
|
205
206
|
)
|
|
206
207
|
return
|
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
readPodFocus,
|
|
81
81
|
} from '../lib/pod-focus.js';
|
|
82
82
|
import { claimReleaseFor, ackResultFor, EMPTY_OUTPUT } from '../lib/claim-outcome.js';
|
|
83
|
+
import { createClaimedWorkGate } from '../lib/claimed-work-gate.js';
|
|
83
84
|
import { prepareCommitAttribution } from '../lib/commit-attribution.js';
|
|
84
85
|
import { detectBwrap } from '../lib/sandbox/bwrap.js';
|
|
85
86
|
import { resolvePublicSandboxMode } from '../lib/sandbox/mode.js';
|
|
@@ -1536,6 +1537,7 @@ export const performRun = ({
|
|
|
1536
1537
|
warn: (message) => log(`cascade config: ${message}`),
|
|
1537
1538
|
});
|
|
1538
1539
|
const cascadeGovernor = createCascadeGovernor(cascadeSettings);
|
|
1540
|
+
const claimedWorkGate = createClaimedWorkGate({ client });
|
|
1539
1541
|
// Unconditional, because the only other cascade line on this path is the
|
|
1540
1542
|
// warn callback above — which fires only when a value is BAD. Without this,
|
|
1541
1543
|
// a seat running COMMONLY_CASCADE_CAP=8 logs exactly what a seat on the
|
|
@@ -1691,6 +1693,13 @@ export const performRun = ({
|
|
|
1691
1693
|
// Recording a human trigger sets the streak to 0, so this call and the
|
|
1692
1694
|
// completion-time one are idempotent with each other.
|
|
1693
1695
|
if (trigger === 'human') cascadeGovernor.record(eventPodId, trigger);
|
|
1696
|
+
// Defer-while-claimed (TASK-640), before the cascade check so a deferred
|
|
1697
|
+
// broadcast spends none of the addressed grace.
|
|
1698
|
+
if (await claimedWorkGate.shouldDefer(eventPodId, [{ event, trigger }])) {
|
|
1699
|
+
log(`[${event.type}] holding a claimed row in pod ${eventPodId} — broadcast`
|
|
1700
|
+
+ (event?.payload?.messageId ? ` ${event.payload.messageId}` : '') + ' acked without a spawn');
|
|
1701
|
+
return { outcome: 'no_action', reason: 'claimed-work', details: { messageId: event?.payload?.messageId || null } };
|
|
1702
|
+
}
|
|
1694
1703
|
const admission = cascadeGovernor.admit(eventPodId, trigger, event.type, event.payload);
|
|
1695
1704
|
if (!admission.allowed) {
|
|
1696
1705
|
// Name the MESSAGE, not just the pod. Without this the refusal is silent at
|
|
@@ -2743,6 +2752,17 @@ export const performRun = ({
|
|
|
2743
2752
|
const trigger = triggers.includes('human') ? 'human'
|
|
2744
2753
|
: (triggers.includes('agent') ? 'agent' : 'unknown');
|
|
2745
2754
|
if (trigger === 'human') cascadeGovernor.record(eventPodId, trigger);
|
|
2755
|
+
if (await claimedWorkGate.shouldDefer(eventPodId, wakingEntries.map((entry, index) => ({
|
|
2756
|
+
event: entry.event, trigger: triggers[index],
|
|
2757
|
+
})))) {
|
|
2758
|
+
log(`[inbox.batch] holding a claimed row in pod ${eventPodId} — ${wakingEntries.length} broadcast`
|
|
2759
|
+
+ `${wakingEntries.length === 1 ? '' : 's'} acked without a spawn`);
|
|
2760
|
+
// Digests are left as the cascade refusal leaves them: unread.
|
|
2761
|
+
for (const entry of wakingEntries) {
|
|
2762
|
+
entry.result = { outcome: 'no_action', reason: 'claimed-work', details: { messageId: entry.event.payload?.messageId || null } };
|
|
2763
|
+
}
|
|
2764
|
+
return { entries };
|
|
2765
|
+
}
|
|
2746
2766
|
const admissionEvent = wakingEntries.find((entry) => (
|
|
2747
2767
|
ADDRESSED_EVENT_TYPES.has(entry.event.type) || entry.event.payload?.addressed === true
|
|
2748
2768
|
))
|
|
@@ -3354,7 +3374,11 @@ const SDK_SOURCES = {
|
|
|
3354
3374
|
botSrc: () => sdkAsset('python/bot.py', join('hello-world-python', 'bot.py')),
|
|
3355
3375
|
sdkOut: 'commonly.py',
|
|
3356
3376
|
botOut: (name) => `${name}.py`,
|
|
3357
|
-
|
|
3377
|
+
// .commonly-env is already KEY=VALUE. Sourcing it under `set -a` exports
|
|
3378
|
+
// the key as written and keeps the token out of argv (`env $(cat …)` would
|
|
3379
|
+
// put it on the command line). `COMMONLY_TOKEN=$(cat …)` would set the
|
|
3380
|
+
// variable to the literal `COMMONLY_TOKEN=cm_agent_…` and the bot would 401.
|
|
3381
|
+
runHint: (name) => `(set -a; . ./.commonly-env; exec python3 ${name}.py)`,
|
|
3358
3382
|
},
|
|
3359
3383
|
};
|
|
3360
3384
|
|
|
@@ -3409,6 +3433,9 @@ export const performInit = async ({
|
|
|
3409
3433
|
});
|
|
3410
3434
|
const installation = installResult.installation || installResult;
|
|
3411
3435
|
const instanceId = installation.instanceId || 'default';
|
|
3436
|
+
// Same rule as `attach`: the mention handle is the instanceId when there is
|
|
3437
|
+
// one, otherwise the registry agentName.
|
|
3438
|
+
const mentionHandle = instanceId !== 'default' ? instanceId : (installation.agentName || agentName);
|
|
3412
3439
|
|
|
3413
3440
|
let runtimeToken = installResult.runtimeToken;
|
|
3414
3441
|
if (!runtimeToken) {
|
|
@@ -3434,6 +3461,7 @@ export const performInit = async ({
|
|
|
3434
3461
|
return {
|
|
3435
3462
|
installation,
|
|
3436
3463
|
instanceId,
|
|
3464
|
+
mentionHandle,
|
|
3437
3465
|
runtimeToken,
|
|
3438
3466
|
files: {
|
|
3439
3467
|
sdk: sdkOutPath,
|
|
@@ -4171,6 +4199,7 @@ Docs:
|
|
|
4171
4199
|
console.log('\nNext:');
|
|
4172
4200
|
console.log(` 1. Edit ${opts.name}.py to handle events.`);
|
|
4173
4201
|
console.log(` 2. Run: ${result.runHint}`);
|
|
4202
|
+
console.log(` 3. In pod ${opts.pod}, @mention ${result.mentionHandle} to talk to it.`);
|
|
4174
4203
|
} catch (err) {
|
|
4175
4204
|
console.error(`Init failed: ${err.message}`);
|
|
4176
4205
|
process.exit(1);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Defer-while-claimed (TASK-640). While a seat holds a claimed board row in a
|
|
2
|
+
// pod, a broadcast wake there that no person sent and that does not concern
|
|
3
|
+
// its row is acked without a spawn. Measured on one seat, 2026-10-30: most of
|
|
4
|
+
// its hourly wake-mode turns were pod broadcasts while it held one row, and
|
|
5
|
+
// most of those ended in NO_REPLY.
|
|
6
|
+
//
|
|
7
|
+
// Mentions, DMs, human posts, heartbeats and digests are never deferred, and
|
|
8
|
+
// neither is a broadcast about a row the seat holds. Every failure fails OPEN:
|
|
9
|
+
// a kernel that does not mark `claimedByMe`, or a board read that errors,
|
|
10
|
+
// means nothing is deferred.
|
|
11
|
+
|
|
12
|
+
export const CLAIMED_WORK_TTL_MS = 60_000;
|
|
13
|
+
|
|
14
|
+
// Pure: may this waking event wait while the seat holds `claimedTaskIds`?
|
|
15
|
+
// `trigger` is classifyTrigger's verdict for the event.
|
|
16
|
+
export const isDeferrableWhileClaimed = (event, trigger, claimedTaskIds) => {
|
|
17
|
+
if (!event || event.deliveryMode === 'digest') return false;
|
|
18
|
+
if (event.type !== 'message.posted') return false;
|
|
19
|
+
const payload = event.payload || {};
|
|
20
|
+
if (payload.addressed === true || payload.repliesToYourMessage === true) return false;
|
|
21
|
+
// A lease warning is about the seat's own row, though it names it differently.
|
|
22
|
+
if (payload.leaseWarningTaskId) return false;
|
|
23
|
+
// A board wake with a reason is a change the seat must see: a dependency of
|
|
24
|
+
// its row finished, an edit to its row folded into another row's wake (so
|
|
25
|
+
// taskId names the first row), or an agent assigned it a new row. Only the
|
|
26
|
+
// reasonless found-work nudge may wait.
|
|
27
|
+
if (payload.boardWake === true && payload.wakeReason) return false;
|
|
28
|
+
if (trigger !== 'agent' && trigger !== 'kernel') return false;
|
|
29
|
+
if (payload.taskId && claimedTaskIds?.has(String(payload.taskId))) return false;
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const createClaimedWorkGate = ({ client, ttlMs = CLAIMED_WORK_TTL_MS, now = () => Date.now() } = {}) => {
|
|
34
|
+
const cache = new Map();
|
|
35
|
+
|
|
36
|
+
// The ids of this seat's claimed rows in `podId`, or null when it holds none
|
|
37
|
+
// or the board cannot say.
|
|
38
|
+
const claimedTaskIds = async (podId) => {
|
|
39
|
+
if (!client || !podId) return null;
|
|
40
|
+
const cached = cache.get(podId);
|
|
41
|
+
if (cached && now() - cached.at < ttlMs) return cached.ids;
|
|
42
|
+
let ids = null;
|
|
43
|
+
try {
|
|
44
|
+
const { tasks = [] } = await client.get(`/api/v1/tasks/${podId}`, { status: 'claimed' });
|
|
45
|
+
// A lapsed lease still carries claimedBy; only a live one holds the seat.
|
|
46
|
+
const mine = tasks.filter((task) => task?.claimedByMe === true && task?.leaseState === 'held')
|
|
47
|
+
.map((task) => String(task.taskId));
|
|
48
|
+
ids = mine.length > 0 ? new Set(mine) : null;
|
|
49
|
+
} catch {
|
|
50
|
+
ids = null;
|
|
51
|
+
}
|
|
52
|
+
cache.set(podId, { at: now(), ids });
|
|
53
|
+
return ids;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// True when every waking event in the group may wait. An empty group is
|
|
57
|
+
// never deferred.
|
|
58
|
+
const shouldDefer = async (podId, wakes) => {
|
|
59
|
+
if (!Array.isArray(wakes) || wakes.length === 0) return false;
|
|
60
|
+
// Only read the board when the group could be deferred at all. A group
|
|
61
|
+
// that can't be deferred spawns a turn, and that turn may complete or
|
|
62
|
+
// release the row, so the next read must not trust the cached claim.
|
|
63
|
+
if (!wakes.every(({ event, trigger }) => isDeferrableWhileClaimed(event, trigger, null))) {
|
|
64
|
+
cache.delete(podId);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const ids = await claimedTaskIds(podId);
|
|
68
|
+
if (!ids) return false;
|
|
69
|
+
return wakes.every(({ event, trigger }) => isDeferrableWhileClaimed(event, trigger, ids));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return { shouldDefer };
|
|
73
|
+
};
|