agent-comms 9.1.0 → 9.1.1

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.
@@ -15,7 +15,7 @@
15
15
  "source": "url",
16
16
  "url": "https://github.com/ExaDev/agent-comms.git"
17
17
  },
18
- "version": "9.1.0"
18
+ "version": "9.1.1"
19
19
  }
20
20
  ]
21
21
  }
@@ -17,5 +17,5 @@
17
17
  },
18
18
  "name": "agent-comms",
19
19
  "repository": "https://github.com/ExaDev/agent-comms",
20
- "version": "9.1.0"
20
+ "version": "9.1.1"
21
21
  }
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * Claims travel as wire-mesh session frames (MeshTransport.broadcastCoordinatorClaim/sendCoordinatorClaim), and only over sessions to peers on this machine, because both duties the role assigns are facts about one machine: a PID probe can only see local processes and the cc-peer front can only reach local Claude Code sessions.
5
5
  *
6
- * When a claim is made: a store claims only when it knows of no incumbent and is the first on the machine as far as it can tell. A store that bound the well-known port with no first-contact presence configured can meet other stores only through that port, so it claims at once; one that may be reachable through first contact (it bound the port, or could neither join nor bind it) first waits one claim wait for an incumbent's announcement, so a store arriving at an existing mesh never takes the role at an equal term before the holder's claim reaches it. Both ends of every machine-local session announce the incumbent they know as the session is enrolled. When the incumbent departs, the lowest device-id among the peers this side has a machine-local session to claims at once at a raised term, and every other survivor claims too if, one claim wait later, the claim it accepts still names a holder it has no session to, which covers a lowest-id peer that is itself gone or was never reachable. The same wait follows accepting a claim whose holder this side has no session to, so a claim naming a device that never joined cannot pin the role vacant.
6
+ * When a claim is made: a store claims only when it knows of no incumbent and is the first on the machine as far as it can tell. A store that bound the well-known port with no first-contact presence configured can meet other stores only through that port, so it claims at once; one that may be reachable through first contact (it bound the port, or could neither join nor bind it) first waits one claim wait for an incumbent's announcement, so a store arriving at an existing mesh never takes the role at an equal term before the holder's claim reaches it. Both ends of every machine-local session announce the incumbent they know as the session is enrolled. When the incumbent departs, the lowest device-id among the peers this side has a machine-local session to claims at once at a raised term, and every other survivor claims too if, one claim wait later, the claim it accepts still names a holder it has no session to, which covers a lowest-id peer that is itself gone or was never reachable. A claim that is accepted names a holder that is here when it is accepted, so the wait after an accepted claim never starts: the claim waits are the one after a departure and the one at start-up.
7
+ *
8
+ * Whose claims count: claims are accepted from the peers this machine already trusts with a machine-local session, and this module does not authenticate them further. A claim is evaluated only when its holder is this side, the peer that sent it (the session's authenticated device-id, never a field of the frame), or a peer this side has a machine-local session to. A claim gossiped onward by a peer other than its holder is therefore still accepted wherever the holder is reachable, which is every peer of a fully connected machine. Any other claim waits until its holder's session is enrolled and is dropped after one claim wait, and is not evaluated at all when, by then, its sender has no machine-local session. Waiting claims are bounded per sender: each machine-local peer has at most PENDING_CLAIMS_PER_SENDER of them, at most one per holder, kept in the election's own order (higher term first, an equal term by lowest holder id), and a claim whose term no later claim could supersede is not kept.
7
9
  */
8
10
  import type { CoordinatorFrame } from "wire-mesh-core/generated/protocol";
9
11
  import type { ConnectionHandle, MeshTransport } from "./transport.js";
@@ -39,6 +41,8 @@ export declare class CoordinatorRole {
39
41
  private takeoverStreak;
40
42
  /** The claim, as `term:holder`, each peer was last answered with. A peer whose view lags keeps sending the claims it knows, and answering each one with the same incumbent only multiplies the traffic: the answer is repeated only when the incumbent changes or the peer's session is enrolled afresh, since the earlier answer may have been lost with the old session. */
41
43
  private readonly answered;
44
+ /** The claims set aside, by the device-id of the session that sent each, best first in the election's own order. */
45
+ private readonly pending;
42
46
  constructor(deps: Readonly<CoordinatorRoleDeps>);
43
47
  private get election();
44
48
  /** Whether this side holds the role now. */
@@ -51,12 +55,21 @@ export declare class CoordinatorRole {
51
55
  claimIfVacantAfterWait(): void;
52
56
  /** Tells a peer whose machine-local session was just enrolled which claim this side accepts, so a joiner learns the incumbent instead of claiming over it. A no-op while this side knows of none. */
53
57
  announceTo(handle: Readonly<ConnectionHandle>): Promise<void>;
54
- /** Evaluates a claim a peer gossiped. An accepted claim is gossiped on to every peer so the supersession reaches peers the sender is not connected to, and when it names a holder this side has no session to, this side waits one claim wait for that holder to appear before claiming over it; a stale or tiebreak-losing claim is answered with the incumbent so its sender converges, and a claim whose term no later claim could supersede is dropped unevaluated, neither gossiped nor answered. Receiving the incumbent itself is neither, and is dropped without a reply, which is what stops two peers that agree from echoing it forever. */
58
+ /** Evaluates a claim a peer gossiped. An accepted claim is gossiped on to every peer so the supersession reaches peers the sender is not connected to, a claim whose holder is not here waits for that holder to enrol and is dropped after one claim wait; a stale or tiebreak-losing claim is answered with the incumbent so its sender converges, and a claim whose term no later claim could supersede is dropped unevaluated, neither gossiped nor answered. Receiving the incumbent itself is neither, and is dropped without a reply, which is what stops two peers that agree from echoing it forever. */
55
59
  handleClaim(handle: Readonly<ConnectionHandle>, frame: Readonly<CoordinatorFrame>): Promise<void>;
56
60
  /** Recovers the role when the departed peer held it. The lowest device-id among the peers this side has a machine-local session to claims at once at a raised term; every other survivor waits one claim wait and claims itself if the claim it then accepts still names a holder it has no session to, then runs onDelayedTakeover (the caller's single-authority announcement of the departed holder, which a delayed successor makes too). */
57
61
  handleDeparture(peerId: string, onDelayedTakeover: () => Promise<void>): Promise<void>;
58
62
  /** Stops taking part and gives the duties up if this side held them, once any transition still running has finished. Idempotent. */
59
63
  stop(): Promise<void>;
64
+ /** Whether the holder a claim names is this side, the peer that sent it, or a peer this side has a machine-local session to. The sender is the handle's authenticated device-id, so a peer cannot name itself as someone else. */
65
+ private holderIsHere;
66
+ /** Keeps a claim whose holder is not here until that holder's session is enrolled, and drops it after one claim wait. The sender keeps at most PENDING_CLAIMS_PER_SENDER of them, one per holder, so a new claim for a holder already waiting replaces it only at a higher term, and when the sender is over its share the claim the election ranks last goes. */
67
+ private setAside;
68
+ /** Removes one waiting claim and its timer. */
69
+ private forget;
70
+ /** Takes every waiting claim that names `holder`, best first, except those whose sender no longer has a machine-local session here. */
71
+ private takePendingFor;
72
+ private dropAllPending;
60
73
  private isLowestSurvivor;
61
74
  /** Whether the claim this side accepts leaves the role without a holder it can reach: there is no claim at all, or it names a device that is neither this side nor a peer this side has a machine-local session to. */
62
75
  private incumbentUnreachable;
@@ -1 +1 @@
1
- {"version":3,"file":"coordinator-role.d.ts","sourceRoot":"","sources":["../../src/core/coordinator-role.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAKtE,yGAAyG;AACzG,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mRAAmR;AACnR,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,IAAI,CAC1B,aAAa,EACb,2BAA2B,GAAG,sBAAsB,GAAG,iBAAiB,CACzE,CAAC;IACF,0XAA0X;IAC1X,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,gFAAgF;IAChF,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC;AAYD,qBAAa,eAAe;IAed,OAAO,CAAC,QAAQ,CAAC,IAAI;IAdjC,4KAA4K;IAC5K,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,oKAAoK;IACpK,OAAO,CAAC,OAAO,CAAS;IACxB,4MAA4M;IAC5M,OAAO,CAAC,WAAW,CAAoC;IACvD,+EAA+E;IAC/E,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAS;IACxB,gRAAgR;IAChR,OAAO,CAAC,cAAc,CAAK;IAC3B,8WAA8W;IAC9W,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;gBAEzB,IAAI,EAAE,QAAQ,CAAC,mBAAmB,CAAC;IAEhE,OAAO,KAAK,QAAQ,GAKnB;IAED,4CAA4C;IAC5C,QAAQ,IAAI,OAAO;IAInB,kGAAkG;IAClG,OAAO,IAAI,oBAAoB,GAAG,SAAS;IAM3C,2NAA2N;IACrN,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAKpC,gSAAgS;IAChS,sBAAsB,IAAI,IAAI;IAK9B,qMAAqM;IAC/L,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnE,unBAAunB;IACjnB,WAAW,CACf,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAClC,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC;IAyBhB,ibAAib;IAC3a,eAAe,CACnB,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACrC,OAAO,CAAC,IAAI,CAAC;IAShB,oIAAoI;IAC9H,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,OAAO,CAAC,gBAAgB;IAUxB,uNAAuN;IACvN,OAAO,CAAC,oBAAoB;IAO5B,4OAA4O;IAC5O,OAAO,CAAC,cAAc;YAkBR,KAAK;IAMnB,OAAO,CAAC,eAAe;IAMvB,wNAAwN;YAC1M,WAAW;IAMzB,mSAAmS;YACrR,YAAY;CAM3B"}
1
+ {"version":3,"file":"coordinator-role.d.ts","sourceRoot":"","sources":["../../src/core/coordinator-role.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAKtE,yGAAyG;AACzG,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mRAAmR;AACnR,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,IAAI,CAC1B,aAAa,EACb,2BAA2B,GAAG,sBAAsB,GAAG,iBAAiB,CACzE,CAAC;IACF,0XAA0X;IAC1X,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,gFAAgF;IAChF,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC;AA+BD,qBAAa,eAAe;IAiBd,OAAO,CAAC,QAAQ,CAAC,IAAI;IAhBjC,4KAA4K;IAC5K,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,oKAAoK;IACpK,OAAO,CAAC,OAAO,CAAS;IACxB,4MAA4M;IAC5M,OAAO,CAAC,WAAW,CAAoC;IACvD,+EAA+E;IAC/E,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAS;IACxB,gRAAgR;IAChR,OAAO,CAAC,cAAc,CAAK;IAC3B,8WAA8W;IAC9W,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,oHAAoH;IACpH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;gBAEhC,IAAI,EAAE,QAAQ,CAAC,mBAAmB,CAAC;IAEhE,OAAO,KAAK,QAAQ,GAKnB;IAED,4CAA4C;IAC5C,QAAQ,IAAI,OAAO;IAInB,kGAAkG;IAClG,OAAO,IAAI,oBAAoB,GAAG,SAAS;IAM3C,2NAA2N;IACrN,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAKpC,gSAAgS;IAChS,sBAAsB,IAAI,IAAI;IAK9B,qMAAqM;IAC/L,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBnE,klBAAklB;IAC5kB,WAAW,CACf,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EAClC,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC;IA6BhB,ibAAib;IAC3a,eAAe,CACnB,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACrC,OAAO,CAAC,IAAI,CAAC;IAShB,oIAAoI;IAC9H,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,kOAAkO;IAClO,OAAO,CAAC,YAAY;IAYpB,kWAAkW;IAClW,OAAO,CAAC,QAAQ;IA+BhB,+CAA+C;IAC/C,OAAO,CAAC,MAAM;IASd,uIAAuI;IACvI,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,gBAAgB;IAUxB,uNAAuN;IACvN,OAAO,CAAC,oBAAoB;IAO5B,4OAA4O;IAC5O,OAAO,CAAC,cAAc;YAkBR,KAAK;IAMnB,OAAO,CAAC,eAAe;IAMvB,wNAAwN;YAC1M,WAAW;IAMzB,mSAAmS;YACrR,YAAY;CAM3B"}
@@ -3,16 +3,27 @@
3
3
  *
4
4
  * Claims travel as wire-mesh session frames (MeshTransport.broadcastCoordinatorClaim/sendCoordinatorClaim), and only over sessions to peers on this machine, because both duties the role assigns are facts about one machine: a PID probe can only see local processes and the cc-peer front can only reach local Claude Code sessions.
5
5
  *
6
- * When a claim is made: a store claims only when it knows of no incumbent and is the first on the machine as far as it can tell. A store that bound the well-known port with no first-contact presence configured can meet other stores only through that port, so it claims at once; one that may be reachable through first contact (it bound the port, or could neither join nor bind it) first waits one claim wait for an incumbent's announcement, so a store arriving at an existing mesh never takes the role at an equal term before the holder's claim reaches it. Both ends of every machine-local session announce the incumbent they know as the session is enrolled. When the incumbent departs, the lowest device-id among the peers this side has a machine-local session to claims at once at a raised term, and every other survivor claims too if, one claim wait later, the claim it accepts still names a holder it has no session to, which covers a lowest-id peer that is itself gone or was never reachable. The same wait follows accepting a claim whose holder this side has no session to, so a claim naming a device that never joined cannot pin the role vacant.
6
+ * When a claim is made: a store claims only when it knows of no incumbent and is the first on the machine as far as it can tell. A store that bound the well-known port with no first-contact presence configured can meet other stores only through that port, so it claims at once; one that may be reachable through first contact (it bound the port, or could neither join nor bind it) first waits one claim wait for an incumbent's announcement, so a store arriving at an existing mesh never takes the role at an equal term before the holder's claim reaches it. Both ends of every machine-local session announce the incumbent they know as the session is enrolled. When the incumbent departs, the lowest device-id among the peers this side has a machine-local session to claims at once at a raised term, and every other survivor claims too if, one claim wait later, the claim it accepts still names a holder it has no session to, which covers a lowest-id peer that is itself gone or was never reachable. A claim that is accepted names a holder that is here when it is accepted, so the wait after an accepted claim never starts: the claim waits are the one after a departure and the one at start-up.
7
+ *
8
+ * Whose claims count: claims are accepted from the peers this machine already trusts with a machine-local session, and this module does not authenticate them further. A claim is evaluated only when its holder is this side, the peer that sent it (the session's authenticated device-id, never a field of the frame), or a peer this side has a machine-local session to. A claim gossiped onward by a peer other than its holder is therefore still accepted wherever the holder is reachable, which is every peer of a fully connected machine. Any other claim waits until its holder's session is enrolled and is dropped after one claim wait, and is not evaluated at all when, by then, its sender has no machine-local session. Waiting claims are bounded per sender: each machine-local peer has at most PENDING_CLAIMS_PER_SENDER of them, at most one per holder, kept in the election's own order (higher term first, an equal term by lowest holder id), and a claim whose term no later claim could supersede is not kept.
7
9
  */
8
- import { CoordinatorElection, compareDeviceIds, } from "wire-mesh-core/domain/coordinator-election";
10
+ import { CoordinatorElection, compareDeviceIds, isSupersedableTerm, } from "wire-mesh-core/domain/coordinator-election";
9
11
  import { deviceIdFromHex, deviceIdToHex, } from "wire-mesh-core/domain/device-id";
10
12
  /** The longest delay setTimeout accepts: past it the timer fires at once, which would turn a long backoff into a burst of takeovers. */
11
13
  const MAX_TIMER_MS = 0x7fff_ffff;
14
+ /** Orders waiting claims as the election ranks claims: a higher term first, and at an equal term the lower holder id, compared as the election compares it. */
15
+ function rankedFirst(left, right) {
16
+ if (left.frame.term !== right.frame.term) {
17
+ return right.frame.term - left.frame.term;
18
+ }
19
+ return compareDeviceIds(left.frame.coordinator, right.frame.coordinator);
20
+ }
12
21
  function sameClaim(frame, claim) {
13
22
  return (frame.term === claim.term &&
14
23
  compareDeviceIds(frame.coordinator, claim.coordinator) === 0);
15
24
  }
25
+ /** How many claims one machine-local peer may have waiting for their holders to enrol, one per holder. */
26
+ const PENDING_CLAIMS_PER_SENDER = 4;
16
27
  export class CoordinatorRole {
17
28
  deps;
18
29
  /** Built on first use rather than at construction, because MeshStore's peerId is assigned after its constructor runs (every bridge sets it to the identity's device-id). */
@@ -28,6 +39,8 @@ export class CoordinatorRole {
28
39
  takeoverStreak = 0;
29
40
  /** The claim, as `term:holder`, each peer was last answered with. A peer whose view lags keeps sending the claims it knows, and answering each one with the same incumbent only multiplies the traffic: the answer is repeated only when the incumbent changes or the peer's session is enrolled afresh, since the earlier answer may have been lost with the old session. */
30
41
  answered = new Map();
42
+ /** The claims set aside, by the device-id of the session that sent each, best first in the election's own order. */
43
+ pending = new Map();
31
44
  constructor(deps) {
32
45
  this.deps = deps;
33
46
  }
@@ -63,23 +76,41 @@ export class CoordinatorRole {
63
76
  /** Tells a peer whose machine-local session was just enrolled which claim this side accepts, so a joiner learns the incumbent instead of claiming over it. A no-op while this side knows of none. */
64
77
  async announceTo(handle) {
65
78
  this.answered.delete(handle.id);
66
- const frame = this.electionInstance?.announceCurrent();
67
- if (frame === undefined)
68
- return;
69
- await this.deps.requireTransport().sendCoordinatorClaim(handle, frame);
79
+ // Taken before the first await, so a claim released here is neither dropped by its timer nor taken twice while the announcement is being sent.
80
+ const released = this.takePendingFor(handle.id);
81
+ try {
82
+ const frame = this.electionInstance?.announceCurrent();
83
+ if (frame !== undefined) {
84
+ await this.deps.requireTransport().sendCoordinatorClaim(handle, frame);
85
+ }
86
+ }
87
+ catch (error) {
88
+ this.deps.onError(error);
89
+ }
90
+ // The session just enrolled may be the holder claims were set aside for. One claim failing to evaluate never costs the others theirs.
91
+ for (const waiting of released) {
92
+ try {
93
+ await this.handleClaim(waiting.handle, waiting.frame);
94
+ }
95
+ catch (error) {
96
+ this.deps.onError(error);
97
+ }
98
+ }
70
99
  }
71
- /** Evaluates a claim a peer gossiped. An accepted claim is gossiped on to every peer so the supersession reaches peers the sender is not connected to, and when it names a holder this side has no session to, this side waits one claim wait for that holder to appear before claiming over it; a stale or tiebreak-losing claim is answered with the incumbent so its sender converges, and a claim whose term no later claim could supersede is dropped unevaluated, neither gossiped nor answered. Receiving the incumbent itself is neither, and is dropped without a reply, which is what stops two peers that agree from echoing it forever. */
100
+ /** Evaluates a claim a peer gossiped. An accepted claim is gossiped on to every peer so the supersession reaches peers the sender is not connected to, a claim whose holder is not here waits for that holder to enrol and is dropped after one claim wait; a stale or tiebreak-losing claim is answered with the incumbent so its sender converges, and a claim whose term no later claim could supersede is dropped unevaluated, neither gossiped nor answered. Receiving the incumbent itself is neither, and is dropped without a reply, which is what stops two peers that agree from echoing it forever. */
72
101
  async handleClaim(handle, frame) {
73
102
  if (this.stopped)
74
103
  return;
104
+ if (!this.holderIsHere(handle, frame)) {
105
+ this.setAside(handle, frame);
106
+ return;
107
+ }
75
108
  const result = this.election.evaluate(frame);
76
109
  if (result.outcome === "accepted") {
77
110
  this.answered.clear();
78
111
  this.cancelClaimWait();
79
- if (this.incumbentUnreachable())
80
- this.startClaimWait();
81
- else
82
- this.takeoverStreak = 0;
112
+ // The holder of an accepted claim is here, so there is no incumbent to wait out.
113
+ this.takeoverStreak = 0;
83
114
  await this.deps.requireTransport().broadcastCoordinatorClaim(frame);
84
115
  }
85
116
  else if (result.outcome === "retained" &&
@@ -109,8 +140,75 @@ export class CoordinatorRole {
109
140
  async stop() {
110
141
  this.stopped = true;
111
142
  this.cancelClaimWait();
143
+ this.dropAllPending();
112
144
  await this.syncHolding();
113
145
  }
146
+ /** Whether the holder a claim names is this side, the peer that sent it, or a peer this side has a machine-local session to. The sender is the handle's authenticated device-id, so a peer cannot name itself as someone else. */
147
+ holderIsHere(handle, frame) {
148
+ const holder = deviceIdToHex(frame.coordinator);
149
+ return (holder === this.deps.getPeerId() ||
150
+ holder === handle.id ||
151
+ this.deps.requireTransport().electionPeerIds().has(holder));
152
+ }
153
+ /** Keeps a claim whose holder is not here until that holder's session is enrolled, and drops it after one claim wait. The sender keeps at most PENDING_CLAIMS_PER_SENDER of them, one per holder, so a new claim for a holder already waiting replaces it only at a higher term, and when the sender is over its share the claim the election ranks last goes. */
154
+ setAside(handle, frame) {
155
+ // A claim whose term no later claim could supersede is dropped by the election unevaluated, wherever its holder is, so it never takes a place here.
156
+ if (!isSupersedableTerm(frame.term))
157
+ return;
158
+ const holder = deviceIdToHex(frame.coordinator);
159
+ const mine = this.pending.get(handle.id) ?? [];
160
+ const same = mine.find((entry) => entry.holder === holder);
161
+ if (same !== undefined) {
162
+ if (same.frame.term >= frame.term)
163
+ return;
164
+ this.forget(handle.id, same);
165
+ }
166
+ const timer = setTimeout(() => {
167
+ const waiting = (this.pending.get(handle.id) ?? []).find((entry) => entry.holder === holder && entry.frame === frame);
168
+ if (waiting !== undefined)
169
+ this.forget(handle.id, waiting);
170
+ }, this.deps.claimWaitMs);
171
+ // Never what keeps a process alive: a store shutting down stops the role anyway.
172
+ timer.unref();
173
+ const kept = [
174
+ ...(this.pending.get(handle.id) ?? []),
175
+ { handle, frame, holder, timer },
176
+ ].sort(rankedFirst);
177
+ this.pending.set(handle.id, kept);
178
+ for (const extra of kept.slice(PENDING_CLAIMS_PER_SENDER)) {
179
+ this.forget(handle.id, extra);
180
+ }
181
+ }
182
+ /** Removes one waiting claim and its timer. */
183
+ forget(sender, entry) {
184
+ clearTimeout(entry.timer);
185
+ const rest = (this.pending.get(sender) ?? []).filter((other) => other !== entry);
186
+ if (rest.length === 0)
187
+ this.pending.delete(sender);
188
+ else
189
+ this.pending.set(sender, rest);
190
+ }
191
+ /** Takes every waiting claim that names `holder`, best first, except those whose sender no longer has a machine-local session here. */
192
+ takePendingFor(holder) {
193
+ const senders = this.deps.requireTransport().electionPeerIds();
194
+ const taken = [];
195
+ for (const [sender, entries] of [...this.pending]) {
196
+ for (const entry of entries) {
197
+ if (entry.holder !== holder)
198
+ continue;
199
+ this.forget(sender, entry);
200
+ if (senders.has(sender))
201
+ taken.push(entry);
202
+ }
203
+ }
204
+ return taken.sort(rankedFirst);
205
+ }
206
+ dropAllPending() {
207
+ for (const [sender, entries] of [...this.pending]) {
208
+ for (const entry of entries)
209
+ this.forget(sender, entry);
210
+ }
211
+ }
114
212
  isLowestSurvivor(departedPeerId) {
115
213
  const selfId = this.deps.getPeerId();
116
214
  const self = deviceIdFromHex(selfId);
@@ -1 +1 @@
1
- {"version":3,"file":"coordinator-role.js","sourceRoot":"","sources":["../../src/core/coordinator-role.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"coordinator-role.js","sourceRoot":"","sources":["../../src/core/coordinator-role.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-comms",
3
3
  "description": "Cross-harness communication mesh for LLM agents — rooms, DMs, presence, and real-time push delivery over TCP",
4
- "version": "9.1.0",
4
+ "version": "9.1.1",
5
5
  "bin": {
6
6
  "agent-comms": "dist/cli.js"
7
7
  },
@@ -3,12 +3,15 @@
3
3
  *
4
4
  * Claims travel as wire-mesh session frames (MeshTransport.broadcastCoordinatorClaim/sendCoordinatorClaim), and only over sessions to peers on this machine, because both duties the role assigns are facts about one machine: a PID probe can only see local processes and the cc-peer front can only reach local Claude Code sessions.
5
5
  *
6
- * When a claim is made: a store claims only when it knows of no incumbent and is the first on the machine as far as it can tell. A store that bound the well-known port with no first-contact presence configured can meet other stores only through that port, so it claims at once; one that may be reachable through first contact (it bound the port, or could neither join nor bind it) first waits one claim wait for an incumbent's announcement, so a store arriving at an existing mesh never takes the role at an equal term before the holder's claim reaches it. Both ends of every machine-local session announce the incumbent they know as the session is enrolled. When the incumbent departs, the lowest device-id among the peers this side has a machine-local session to claims at once at a raised term, and every other survivor claims too if, one claim wait later, the claim it accepts still names a holder it has no session to, which covers a lowest-id peer that is itself gone or was never reachable. The same wait follows accepting a claim whose holder this side has no session to, so a claim naming a device that never joined cannot pin the role vacant.
6
+ * When a claim is made: a store claims only when it knows of no incumbent and is the first on the machine as far as it can tell. A store that bound the well-known port with no first-contact presence configured can meet other stores only through that port, so it claims at once; one that may be reachable through first contact (it bound the port, or could neither join nor bind it) first waits one claim wait for an incumbent's announcement, so a store arriving at an existing mesh never takes the role at an equal term before the holder's claim reaches it. Both ends of every machine-local session announce the incumbent they know as the session is enrolled. When the incumbent departs, the lowest device-id among the peers this side has a machine-local session to claims at once at a raised term, and every other survivor claims too if, one claim wait later, the claim it accepts still names a holder it has no session to, which covers a lowest-id peer that is itself gone or was never reachable. A claim that is accepted names a holder that is here when it is accepted, so the wait after an accepted claim never starts: the claim waits are the one after a departure and the one at start-up.
7
+ *
8
+ * Whose claims count: claims are accepted from the peers this machine already trusts with a machine-local session, and this module does not authenticate them further. A claim is evaluated only when its holder is this side, the peer that sent it (the session's authenticated device-id, never a field of the frame), or a peer this side has a machine-local session to. A claim gossiped onward by a peer other than its holder is therefore still accepted wherever the holder is reachable, which is every peer of a fully connected machine. Any other claim waits until its holder's session is enrolled and is dropped after one claim wait, and is not evaluated at all when, by then, its sender has no machine-local session. Waiting claims are bounded per sender: each machine-local peer has at most PENDING_CLAIMS_PER_SENDER of them, at most one per holder, kept in the election's own order (higher term first, an equal term by lowest holder id), and a claim whose term no later claim could supersede is not kept.
7
9
  */
8
10
 
9
11
  import {
10
12
  CoordinatorElection,
11
13
  compareDeviceIds,
14
+ isSupersedableTerm,
12
15
  type CoordinatorClaim,
13
16
  } from "wire-mesh-core/domain/coordinator-election";
14
17
  import {
@@ -43,6 +46,14 @@ export interface CoordinatorRoleDeps {
43
46
  onError: (error: unknown) => void;
44
47
  }
45
48
 
49
+ /** Orders waiting claims as the election ranks claims: a higher term first, and at an equal term the lower holder id, compared as the election compares it. */
50
+ function rankedFirst(left: PendingClaim, right: PendingClaim): number {
51
+ if (left.frame.term !== right.frame.term) {
52
+ return right.frame.term - left.frame.term;
53
+ }
54
+ return compareDeviceIds(left.frame.coordinator, right.frame.coordinator);
55
+ }
56
+
46
57
  function sameClaim(
47
58
  frame: Readonly<CoordinatorFrame>,
48
59
  claim: Readonly<CoordinatorClaim>,
@@ -53,6 +64,17 @@ function sameClaim(
53
64
  );
54
65
  }
55
66
 
67
+ /** How many claims one machine-local peer may have waiting for their holders to enrol, one per holder. */
68
+ const PENDING_CLAIMS_PER_SENDER = 4;
69
+
70
+ /** A claim set aside because its holder is neither this side, its sender nor a peer with a session here, with who sent it and the timer that drops it after one claim wait. */
71
+ interface PendingClaim {
72
+ readonly handle: Readonly<ConnectionHandle>;
73
+ readonly frame: Readonly<CoordinatorFrame>;
74
+ readonly holder: string;
75
+ readonly timer: ReturnType<typeof setTimeout>;
76
+ }
77
+
56
78
  export class CoordinatorRole {
57
79
  /** Built on first use rather than at construction, because MeshStore's peerId is assigned after its constructor runs (every bridge sets it to the identity's device-id). */
58
80
  private electionInstance: CoordinatorElection | undefined;
@@ -67,6 +89,8 @@ export class CoordinatorRole {
67
89
  private takeoverStreak = 0;
68
90
  /** The claim, as `term:holder`, each peer was last answered with. A peer whose view lags keeps sending the claims it knows, and answering each one with the same incumbent only multiplies the traffic: the answer is repeated only when the incumbent changes or the peer's session is enrolled afresh, since the earlier answer may have been lost with the old session. */
69
91
  private readonly answered = new Map<string, string>();
92
+ /** The claims set aside, by the device-id of the session that sent each, best first in the election's own order. */
93
+ private readonly pending = new Map<string, PendingClaim[]>();
70
94
 
71
95
  constructor(private readonly deps: Readonly<CoordinatorRoleDeps>) {}
72
96
 
@@ -104,23 +128,42 @@ export class CoordinatorRole {
104
128
  /** Tells a peer whose machine-local session was just enrolled which claim this side accepts, so a joiner learns the incumbent instead of claiming over it. A no-op while this side knows of none. */
105
129
  async announceTo(handle: Readonly<ConnectionHandle>): Promise<void> {
106
130
  this.answered.delete(handle.id);
107
- const frame = this.electionInstance?.announceCurrent();
108
- if (frame === undefined) return;
109
- await this.deps.requireTransport().sendCoordinatorClaim(handle, frame);
131
+ // Taken before the first await, so a claim released here is neither dropped by its timer nor taken twice while the announcement is being sent.
132
+ const released = this.takePendingFor(handle.id);
133
+ try {
134
+ const frame = this.electionInstance?.announceCurrent();
135
+ if (frame !== undefined) {
136
+ await this.deps.requireTransport().sendCoordinatorClaim(handle, frame);
137
+ }
138
+ } catch (error) {
139
+ this.deps.onError(error);
140
+ }
141
+ // The session just enrolled may be the holder claims were set aside for. One claim failing to evaluate never costs the others theirs.
142
+ for (const waiting of released) {
143
+ try {
144
+ await this.handleClaim(waiting.handle, waiting.frame);
145
+ } catch (error) {
146
+ this.deps.onError(error);
147
+ }
148
+ }
110
149
  }
111
150
 
112
- /** Evaluates a claim a peer gossiped. An accepted claim is gossiped on to every peer so the supersession reaches peers the sender is not connected to, and when it names a holder this side has no session to, this side waits one claim wait for that holder to appear before claiming over it; a stale or tiebreak-losing claim is answered with the incumbent so its sender converges, and a claim whose term no later claim could supersede is dropped unevaluated, neither gossiped nor answered. Receiving the incumbent itself is neither, and is dropped without a reply, which is what stops two peers that agree from echoing it forever. */
151
+ /** Evaluates a claim a peer gossiped. An accepted claim is gossiped on to every peer so the supersession reaches peers the sender is not connected to, a claim whose holder is not here waits for that holder to enrol and is dropped after one claim wait; a stale or tiebreak-losing claim is answered with the incumbent so its sender converges, and a claim whose term no later claim could supersede is dropped unevaluated, neither gossiped nor answered. Receiving the incumbent itself is neither, and is dropped without a reply, which is what stops two peers that agree from echoing it forever. */
113
152
  async handleClaim(
114
153
  handle: Readonly<ConnectionHandle>,
115
154
  frame: Readonly<CoordinatorFrame>,
116
155
  ): Promise<void> {
117
156
  if (this.stopped) return;
157
+ if (!this.holderIsHere(handle, frame)) {
158
+ this.setAside(handle, frame);
159
+ return;
160
+ }
118
161
  const result = this.election.evaluate(frame);
119
162
  if (result.outcome === "accepted") {
120
163
  this.answered.clear();
121
164
  this.cancelClaimWait();
122
- if (this.incumbentUnreachable()) this.startClaimWait();
123
- else this.takeoverStreak = 0;
165
+ // The holder of an accepted claim is here, so there is no incumbent to wait out.
166
+ this.takeoverStreak = 0;
124
167
  await this.deps.requireTransport().broadcastCoordinatorClaim(frame);
125
168
  } else if (
126
169
  result.outcome === "retained" &&
@@ -155,9 +198,85 @@ export class CoordinatorRole {
155
198
  async stop(): Promise<void> {
156
199
  this.stopped = true;
157
200
  this.cancelClaimWait();
201
+ this.dropAllPending();
158
202
  await this.syncHolding();
159
203
  }
160
204
 
205
+ /** Whether the holder a claim names is this side, the peer that sent it, or a peer this side has a machine-local session to. The sender is the handle's authenticated device-id, so a peer cannot name itself as someone else. */
206
+ private holderIsHere(
207
+ handle: Readonly<ConnectionHandle>,
208
+ frame: Readonly<CoordinatorFrame>,
209
+ ): boolean {
210
+ const holder = deviceIdToHex(frame.coordinator);
211
+ return (
212
+ holder === this.deps.getPeerId() ||
213
+ holder === handle.id ||
214
+ this.deps.requireTransport().electionPeerIds().has(holder)
215
+ );
216
+ }
217
+
218
+ /** Keeps a claim whose holder is not here until that holder's session is enrolled, and drops it after one claim wait. The sender keeps at most PENDING_CLAIMS_PER_SENDER of them, one per holder, so a new claim for a holder already waiting replaces it only at a higher term, and when the sender is over its share the claim the election ranks last goes. */
219
+ private setAside(
220
+ handle: Readonly<ConnectionHandle>,
221
+ frame: Readonly<CoordinatorFrame>,
222
+ ): void {
223
+ // A claim whose term no later claim could supersede is dropped by the election unevaluated, wherever its holder is, so it never takes a place here.
224
+ if (!isSupersedableTerm(frame.term)) return;
225
+ const holder = deviceIdToHex(frame.coordinator);
226
+ const mine = this.pending.get(handle.id) ?? [];
227
+ const same = mine.find((entry) => entry.holder === holder);
228
+ if (same !== undefined) {
229
+ if (same.frame.term >= frame.term) return;
230
+ this.forget(handle.id, same);
231
+ }
232
+ const timer = setTimeout(() => {
233
+ const waiting = (this.pending.get(handle.id) ?? []).find(
234
+ (entry) => entry.holder === holder && entry.frame === frame,
235
+ );
236
+ if (waiting !== undefined) this.forget(handle.id, waiting);
237
+ }, this.deps.claimWaitMs);
238
+ // Never what keeps a process alive: a store shutting down stops the role anyway.
239
+ timer.unref();
240
+ const kept = [
241
+ ...(this.pending.get(handle.id) ?? []),
242
+ { handle, frame, holder, timer },
243
+ ].sort(rankedFirst);
244
+ this.pending.set(handle.id, kept);
245
+ for (const extra of kept.slice(PENDING_CLAIMS_PER_SENDER)) {
246
+ this.forget(handle.id, extra);
247
+ }
248
+ }
249
+
250
+ /** Removes one waiting claim and its timer. */
251
+ private forget(sender: string, entry: PendingClaim): void {
252
+ clearTimeout(entry.timer);
253
+ const rest = (this.pending.get(sender) ?? []).filter(
254
+ (other) => other !== entry,
255
+ );
256
+ if (rest.length === 0) this.pending.delete(sender);
257
+ else this.pending.set(sender, rest);
258
+ }
259
+
260
+ /** Takes every waiting claim that names `holder`, best first, except those whose sender no longer has a machine-local session here. */
261
+ private takePendingFor(holder: string): PendingClaim[] {
262
+ const senders = this.deps.requireTransport().electionPeerIds();
263
+ const taken: PendingClaim[] = [];
264
+ for (const [sender, entries] of [...this.pending]) {
265
+ for (const entry of entries) {
266
+ if (entry.holder !== holder) continue;
267
+ this.forget(sender, entry);
268
+ if (senders.has(sender)) taken.push(entry);
269
+ }
270
+ }
271
+ return taken.sort(rankedFirst);
272
+ }
273
+
274
+ private dropAllPending(): void {
275
+ for (const [sender, entries] of [...this.pending]) {
276
+ for (const entry of entries) this.forget(sender, entry);
277
+ }
278
+ }
279
+
161
280
  private isLowestSurvivor(departedPeerId: string): boolean {
162
281
  const selfId = this.deps.getPeerId();
163
282
  const self = deviceIdFromHex(selfId);