@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -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
- log(`[${event.type}] no wrapper-post (${reason})`);
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
@@ -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
- * NOTE on `resetMs`: it is a SILENCE window, not a decay. It requires
79
- * `resetMs` with ZERO agent-triggered turns in the pod. Three chatty seats
80
- * never leave a ten-minute hole, so in a busy room the only live release is a
81
- * human turn. Say so plainly rather than letting "or the streak decays" imply
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