@commonlyai/cli 0.1.10 → 0.1.11
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 +62 -1
- package/src/lib/enforcement.js +4 -5
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -803,6 +803,40 @@ export const performRun = ({
|
|
|
803
803
|
// capped agent-triggered event is exactly the traffic we want dropped.
|
|
804
804
|
// Human-triggered turns are never capped.
|
|
805
805
|
const trigger = classifyTrigger(event, preSpawn);
|
|
806
|
+
// A human turn is a property of the POD, not of this seat's reaction to
|
|
807
|
+
// it. Record it as soon as it is classified — before the cascade check and
|
|
808
|
+
// before the claim race — so a seat that stands down still observes the
|
|
809
|
+
// reset.
|
|
810
|
+
//
|
|
811
|
+
// Without this, losing a claim on a human message is SELF-REINFORCING.
|
|
812
|
+
// The claim stand-down returns before `runTurn`, and `record()` lives at
|
|
813
|
+
// the end of `runTurn`, so the loser never records the human turn that
|
|
814
|
+
// would have cleared its streak. It meets the next broadcast still capped,
|
|
815
|
+
// declines, and again skips `record()`. The only other escape is the
|
|
816
|
+
// seat's own resetMs clock: stateFor reads only this process's
|
|
817
|
+
// lastAgentTurnAt, so a capped seat self-clears 10 minutes after its last
|
|
818
|
+
// completed agent turn regardless of room traffic — cyclical throttling,
|
|
819
|
+
// not permanent starvation.
|
|
820
|
+
//
|
|
821
|
+
// Measured in one pod, 2026-08-18, same window: ux-lead 14 lost claims /
|
|
822
|
+
// 73 cap refusals / 2 posts, against sprint-review 0 / 2 / 95. Losing one
|
|
823
|
+
// race is what kept it losing — the mechanism meant to SHARE work
|
|
824
|
+
// concentrated it instead.
|
|
825
|
+
//
|
|
826
|
+
// `=== 'human'` rather than `!== 'agent'`. These are behaviourally the
|
|
827
|
+
// same today: `record()` dispatches on exact equality and leaves every
|
|
828
|
+
// other value untouched, including the 'unknown' that `classifyTrigger`
|
|
829
|
+
// returns for an unresolvable trigger — `enforcement.js:123-131`, and the
|
|
830
|
+
// "'unknown' is neutral: no count, no reset" note at :130. The narrow form
|
|
831
|
+
// is defensive, not load-bearing: it keeps an unidentifiable event from
|
|
832
|
+
// becoming a cap-reset primitive if `record()` ever grows a fallthrough
|
|
833
|
+
// branch. It closes no hole that was open.
|
|
834
|
+
//
|
|
835
|
+
// Agent-triggered turns still record only on completion (see `runTurn`),
|
|
836
|
+
// so the no-double-count property for redelivered events is unchanged.
|
|
837
|
+
// Recording a human trigger sets the streak to 0, so this call and the
|
|
838
|
+
// completion-time one are idempotent with each other.
|
|
839
|
+
if (trigger === 'human') cascadeGovernor.record(eventPodId, trigger);
|
|
806
840
|
const admission = cascadeGovernor.admit(eventPodId, trigger, event.type);
|
|
807
841
|
if (!admission.allowed) {
|
|
808
842
|
log(
|
|
@@ -987,7 +1021,34 @@ export const performRun = ({
|
|
|
987
1021
|
}
|
|
988
1022
|
} else if (silentReply) {
|
|
989
1023
|
const reason = heartbeatControlReply ? replyText : (replyText || 'empty output');
|
|
990
|
-
|
|
1024
|
+
// A turn that posted via tool and THEN ended with the sentinel is not the
|
|
1025
|
+
// same event as a turn that produced nothing — but this branch reported
|
|
1026
|
+
// both as `no wrapper-post (NO_REPLY)`, because it is evaluated before
|
|
1027
|
+
// the `agentPostedItself` branch below and swallowed that fact.
|
|
1028
|
+
//
|
|
1029
|
+
// The skill told agents to do exactly this (post via commonly_post_message,
|
|
1030
|
+
// then end with NO_REPLY so the wrapper does not double-post), so CORRECT
|
|
1031
|
+
// behaviour and total silence were indistinguishable on stdout.
|
|
1032
|
+
//
|
|
1033
|
+
// Cost, 2026-08-18: a seat was diagnosed as mute for "19 hours" on the
|
|
1034
|
+
// strength of `grep -c "posted via tool" == 0` against its log. It had in
|
|
1035
|
+
// fact posted seven times in that window. Five remedies were applied to a
|
|
1036
|
+
// seat that was never broken — cleared session, fresh process, MCP
|
|
1037
|
+
// repointed, model repinned — before the seat itself pointed at the
|
|
1038
|
+
// ledger. The instrument was broken, not the agent.
|
|
1039
|
+
//
|
|
1040
|
+
// Report the two cases distinctly. `posted` is the load-bearing word: a
|
|
1041
|
+
// reader scanning for whether a seat is contributing must not have to
|
|
1042
|
+
// infer it from the absence of a different line.
|
|
1043
|
+
if (agentPostedItself) {
|
|
1044
|
+
log(
|
|
1045
|
+
`[${event.type}] posted via tool, then ${reason} — no echo needed `
|
|
1046
|
+
+ `(matched message ${suppressedBy.id} by ${suppressedBy.author} `
|
|
1047
|
+
+ `via ${suppressedBy.basis})`,
|
|
1048
|
+
);
|
|
1049
|
+
} else {
|
|
1050
|
+
log(`[${event.type}] no wrapper-post (${reason}) — nothing posted this turn`);
|
|
1051
|
+
}
|
|
991
1052
|
} else if (agentPostedItself) {
|
|
992
1053
|
// Name the message that caused the suppression. A silently dropped reply
|
|
993
1054
|
// is invisible to everyone; #757 went unnoticed precisely because this
|
package/src/lib/enforcement.js
CHANGED
|
@@ -75,11 +75,10 @@ export const ADDRESSED_EVENT_TYPES = new Set(['chat.mention', 'thread.mention',
|
|
|
75
75
|
* pod recovers on its own — a legitimate a2a handoff an hour later must not
|
|
76
76
|
* inherit a stale cap).
|
|
77
77
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* a timer that will save you.
|
|
78
|
+
* The only other escape is the seat's own resetMs clock: stateFor reads only
|
|
79
|
+
* this process's lastAgentTurnAt, so a capped seat self-clears 10
|
|
80
|
+
* minutes after its last completed agent turn regardless of room traffic —
|
|
81
|
+
* cyclical throttling, not permanent starvation.
|
|
83
82
|
*
|
|
84
83
|
* Split into admit/record so a spawn that fails (and will be redelivered)
|
|
85
84
|
* never double-counts: admit() only reads, record() runs after a turn
|