@cotal-ai/connector-claude-code 0.21.0 → 0.23.0

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/dist/mcp.cjs CHANGED
@@ -406,11 +406,11 @@ var require_codegen = __commonJS({
406
406
  const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
407
407
  return `${varKind} ${this.name}${rhs};` + _n;
408
408
  }
409
- optimizeNames(names, constants) {
409
+ optimizeNames(names, constants4) {
410
410
  if (!names[this.name.str])
411
411
  return;
412
412
  if (this.rhs)
413
- this.rhs = optimizeExpr(this.rhs, names, constants);
413
+ this.rhs = optimizeExpr(this.rhs, names, constants4);
414
414
  return this;
415
415
  }
416
416
  get names() {
@@ -427,10 +427,10 @@ var require_codegen = __commonJS({
427
427
  render({ _n }) {
428
428
  return `${this.lhs} = ${this.rhs};` + _n;
429
429
  }
430
- optimizeNames(names, constants) {
430
+ optimizeNames(names, constants4) {
431
431
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
432
432
  return;
433
- this.rhs = optimizeExpr(this.rhs, names, constants);
433
+ this.rhs = optimizeExpr(this.rhs, names, constants4);
434
434
  return this;
435
435
  }
436
436
  get names() {
@@ -491,8 +491,8 @@ var require_codegen = __commonJS({
491
491
  optimizeNodes() {
492
492
  return `${this.code}` ? this : void 0;
493
493
  }
494
- optimizeNames(names, constants) {
495
- this.code = optimizeExpr(this.code, names, constants);
494
+ optimizeNames(names, constants4) {
495
+ this.code = optimizeExpr(this.code, names, constants4);
496
496
  return this;
497
497
  }
498
498
  get names() {
@@ -521,12 +521,12 @@ var require_codegen = __commonJS({
521
521
  }
522
522
  return nodes.length > 0 ? this : void 0;
523
523
  }
524
- optimizeNames(names, constants) {
524
+ optimizeNames(names, constants4) {
525
525
  const { nodes } = this;
526
526
  let i = nodes.length;
527
527
  while (i--) {
528
528
  const n = nodes[i];
529
- if (n.optimizeNames(names, constants))
529
+ if (n.optimizeNames(names, constants4))
530
530
  continue;
531
531
  subtractNames(names, n.names);
532
532
  nodes.splice(i, 1);
@@ -579,12 +579,12 @@ var require_codegen = __commonJS({
579
579
  return void 0;
580
580
  return this;
581
581
  }
582
- optimizeNames(names, constants) {
582
+ optimizeNames(names, constants4) {
583
583
  var _a3;
584
- this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
585
- if (!(super.optimizeNames(names, constants) || this.else))
584
+ this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants4);
585
+ if (!(super.optimizeNames(names, constants4) || this.else))
586
586
  return;
587
- this.condition = optimizeExpr(this.condition, names, constants);
587
+ this.condition = optimizeExpr(this.condition, names, constants4);
588
588
  return this;
589
589
  }
590
590
  get names() {
@@ -607,10 +607,10 @@ var require_codegen = __commonJS({
607
607
  render(opts) {
608
608
  return `for(${this.iteration})` + super.render(opts);
609
609
  }
610
- optimizeNames(names, constants) {
611
- if (!super.optimizeNames(names, constants))
610
+ optimizeNames(names, constants4) {
611
+ if (!super.optimizeNames(names, constants4))
612
612
  return;
613
- this.iteration = optimizeExpr(this.iteration, names, constants);
613
+ this.iteration = optimizeExpr(this.iteration, names, constants4);
614
614
  return this;
615
615
  }
616
616
  get names() {
@@ -646,10 +646,10 @@ var require_codegen = __commonJS({
646
646
  render(opts) {
647
647
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
648
648
  }
649
- optimizeNames(names, constants) {
650
- if (!super.optimizeNames(names, constants))
649
+ optimizeNames(names, constants4) {
650
+ if (!super.optimizeNames(names, constants4))
651
651
  return;
652
- this.iterable = optimizeExpr(this.iterable, names, constants);
652
+ this.iterable = optimizeExpr(this.iterable, names, constants4);
653
653
  return this;
654
654
  }
655
655
  get names() {
@@ -691,11 +691,11 @@ var require_codegen = __commonJS({
691
691
  (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
692
692
  return this;
693
693
  }
694
- optimizeNames(names, constants) {
694
+ optimizeNames(names, constants4) {
695
695
  var _a3, _b;
696
- super.optimizeNames(names, constants);
697
- (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
698
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants);
696
+ super.optimizeNames(names, constants4);
697
+ (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants4);
698
+ (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants4);
699
699
  return this;
700
700
  }
701
701
  get names() {
@@ -996,7 +996,7 @@ var require_codegen = __commonJS({
996
996
  function addExprNames(names, from) {
997
997
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
998
998
  }
999
- function optimizeExpr(expr, names, constants) {
999
+ function optimizeExpr(expr, names, constants4) {
1000
1000
  if (expr instanceof code_1.Name)
1001
1001
  return replaceName(expr);
1002
1002
  if (!canOptimize(expr))
@@ -1011,14 +1011,14 @@ var require_codegen = __commonJS({
1011
1011
  return items;
1012
1012
  }, []));
1013
1013
  function replaceName(n) {
1014
- const c = constants[n.str];
1014
+ const c = constants4[n.str];
1015
1015
  if (c === void 0 || names[n.str] !== 1)
1016
1016
  return n;
1017
1017
  delete names[n.str];
1018
1018
  return c;
1019
1019
  }
1020
1020
  function canOptimize(e) {
1021
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants[c.str] !== void 0);
1021
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants4[c.str] !== void 0);
1022
1022
  }
1023
1023
  }
1024
1024
  function subtractNames(names, from) {
@@ -7887,16 +7887,16 @@ var require_ipparser = __commonJS({
7887
7887
  ip[15] = d;
7888
7888
  return ip;
7889
7889
  }
7890
- function isIP(h) {
7891
- return parseIP(h) !== void 0;
7890
+ function isIP(h2) {
7891
+ return parseIP(h2) !== void 0;
7892
7892
  }
7893
- function parseIP(h) {
7894
- for (let i = 0; i < h.length; i++) {
7895
- switch (h[i]) {
7893
+ function parseIP(h2) {
7894
+ for (let i = 0; i < h2.length; i++) {
7895
+ switch (h2[i]) {
7896
7896
  case ".":
7897
- return parseIPv4(h);
7897
+ return parseIPv4(h2);
7898
7898
  case ":":
7899
- return parseIPv6(h);
7899
+ return parseIPv6(h2);
7900
7900
  }
7901
7901
  }
7902
7902
  return;
@@ -8092,12 +8092,12 @@ var require_servers = __commonJS({
8092
8092
  const protocol = port === 80 ? "https" : "http";
8093
8093
  const url2 = new URL(`${protocol}://${u}`);
8094
8094
  url2.port = `${port}`;
8095
- let hostname4 = url2.hostname;
8096
- if (hostname4.charAt(0) === "[") {
8097
- hostname4 = hostname4.substring(1, hostname4.length - 1);
8095
+ let hostname5 = url2.hostname;
8096
+ if (hostname5.charAt(0) === "[") {
8097
+ hostname5 = hostname5.substring(1, hostname5.length - 1);
8098
8098
  }
8099
8099
  const listen = url2.host;
8100
- return { listen, hostname: hostname4, port };
8100
+ return { listen, hostname: hostname5, port };
8101
8101
  }
8102
8102
  var ServerImpl = class _ServerImpl {
8103
8103
  src;
@@ -9605,9 +9605,9 @@ var require_headers = __commonJS({
9605
9605
  const mh = new _MsgHdrsImpl();
9606
9606
  const s = encoders_1.TD.decode(a);
9607
9607
  const lines = s.split("\r\n");
9608
- const h = lines[0];
9609
- if (h !== HEADER) {
9610
- let str2 = h.replace(HEADER, "").trim();
9608
+ const h2 = lines[0];
9609
+ if (h2 !== HEADER) {
9610
+ let str2 = h2.replace(HEADER, "").trim();
9611
9611
  if (str2.length > 0) {
9612
9612
  mh._code = parseInt(str2, 10);
9613
9613
  if (isNaN(mh._code)) {
@@ -9766,12 +9766,12 @@ ${k}: ${v[i]}`;
9766
9766
  return this._description;
9767
9767
  }
9768
9768
  static fromRecord(r) {
9769
- const h = new _MsgHdrsImpl();
9769
+ const h2 = new _MsgHdrsImpl();
9770
9770
  for (const k in r) {
9771
9771
  const v = r[k];
9772
- h.headers.set(k, Array.isArray(v) ? v : [`${v}`]);
9772
+ h2.headers.set(k, Array.isArray(v) ? v : [`${v}`]);
9773
9773
  }
9774
- return h;
9774
+ return h2;
9775
9775
  }
9776
9776
  };
9777
9777
  exports2.MsgHdrsImpl = MsgHdrsImpl;
@@ -10475,11 +10475,11 @@ var require_nacl_fast = __commonJS({
10475
10475
  var _9 = new Uint8Array(32);
10476
10476
  _9[0] = 9;
10477
10477
  var gf0 = gf(), gf1 = gf([1]), _121665 = gf([56129, 1]), D = gf([30883, 4953, 19914, 30187, 55467, 16705, 2637, 112, 59544, 30585, 16505, 36039, 65139, 11119, 27886, 20995]), D2 = gf([61785, 9906, 39828, 60374, 45398, 33411, 5274, 224, 53552, 61171, 33010, 6542, 64743, 22239, 55772, 9222]), X = gf([54554, 36645, 11616, 51542, 42930, 38181, 51040, 26924, 56412, 64982, 57905, 49316, 21502, 52590, 14035, 8553]), Y = gf([26200, 26214, 26214, 26214, 26214, 26214, 26214, 26214, 26214, 26214, 26214, 26214, 26214, 26214, 26214, 26214]), I = gf([41136, 18958, 6951, 50414, 58488, 44335, 6150, 12099, 55207, 15867, 153, 11085, 57099, 20417, 9344, 11139]);
10478
- function ts64(x, i, h, l) {
10479
- x[i] = h >> 24 & 255;
10480
- x[i + 1] = h >> 16 & 255;
10481
- x[i + 2] = h >> 8 & 255;
10482
- x[i + 3] = h & 255;
10478
+ function ts64(x, i, h2, l) {
10479
+ x[i] = h2 >> 24 & 255;
10480
+ x[i + 1] = h2 >> 16 & 255;
10481
+ x[i + 2] = h2 >> 8 & 255;
10482
+ x[i + 3] = h2 & 255;
10483
10483
  x[i + 4] = l >> 24 & 255;
10484
10484
  x[i + 5] = l >> 16 & 255;
10485
10485
  x[i + 6] = l >> 8 & 255;
@@ -11158,10 +11158,10 @@ var require_nacl_fast = __commonJS({
11158
11158
  s.finish(out, outpos);
11159
11159
  return 0;
11160
11160
  }
11161
- function crypto_onetimeauth_verify(h, hpos, m, mpos, n, k) {
11161
+ function crypto_onetimeauth_verify(h2, hpos, m, mpos, n, k) {
11162
11162
  var x = new Uint8Array(16);
11163
11163
  crypto_onetimeauth(x, 0, m, mpos, n, k);
11164
- return crypto_verify_16(h, hpos, x, 0);
11164
+ return crypto_verify_16(h2, hpos, x, 0);
11165
11165
  }
11166
11166
  function crypto_secretbox(c, m, d, n, k) {
11167
11167
  var i;
@@ -11914,7 +11914,7 @@ var require_nacl_fast = __commonJS({
11914
11914
  1246189591
11915
11915
  ];
11916
11916
  function crypto_hashblocks_hl(hh, hl, m, n) {
11917
- var wh = new Int32Array(16), wl = new Int32Array(16), bh0, bh1, bh2, bh3, bh4, bh5, bh6, bh7, bl0, bl1, bl2, bl3, bl4, bl5, bl6, bl7, th, tl, i, j, h, l, a, b, c, d;
11917
+ var wh = new Int32Array(16), wl = new Int32Array(16), bh0, bh1, bh2, bh3, bh4, bh5, bh6, bh7, bl0, bl1, bl2, bl3, bl4, bl5, bl6, bl7, th, tl, i, j, h2, l, a, b, c, d;
11918
11918
  var ah0 = hh[0], ah1 = hh[1], ah2 = hh[2], ah3 = hh[3], ah4 = hh[4], ah5 = hh[5], ah6 = hh[6], ah7 = hh[7], al0 = hl[0], al1 = hl[1], al2 = hl[2], al3 = hl[3], al4 = hl[4], al5 = hl[5], al6 = hl[6], al7 = hl[7];
11919
11919
  var pos = 0;
11920
11920
  while (n >= 128) {
@@ -11940,76 +11940,76 @@ var require_nacl_fast = __commonJS({
11940
11940
  bl5 = al5;
11941
11941
  bl6 = al6;
11942
11942
  bl7 = al7;
11943
- h = ah7;
11943
+ h2 = ah7;
11944
11944
  l = al7;
11945
11945
  a = l & 65535;
11946
11946
  b = l >>> 16;
11947
- c = h & 65535;
11948
- d = h >>> 16;
11949
- h = (ah4 >>> 14 | al4 << 32 - 14) ^ (ah4 >>> 18 | al4 << 32 - 18) ^ (al4 >>> 41 - 32 | ah4 << 32 - (41 - 32));
11947
+ c = h2 & 65535;
11948
+ d = h2 >>> 16;
11949
+ h2 = (ah4 >>> 14 | al4 << 32 - 14) ^ (ah4 >>> 18 | al4 << 32 - 18) ^ (al4 >>> 41 - 32 | ah4 << 32 - (41 - 32));
11950
11950
  l = (al4 >>> 14 | ah4 << 32 - 14) ^ (al4 >>> 18 | ah4 << 32 - 18) ^ (ah4 >>> 41 - 32 | al4 << 32 - (41 - 32));
11951
11951
  a += l & 65535;
11952
11952
  b += l >>> 16;
11953
- c += h & 65535;
11954
- d += h >>> 16;
11955
- h = ah4 & ah5 ^ ~ah4 & ah6;
11953
+ c += h2 & 65535;
11954
+ d += h2 >>> 16;
11955
+ h2 = ah4 & ah5 ^ ~ah4 & ah6;
11956
11956
  l = al4 & al5 ^ ~al4 & al6;
11957
11957
  a += l & 65535;
11958
11958
  b += l >>> 16;
11959
- c += h & 65535;
11960
- d += h >>> 16;
11961
- h = K[i * 2];
11959
+ c += h2 & 65535;
11960
+ d += h2 >>> 16;
11961
+ h2 = K[i * 2];
11962
11962
  l = K[i * 2 + 1];
11963
11963
  a += l & 65535;
11964
11964
  b += l >>> 16;
11965
- c += h & 65535;
11966
- d += h >>> 16;
11967
- h = wh[i % 16];
11965
+ c += h2 & 65535;
11966
+ d += h2 >>> 16;
11967
+ h2 = wh[i % 16];
11968
11968
  l = wl[i % 16];
11969
11969
  a += l & 65535;
11970
11970
  b += l >>> 16;
11971
- c += h & 65535;
11972
- d += h >>> 16;
11971
+ c += h2 & 65535;
11972
+ d += h2 >>> 16;
11973
11973
  b += a >>> 16;
11974
11974
  c += b >>> 16;
11975
11975
  d += c >>> 16;
11976
11976
  th = c & 65535 | d << 16;
11977
11977
  tl = a & 65535 | b << 16;
11978
- h = th;
11978
+ h2 = th;
11979
11979
  l = tl;
11980
11980
  a = l & 65535;
11981
11981
  b = l >>> 16;
11982
- c = h & 65535;
11983
- d = h >>> 16;
11984
- h = (ah0 >>> 28 | al0 << 32 - 28) ^ (al0 >>> 34 - 32 | ah0 << 32 - (34 - 32)) ^ (al0 >>> 39 - 32 | ah0 << 32 - (39 - 32));
11982
+ c = h2 & 65535;
11983
+ d = h2 >>> 16;
11984
+ h2 = (ah0 >>> 28 | al0 << 32 - 28) ^ (al0 >>> 34 - 32 | ah0 << 32 - (34 - 32)) ^ (al0 >>> 39 - 32 | ah0 << 32 - (39 - 32));
11985
11985
  l = (al0 >>> 28 | ah0 << 32 - 28) ^ (ah0 >>> 34 - 32 | al0 << 32 - (34 - 32)) ^ (ah0 >>> 39 - 32 | al0 << 32 - (39 - 32));
11986
11986
  a += l & 65535;
11987
11987
  b += l >>> 16;
11988
- c += h & 65535;
11989
- d += h >>> 16;
11990
- h = ah0 & ah1 ^ ah0 & ah2 ^ ah1 & ah2;
11988
+ c += h2 & 65535;
11989
+ d += h2 >>> 16;
11990
+ h2 = ah0 & ah1 ^ ah0 & ah2 ^ ah1 & ah2;
11991
11991
  l = al0 & al1 ^ al0 & al2 ^ al1 & al2;
11992
11992
  a += l & 65535;
11993
11993
  b += l >>> 16;
11994
- c += h & 65535;
11995
- d += h >>> 16;
11994
+ c += h2 & 65535;
11995
+ d += h2 >>> 16;
11996
11996
  b += a >>> 16;
11997
11997
  c += b >>> 16;
11998
11998
  d += c >>> 16;
11999
11999
  bh7 = c & 65535 | d << 16;
12000
12000
  bl7 = a & 65535 | b << 16;
12001
- h = bh3;
12001
+ h2 = bh3;
12002
12002
  l = bl3;
12003
12003
  a = l & 65535;
12004
12004
  b = l >>> 16;
12005
- c = h & 65535;
12006
- d = h >>> 16;
12007
- h = th;
12005
+ c = h2 & 65535;
12006
+ d = h2 >>> 16;
12007
+ h2 = th;
12008
12008
  l = tl;
12009
12009
  a += l & 65535;
12010
12010
  b += l >>> 16;
12011
- c += h & 65535;
12012
- d += h >>> 16;
12011
+ c += h2 & 65535;
12012
+ d += h2 >>> 16;
12013
12013
  b += a >>> 16;
12014
12014
  c += b >>> 16;
12015
12015
  d += c >>> 16;
@@ -12033,34 +12033,34 @@ var require_nacl_fast = __commonJS({
12033
12033
  al0 = bl7;
12034
12034
  if (i % 16 === 15) {
12035
12035
  for (j = 0; j < 16; j++) {
12036
- h = wh[j];
12036
+ h2 = wh[j];
12037
12037
  l = wl[j];
12038
12038
  a = l & 65535;
12039
12039
  b = l >>> 16;
12040
- c = h & 65535;
12041
- d = h >>> 16;
12042
- h = wh[(j + 9) % 16];
12040
+ c = h2 & 65535;
12041
+ d = h2 >>> 16;
12042
+ h2 = wh[(j + 9) % 16];
12043
12043
  l = wl[(j + 9) % 16];
12044
12044
  a += l & 65535;
12045
12045
  b += l >>> 16;
12046
- c += h & 65535;
12047
- d += h >>> 16;
12046
+ c += h2 & 65535;
12047
+ d += h2 >>> 16;
12048
12048
  th = wh[(j + 1) % 16];
12049
12049
  tl = wl[(j + 1) % 16];
12050
- h = (th >>> 1 | tl << 32 - 1) ^ (th >>> 8 | tl << 32 - 8) ^ th >>> 7;
12050
+ h2 = (th >>> 1 | tl << 32 - 1) ^ (th >>> 8 | tl << 32 - 8) ^ th >>> 7;
12051
12051
  l = (tl >>> 1 | th << 32 - 1) ^ (tl >>> 8 | th << 32 - 8) ^ (tl >>> 7 | th << 32 - 7);
12052
12052
  a += l & 65535;
12053
12053
  b += l >>> 16;
12054
- c += h & 65535;
12055
- d += h >>> 16;
12054
+ c += h2 & 65535;
12055
+ d += h2 >>> 16;
12056
12056
  th = wh[(j + 14) % 16];
12057
12057
  tl = wl[(j + 14) % 16];
12058
- h = (th >>> 19 | tl << 32 - 19) ^ (tl >>> 61 - 32 | th << 32 - (61 - 32)) ^ th >>> 6;
12058
+ h2 = (th >>> 19 | tl << 32 - 19) ^ (tl >>> 61 - 32 | th << 32 - (61 - 32)) ^ th >>> 6;
12059
12059
  l = (tl >>> 19 | th << 32 - 19) ^ (th >>> 61 - 32 | tl << 32 - (61 - 32)) ^ (tl >>> 6 | th << 32 - 6);
12060
12060
  a += l & 65535;
12061
12061
  b += l >>> 16;
12062
- c += h & 65535;
12063
- d += h >>> 16;
12062
+ c += h2 & 65535;
12063
+ d += h2 >>> 16;
12064
12064
  b += a >>> 16;
12065
12065
  c += b >>> 16;
12066
12066
  d += c >>> 16;
@@ -12069,137 +12069,137 @@ var require_nacl_fast = __commonJS({
12069
12069
  }
12070
12070
  }
12071
12071
  }
12072
- h = ah0;
12072
+ h2 = ah0;
12073
12073
  l = al0;
12074
12074
  a = l & 65535;
12075
12075
  b = l >>> 16;
12076
- c = h & 65535;
12077
- d = h >>> 16;
12078
- h = hh[0];
12076
+ c = h2 & 65535;
12077
+ d = h2 >>> 16;
12078
+ h2 = hh[0];
12079
12079
  l = hl[0];
12080
12080
  a += l & 65535;
12081
12081
  b += l >>> 16;
12082
- c += h & 65535;
12083
- d += h >>> 16;
12082
+ c += h2 & 65535;
12083
+ d += h2 >>> 16;
12084
12084
  b += a >>> 16;
12085
12085
  c += b >>> 16;
12086
12086
  d += c >>> 16;
12087
12087
  hh[0] = ah0 = c & 65535 | d << 16;
12088
12088
  hl[0] = al0 = a & 65535 | b << 16;
12089
- h = ah1;
12089
+ h2 = ah1;
12090
12090
  l = al1;
12091
12091
  a = l & 65535;
12092
12092
  b = l >>> 16;
12093
- c = h & 65535;
12094
- d = h >>> 16;
12095
- h = hh[1];
12093
+ c = h2 & 65535;
12094
+ d = h2 >>> 16;
12095
+ h2 = hh[1];
12096
12096
  l = hl[1];
12097
12097
  a += l & 65535;
12098
12098
  b += l >>> 16;
12099
- c += h & 65535;
12100
- d += h >>> 16;
12099
+ c += h2 & 65535;
12100
+ d += h2 >>> 16;
12101
12101
  b += a >>> 16;
12102
12102
  c += b >>> 16;
12103
12103
  d += c >>> 16;
12104
12104
  hh[1] = ah1 = c & 65535 | d << 16;
12105
12105
  hl[1] = al1 = a & 65535 | b << 16;
12106
- h = ah2;
12106
+ h2 = ah2;
12107
12107
  l = al2;
12108
12108
  a = l & 65535;
12109
12109
  b = l >>> 16;
12110
- c = h & 65535;
12111
- d = h >>> 16;
12112
- h = hh[2];
12110
+ c = h2 & 65535;
12111
+ d = h2 >>> 16;
12112
+ h2 = hh[2];
12113
12113
  l = hl[2];
12114
12114
  a += l & 65535;
12115
12115
  b += l >>> 16;
12116
- c += h & 65535;
12117
- d += h >>> 16;
12116
+ c += h2 & 65535;
12117
+ d += h2 >>> 16;
12118
12118
  b += a >>> 16;
12119
12119
  c += b >>> 16;
12120
12120
  d += c >>> 16;
12121
12121
  hh[2] = ah2 = c & 65535 | d << 16;
12122
12122
  hl[2] = al2 = a & 65535 | b << 16;
12123
- h = ah3;
12123
+ h2 = ah3;
12124
12124
  l = al3;
12125
12125
  a = l & 65535;
12126
12126
  b = l >>> 16;
12127
- c = h & 65535;
12128
- d = h >>> 16;
12129
- h = hh[3];
12127
+ c = h2 & 65535;
12128
+ d = h2 >>> 16;
12129
+ h2 = hh[3];
12130
12130
  l = hl[3];
12131
12131
  a += l & 65535;
12132
12132
  b += l >>> 16;
12133
- c += h & 65535;
12134
- d += h >>> 16;
12133
+ c += h2 & 65535;
12134
+ d += h2 >>> 16;
12135
12135
  b += a >>> 16;
12136
12136
  c += b >>> 16;
12137
12137
  d += c >>> 16;
12138
12138
  hh[3] = ah3 = c & 65535 | d << 16;
12139
12139
  hl[3] = al3 = a & 65535 | b << 16;
12140
- h = ah4;
12140
+ h2 = ah4;
12141
12141
  l = al4;
12142
12142
  a = l & 65535;
12143
12143
  b = l >>> 16;
12144
- c = h & 65535;
12145
- d = h >>> 16;
12146
- h = hh[4];
12144
+ c = h2 & 65535;
12145
+ d = h2 >>> 16;
12146
+ h2 = hh[4];
12147
12147
  l = hl[4];
12148
12148
  a += l & 65535;
12149
12149
  b += l >>> 16;
12150
- c += h & 65535;
12151
- d += h >>> 16;
12150
+ c += h2 & 65535;
12151
+ d += h2 >>> 16;
12152
12152
  b += a >>> 16;
12153
12153
  c += b >>> 16;
12154
12154
  d += c >>> 16;
12155
12155
  hh[4] = ah4 = c & 65535 | d << 16;
12156
12156
  hl[4] = al4 = a & 65535 | b << 16;
12157
- h = ah5;
12157
+ h2 = ah5;
12158
12158
  l = al5;
12159
12159
  a = l & 65535;
12160
12160
  b = l >>> 16;
12161
- c = h & 65535;
12162
- d = h >>> 16;
12163
- h = hh[5];
12161
+ c = h2 & 65535;
12162
+ d = h2 >>> 16;
12163
+ h2 = hh[5];
12164
12164
  l = hl[5];
12165
12165
  a += l & 65535;
12166
12166
  b += l >>> 16;
12167
- c += h & 65535;
12168
- d += h >>> 16;
12167
+ c += h2 & 65535;
12168
+ d += h2 >>> 16;
12169
12169
  b += a >>> 16;
12170
12170
  c += b >>> 16;
12171
12171
  d += c >>> 16;
12172
12172
  hh[5] = ah5 = c & 65535 | d << 16;
12173
12173
  hl[5] = al5 = a & 65535 | b << 16;
12174
- h = ah6;
12174
+ h2 = ah6;
12175
12175
  l = al6;
12176
12176
  a = l & 65535;
12177
12177
  b = l >>> 16;
12178
- c = h & 65535;
12179
- d = h >>> 16;
12180
- h = hh[6];
12178
+ c = h2 & 65535;
12179
+ d = h2 >>> 16;
12180
+ h2 = hh[6];
12181
12181
  l = hl[6];
12182
12182
  a += l & 65535;
12183
12183
  b += l >>> 16;
12184
- c += h & 65535;
12185
- d += h >>> 16;
12184
+ c += h2 & 65535;
12185
+ d += h2 >>> 16;
12186
12186
  b += a >>> 16;
12187
12187
  c += b >>> 16;
12188
12188
  d += c >>> 16;
12189
12189
  hh[6] = ah6 = c & 65535 | d << 16;
12190
12190
  hl[6] = al6 = a & 65535 | b << 16;
12191
- h = ah7;
12191
+ h2 = ah7;
12192
12192
  l = al7;
12193
12193
  a = l & 65535;
12194
12194
  b = l >>> 16;
12195
- c = h & 65535;
12196
- d = h >>> 16;
12197
- h = hh[7];
12195
+ c = h2 & 65535;
12196
+ d = h2 >>> 16;
12197
+ h2 = hh[7];
12198
12198
  l = hl[7];
12199
12199
  a += l & 65535;
12200
12200
  b += l >>> 16;
12201
- c += h & 65535;
12202
- d += h >>> 16;
12201
+ c += h2 & 65535;
12202
+ d += h2 >>> 16;
12203
12203
  b += a >>> 16;
12204
12204
  c += b >>> 16;
12205
12205
  d += c >>> 16;
@@ -12240,7 +12240,7 @@ var require_nacl_fast = __commonJS({
12240
12240
  return 0;
12241
12241
  }
12242
12242
  function add(p, q) {
12243
- var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf(), g = gf(), h = gf(), t = gf();
12243
+ var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf(), g = gf(), h2 = gf(), t = gf();
12244
12244
  Z(a, p[1], p[0]);
12245
12245
  Z(t, q[1], q[0]);
12246
12246
  M(a, a, t);
@@ -12254,11 +12254,11 @@ var require_nacl_fast = __commonJS({
12254
12254
  Z(e, b, a);
12255
12255
  Z(f, d, c);
12256
12256
  A(g, d, c);
12257
- A(h, b, a);
12257
+ A(h2, b, a);
12258
12258
  M(p[0], e, f);
12259
- M(p[1], h, g);
12259
+ M(p[1], h2, g);
12260
12260
  M(p[2], g, f);
12261
- M(p[3], e, h);
12261
+ M(p[3], e, h2);
12262
12262
  }
12263
12263
  function cswap(p, q, b) {
12264
12264
  var i;
@@ -12342,7 +12342,7 @@ var require_nacl_fast = __commonJS({
12342
12342
  modL(r, x);
12343
12343
  }
12344
12344
  function crypto_sign(sm, m, n, sk) {
12345
- var d = new Uint8Array(64), h = new Uint8Array(64), r = new Uint8Array(64);
12345
+ var d = new Uint8Array(64), h2 = new Uint8Array(64), r = new Uint8Array(64);
12346
12346
  var i, j, x = new Float64Array(64);
12347
12347
  var p = [gf(), gf(), gf(), gf()];
12348
12348
  crypto_hash(d, sk, 32);
@@ -12357,13 +12357,13 @@ var require_nacl_fast = __commonJS({
12357
12357
  scalarbase(p, r);
12358
12358
  pack(sm, p);
12359
12359
  for (i = 32; i < 64; i++) sm[i] = sk[i];
12360
- crypto_hash(h, sm, n + 64);
12361
- reduce(h);
12360
+ crypto_hash(h2, sm, n + 64);
12361
+ reduce(h2);
12362
12362
  for (i = 0; i < 64; i++) x[i] = 0;
12363
12363
  for (i = 0; i < 32; i++) x[i] = r[i];
12364
12364
  for (i = 0; i < 32; i++) {
12365
12365
  for (j = 0; j < 32; j++) {
12366
- x[i + j] += h[i] * d[j];
12366
+ x[i + j] += h2[i] * d[j];
12367
12367
  }
12368
12368
  }
12369
12369
  modL(sm.subarray(32), x);
@@ -12399,15 +12399,15 @@ var require_nacl_fast = __commonJS({
12399
12399
  }
12400
12400
  function crypto_sign_open(m, sm, n, pk) {
12401
12401
  var i;
12402
- var t = new Uint8Array(32), h = new Uint8Array(64);
12402
+ var t = new Uint8Array(32), h2 = new Uint8Array(64);
12403
12403
  var p = [gf(), gf(), gf(), gf()], q = [gf(), gf(), gf(), gf()];
12404
12404
  if (n < 64) return -1;
12405
12405
  if (unpackneg(q, pk)) return -1;
12406
12406
  for (i = 0; i < n; i++) m[i] = sm[i];
12407
12407
  for (i = 0; i < 32; i++) m[i + 32] = pk[i];
12408
- crypto_hash(h, m, n);
12409
- reduce(h);
12410
- scalarmult(p, q, h);
12408
+ crypto_hash(h2, m, n);
12409
+ reduce(h2);
12410
+ scalarmult(p, q, h2);
12411
12411
  scalarbase(q, sm.subarray(32));
12412
12412
  add(p, q);
12413
12413
  pack(t, p);
@@ -12641,9 +12641,9 @@ var require_nacl_fast = __commonJS({
12641
12641
  nacl2.sign.signatureLength = crypto_sign_BYTES;
12642
12642
  nacl2.hash = function(msg) {
12643
12643
  checkArrayTypes(msg);
12644
- var h = new Uint8Array(crypto_hash_BYTES);
12645
- crypto_hash(h, msg, msg.length);
12646
- return h;
12644
+ var h2 = new Uint8Array(crypto_hash_BYTES);
12645
+ crypto_hash(h2, msg, msg.length);
12646
+ return h2;
12647
12647
  };
12648
12648
  nacl2.hash.hashLength = crypto_hash_BYTES;
12649
12649
  nacl2.verify = function(x, y) {
@@ -14065,9 +14065,9 @@ var require_protocol = __commonJS({
14065
14065
  }
14066
14066
  }
14067
14067
  static async connect(options, publisher) {
14068
- const h = new _ProtocolHandler(options, publisher);
14069
- await h.dialLoop();
14070
- return h;
14068
+ const h2 = new _ProtocolHandler(options, publisher);
14069
+ await h2.dialLoop();
14070
+ return h2;
14071
14071
  }
14072
14072
  static toError(s) {
14073
14073
  let err2 = errors_1.errors.PermissionViolationError.parse(s);
@@ -16399,9 +16399,9 @@ var require_jsbaseclient_api = __commonJS({
16399
16399
  a = new TextEncoder().encode(JSON.stringify(data));
16400
16400
  }
16401
16401
  if (typeof minApiVersion === "number") {
16402
- const h = reqOpts.headers ?? (0, internal_1.headers)();
16403
- h.set(types_1.JsHeaders.RequiredApiLevel, minApiVersion.toString());
16404
- reqOpts.headers = h;
16402
+ const h2 = reqOpts.headers ?? (0, internal_1.headers)();
16403
+ h2.set(types_1.JsHeaders.RequiredApiLevel, minApiVersion.toString());
16404
+ reqOpts.headers = h2;
16405
16405
  }
16406
16406
  let retries = r || 1;
16407
16407
  retries = retries === -1 ? Number.MAX_SAFE_INTEGER : retries;
@@ -20796,11 +20796,11 @@ var require_kv = __commonJS({
20796
20796
  sc.subject_delete_marker_ttl = (0, internal_1.nanos)(bo.markerTTL);
20797
20797
  }
20798
20798
  if (opts.mirror) {
20799
- const mirror2 = Object.assign({}, opts.mirror);
20800
- if (!mirror2.name.startsWith(types_1.kvPrefix)) {
20801
- mirror2.name = `${types_1.kvPrefix}${mirror2.name}`;
20799
+ const mirror = Object.assign({}, opts.mirror);
20800
+ if (!mirror.name.startsWith(types_1.kvPrefix)) {
20801
+ mirror.name = `${types_1.kvPrefix}${mirror.name}`;
20802
20802
  }
20803
- sc.mirror = mirror2;
20803
+ sc.mirror = mirror;
20804
20804
  sc.mirror_direct = true;
20805
20805
  } else if (opts.sources) {
20806
20806
  const sources = opts.sources.map((s) => {
@@ -20863,17 +20863,17 @@ var require_kv = __commonJS({
20863
20863
  this._prefixLen = 0;
20864
20864
  this.prefix = `$KV.${this.bucket}`;
20865
20865
  this.useJsPrefix = this.js.apiPrefix !== "$JS.API";
20866
- const { mirror: mirror2 } = info.config;
20867
- if (mirror2) {
20868
- let n = mirror2.name;
20866
+ const { mirror } = info.config;
20867
+ if (mirror) {
20868
+ let n = mirror.name;
20869
20869
  if (n.startsWith(types_1.kvPrefix)) {
20870
20870
  n = n.substring(types_1.kvPrefix.length);
20871
20871
  }
20872
- if (mirror2.external && mirror2.external.api !== "") {
20873
- const mb = mirror2.name.substring(types_1.kvPrefix.length);
20872
+ if (mirror.external && mirror.external.api !== "") {
20873
+ const mb = mirror.name.substring(types_1.kvPrefix.length);
20874
20874
  this.useJsPrefix = false;
20875
20875
  this.prefix = `$KV.${mb}`;
20876
- this.editPrefix = `${mirror2.external.api}.$KV.${n}`;
20876
+ this.editPrefix = `${mirror.external.api}.$KV.${n}`;
20877
20877
  } else {
20878
20878
  this.editPrefix = this.prefix;
20879
20879
  }
@@ -20952,8 +20952,8 @@ var require_kv = __commonJS({
20952
20952
  close() {
20953
20953
  return Promise.resolve();
20954
20954
  }
20955
- dataLen(data, h) {
20956
- const slen = h ? h.get(internal_2.JsHeaders.MessageSizeHdr) || "" : "";
20955
+ dataLen(data, h2) {
20956
+ const slen = h2 ? h2.get(internal_2.JsHeaders.MessageSizeHdr) || "" : "";
20957
20957
  if (slen !== "") {
20958
20958
  return parseInt(slen, 10);
20959
20959
  }
@@ -21005,13 +21005,13 @@ var require_kv = __commonJS({
21005
21005
  data = this.codec.value.encode(data);
21006
21006
  const o = { timeout: opts?.timeout };
21007
21007
  if (opts.previousSeq !== void 0) {
21008
- const h = (0, internal_1.headers)();
21009
- o.headers = h;
21010
- h.set(internal_2.PubHeaders.ExpectedLastSubjectSequenceHdr, `${opts.previousSeq}`);
21008
+ const h2 = (0, internal_1.headers)();
21009
+ o.headers = h2;
21010
+ h2.set(internal_2.PubHeaders.ExpectedLastSubjectSequenceHdr, `${opts.previousSeq}`);
21011
21011
  }
21012
21012
  if (markerTTL) {
21013
- const h = o.headers || (0, internal_1.headers)();
21014
- h.set(internal_2.PubHeaders.MessageTTL, markerTTL);
21013
+ const h2 = o.headers || (0, internal_1.headers)();
21014
+ h2.set(internal_2.PubHeaders.MessageTTL, markerTTL);
21015
21015
  }
21016
21016
  try {
21017
21017
  const pa = await this.js.publish(this.subjectForKey(ek, true), data, o);
@@ -21105,19 +21105,19 @@ var require_kv = __commonJS({
21105
21105
  async _doDeleteOrPurge(k, op, opts) {
21106
21106
  const ek = this.encodeKey(k);
21107
21107
  this.validateKey(ek);
21108
- const h = (0, internal_1.headers)();
21109
- h.set(exports2.kvOperationHdr, op);
21108
+ const h2 = (0, internal_1.headers)();
21109
+ h2.set(exports2.kvOperationHdr, op);
21110
21110
  if (op === "PURGE") {
21111
21111
  const popts = opts;
21112
- h.set(internal_2.JsHeaders.RollupHdr, internal_2.JsHeaders.RollupValueSubject);
21112
+ h2.set(internal_2.JsHeaders.RollupHdr, internal_2.JsHeaders.RollupValueSubject);
21113
21113
  if (typeof popts?.ttl === "string" && popts.ttl !== "") {
21114
- h.set(internal_2.PubHeaders.MessageTTL, `${popts.ttl}`);
21114
+ h2.set(internal_2.PubHeaders.MessageTTL, `${popts.ttl}`);
21115
21115
  }
21116
21116
  }
21117
21117
  if (opts?.previousSeq) {
21118
- h.set(internal_2.PubHeaders.ExpectedLastSubjectSequenceHdr, `${opts.previousSeq}`);
21118
+ h2.set(internal_2.PubHeaders.ExpectedLastSubjectSequenceHdr, `${opts.previousSeq}`);
21119
21119
  }
21120
- await this.js.publish(this.subjectForKey(ek, true), internal_1.Empty, { headers: h });
21120
+ await this.js.publish(this.subjectForKey(ek, true), internal_1.Empty, { headers: h2 });
21121
21121
  }
21122
21122
  _buildCC(k, content, opts = {}) {
21123
21123
  const a = !Array.isArray(k) ? [k] : k;
@@ -22869,7 +22869,7 @@ var require_sha256 = __commonJS({
22869
22869
  this.hash();
22870
22870
  };
22871
22871
  Sha256.prototype.hash = function() {
22872
- var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, h = this.h7, blocks2 = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc;
22872
+ var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, h2 = this.h7, blocks2 = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc;
22873
22873
  for (j = 16; j < 64; ++j) {
22874
22874
  t1 = blocks2[j - 15];
22875
22875
  s0 = (t1 >>> 7 | t1 << 25) ^ (t1 >>> 18 | t1 << 14) ^ t1 >>> 3;
@@ -22883,12 +22883,12 @@ var require_sha256 = __commonJS({
22883
22883
  if (this.is224) {
22884
22884
  ab = 300032;
22885
22885
  t1 = blocks2[0] - 1413257819;
22886
- h = t1 - 150054599 << 0;
22886
+ h2 = t1 - 150054599 << 0;
22887
22887
  d = t1 + 24177077 << 0;
22888
22888
  } else {
22889
22889
  ab = 704751109;
22890
22890
  t1 = blocks2[0] - 210244248;
22891
- h = t1 - 1521486534 << 0;
22891
+ h2 = t1 - 1521486534 << 0;
22892
22892
  d = t1 + 143694565 << 0;
22893
22893
  }
22894
22894
  this.first = false;
@@ -22898,16 +22898,16 @@ var require_sha256 = __commonJS({
22898
22898
  ab = a & b;
22899
22899
  maj = ab ^ a & c ^ bc;
22900
22900
  ch = e & f ^ ~e & g;
22901
- t1 = h + s1 + ch + K[j] + blocks2[j];
22901
+ t1 = h2 + s1 + ch + K[j] + blocks2[j];
22902
22902
  t2 = s0 + maj;
22903
- h = d + t1 << 0;
22903
+ h2 = d + t1 << 0;
22904
22904
  d = t1 + t2 << 0;
22905
22905
  }
22906
22906
  s0 = (d >>> 2 | d << 30) ^ (d >>> 13 | d << 19) ^ (d >>> 22 | d << 10);
22907
- s1 = (h >>> 6 | h << 26) ^ (h >>> 11 | h << 21) ^ (h >>> 25 | h << 7);
22907
+ s1 = (h2 >>> 6 | h2 << 26) ^ (h2 >>> 11 | h2 << 21) ^ (h2 >>> 25 | h2 << 7);
22908
22908
  da = d & a;
22909
22909
  maj = da ^ d & b ^ ab;
22910
- ch = h & e ^ ~h & f;
22910
+ ch = h2 & e ^ ~h2 & f;
22911
22911
  t1 = g + s1 + ch + K[j + 1] + blocks2[j + 1];
22912
22912
  t2 = s0 + maj;
22913
22913
  g = c + t1 << 0;
@@ -22916,7 +22916,7 @@ var require_sha256 = __commonJS({
22916
22916
  s1 = (g >>> 6 | g << 26) ^ (g >>> 11 | g << 21) ^ (g >>> 25 | g << 7);
22917
22917
  cd = c & d;
22918
22918
  maj = cd ^ c & a ^ da;
22919
- ch = g & h ^ ~g & e;
22919
+ ch = g & h2 ^ ~g & e;
22920
22920
  t1 = f + s1 + ch + K[j + 2] + blocks2[j + 2];
22921
22921
  t2 = s0 + maj;
22922
22922
  f = b + t1 << 0;
@@ -22925,7 +22925,7 @@ var require_sha256 = __commonJS({
22925
22925
  s1 = (f >>> 6 | f << 26) ^ (f >>> 11 | f << 21) ^ (f >>> 25 | f << 7);
22926
22926
  bc = b & c;
22927
22927
  maj = bc ^ b & d ^ cd;
22928
- ch = f & g ^ ~f & h;
22928
+ ch = f & g ^ ~f & h2;
22929
22929
  t1 = e + s1 + ch + K[j + 3] + blocks2[j + 3];
22930
22930
  t2 = s0 + maj;
22931
22931
  e = a + t1 << 0;
@@ -22939,7 +22939,7 @@ var require_sha256 = __commonJS({
22939
22939
  this.h4 = this.h4 + e << 0;
22940
22940
  this.h5 = this.h5 + f << 0;
22941
22941
  this.h6 = this.h6 + g << 0;
22942
- this.h7 = this.h7 + h << 0;
22942
+ this.h7 = this.h7 + h2 << 0;
22943
22943
  };
22944
22944
  Sha256.prototype.hex = function() {
22945
22945
  this.finalize();
@@ -23138,13 +23138,13 @@ var require_sha2562 = __commonJS({
23138
23138
  }
23139
23139
  const create = m.createHash;
23140
23140
  factory = () => {
23141
- const h = create("sha256");
23141
+ const h2 = create("sha256");
23142
23142
  return {
23143
23143
  update(data) {
23144
- h.update(data);
23144
+ h2.update(data);
23145
23145
  },
23146
23146
  digest() {
23147
- return new Uint8Array(h.digest());
23147
+ return new Uint8Array(h2.digest());
23148
23148
  }
23149
23149
  };
23150
23150
  };
@@ -23623,16 +23623,16 @@ var require_objectstore = __commonJS({
23623
23623
  const digest = base64_1.Base64UrlPaddedCodec.encode(sha.digest());
23624
23624
  info.digest = `${exports2.digestType}${digest}`;
23625
23625
  info.deleted = false;
23626
- const h = (0, internal_1.headers)();
23626
+ const h2 = (0, internal_1.headers)();
23627
23627
  if (typeof previousRevision === "number") {
23628
- h.set(internal_2.PubHeaders.ExpectedLastSubjectSequenceHdr, `${previousRevision}`);
23628
+ h2.set(internal_2.PubHeaders.ExpectedLastSubjectSequenceHdr, `${previousRevision}`);
23629
23629
  }
23630
- h.set(internal_2.JsHeaders.RollupHdr, internal_2.JsHeaders.RollupValueSubject);
23630
+ h2.set(internal_2.JsHeaders.RollupHdr, internal_2.JsHeaders.RollupValueSubject);
23631
23631
  const ack = fi ? await fi.last(metaSubj, JSON.stringify(info), {
23632
- headers: h,
23632
+ headers: h2,
23633
23633
  timeout
23634
23634
  }) : await this.js.publish(metaSubj, JSON.stringify(info), {
23635
- headers: h,
23635
+ headers: h2,
23636
23636
  timeout
23637
23637
  });
23638
23638
  info.revision = ack.seq;
@@ -23840,10 +23840,10 @@ var require_objectstore = __commonJS({
23840
23840
  info.size = 0;
23841
23841
  info.chunks = 0;
23842
23842
  info.digest = "";
23843
- const h = (0, internal_1.headers)();
23844
- h.set(internal_2.JsHeaders.RollupHdr, internal_2.JsHeaders.RollupValueSubject);
23843
+ const h2 = (0, internal_1.headers)();
23844
+ h2.set(internal_2.JsHeaders.RollupHdr, internal_2.JsHeaders.RollupValueSubject);
23845
23845
  await this.js.publish(this._metaSubject(info.name), JSON.stringify(info), {
23846
- headers: h
23846
+ headers: h2
23847
23847
  });
23848
23848
  return this.jsm.streams.purge(this.stream, {
23849
23849
  filter: this._chunkSubject(info.nuid)
@@ -55478,7 +55478,7 @@ var StdioServerTransport = class {
55478
55478
 
55479
55479
  // ../connector-core/dist/config.js
55480
55480
  var import_node_os = require("node:os");
55481
- var import_node_fs2 = require("node:fs");
55481
+ var import_node_fs3 = require("node:fs");
55482
55482
 
55483
55483
  // ../../packages/core/dist/subjects.js
55484
55484
  var import_node_crypto = require("node:crypto");
@@ -57220,6 +57220,10 @@ for (const def of [...Object.values(RECORD_KINDS), ...AUTHORITY_KIND_DEFS]) {
57220
57220
  Object.freeze(AUTHORITY_KIND_DEFS);
57221
57221
  Object.freeze(RECORD_KINDS);
57222
57222
  var AUTHORITY_DEF_SET = new Set(AUTHORITY_KIND_DEFS);
57223
+ function isCasLoss(e) {
57224
+ const code = e?.code;
57225
+ return code === 10071 || code === 10164;
57226
+ }
57223
57227
 
57224
57228
  // ../../packages/core/dist/endpoint-journal.js
57225
57229
  var import_transport_node = __toESM(require_transport_node(), 1);
@@ -57545,8 +57549,8 @@ function assertDeadline(deadlineMs, what = "deadlineMs") {
57545
57549
  return deadlineMs;
57546
57550
  }
57547
57551
  function isNoRespondersMsg(msg) {
57548
- const h = msg.headers;
57549
- return h != null && (h.code === 503 || h.status === "503");
57552
+ const h2 = msg.headers;
57553
+ return h2 != null && (h2.code === 503 || h2.status === "503");
57550
57554
  }
57551
57555
  async function raceBounded(read, ms, what, details) {
57552
57556
  let t;
@@ -57566,10 +57570,10 @@ var unansweredDetail = (op) => ({ kind: EP_UNANSWERED, endpoint: op.endpoint, co
57566
57570
  function replySubjectFor(space, caller, n) {
57567
57571
  return `${spacePrefix(space)}.ep.reply.*.*.*.${callerTokens(caller).join(".")}.${n}`;
57568
57572
  }
57569
- function staleEpochRefusal(op, responder, held, reference, rail) {
57573
+ function staleEpochRefusal(op, responder, held2, reference, rail) {
57570
57574
  const who2 = `the ${op.endpoint} instance ${responder.instanceId}`;
57571
- const ahead = responder.epoch > held;
57572
- const situation = reference === "bind" ? ahead ? `${who2} answered at epoch ${responder.epoch}, but this handle resolved against it at epoch ${held}: a SUCCESSOR of the bound incarnation answered (a restart or supersession), so the handle is the stale side; re-resolve to adopt it` : `${who2} answered at epoch ${responder.epoch}, but this handle resolved against it at epoch ${held}: a SUPERSEDED incarnation still connected answered, and its reply is rejected` : ahead ? `${who2} answered at epoch ${responder.epoch}, but a currency read of its registered epoch returned ${held}: the responder is ahead of the registry read (the read lags a restart), so its reply is rejected until the read catches up; nothing of this caller's is stale` : `${who2} answered at epoch ${responder.epoch}, but a currency read of its registered epoch returned ${held}: a SUPERSEDED incarnation still connected answered, and its reply is rejected`;
57575
+ const ahead = responder.epoch > held2;
57576
+ const situation = reference === "bind" ? ahead ? `${who2} answered at epoch ${responder.epoch}, but this handle resolved against it at epoch ${held2}: a SUCCESSOR of the bound incarnation answered (a restart or supersession), so the handle is the stale side; re-resolve to adopt it` : `${who2} answered at epoch ${responder.epoch}, but this handle resolved against it at epoch ${held2}: a SUPERSEDED incarnation still connected answered, and its reply is rejected` : ahead ? `${who2} answered at epoch ${responder.epoch}, but a currency read of its registered epoch returned ${held2}: the responder is ahead of the registry read (the read lags a restart), so its reply is rejected until the read catches up; nothing of this caller's is stale` : `${who2} answered at epoch ${responder.epoch}, but a currency read of its registered epoch returned ${held2}: a SUPERSEDED incarnation still connected answered, and its reply is rejected`;
57573
57577
  const detail = {
57574
57578
  kind: EP_UNBOUND_RESPONDER,
57575
57579
  endpoint: op.endpoint,
@@ -57577,7 +57581,7 @@ function staleEpochRefusal(op, responder, held, reference, rail) {
57577
57581
  answeredBy: responder.instanceId,
57578
57582
  ...reference === "bind" ? { boundTo: responder.instanceId } : {},
57579
57583
  answeredEpoch: responder.epoch,
57580
- heldEpoch: held,
57584
+ heldEpoch: held2,
57581
57585
  reference,
57582
57586
  pinned: rail === "inst"
57583
57587
  };
@@ -58684,8 +58688,8 @@ var Types;
58684
58688
  })(Types || (Types = {}));
58685
58689
  (function(nacl2) {
58686
58690
  "use strict";
58687
- var u64 = function(h, l) {
58688
- this.hi = h | 0 >>> 0;
58691
+ var u64 = function(h2, l) {
58692
+ this.hi = h2 | 0 >>> 0;
58689
58693
  this.lo = l | 0 >>> 0;
58690
58694
  };
58691
58695
  var gf = function(init) {
@@ -58800,9 +58804,9 @@ var Types;
58800
58804
  return u << 8 | x[i + 0] & 255;
58801
58805
  }
58802
58806
  function dl64(x, i) {
58803
- var h = x[i] << 24 | x[i + 1] << 16 | x[i + 2] << 8 | x[i + 3];
58807
+ var h2 = x[i] << 24 | x[i + 1] << 16 | x[i + 2] << 8 | x[i + 3];
58804
58808
  var l = x[i + 4] << 24 | x[i + 5] << 16 | x[i + 6] << 8 | x[i + 7];
58805
- return new u64(h, l);
58809
+ return new u64(h2, l);
58806
58810
  }
58807
58811
  function st32(x, j, u) {
58808
58812
  var i;
@@ -58832,7 +58836,7 @@ var Types;
58832
58836
  function crypto_verify_32(x, xi, y, yi) {
58833
58837
  return vn(x, xi, y, yi, 32);
58834
58838
  }
58835
- function core(out, inp, k, c, h) {
58839
+ function core(out, inp, k, c, h2) {
58836
58840
  var w = new Uint32Array(16), x = new Uint32Array(16), y = new Uint32Array(16), t = new Uint32Array(4);
58837
58841
  var i, j, m;
58838
58842
  for (i = 0; i < 4; i++) {
@@ -58853,7 +58857,7 @@ var Types;
58853
58857
  }
58854
58858
  for (m = 0; m < 16; m++) x[m] = w[m];
58855
58859
  }
58856
- if (h) {
58860
+ if (h2) {
58857
58861
  for (i = 0; i < 16; i++) x[i] = x[i] + y[i] | 0;
58858
58862
  for (i = 0; i < 4; i++) {
58859
58863
  x[5 * i] = x[5 * i] - ld32(c, 4 * i) | 0;
@@ -58931,11 +58935,11 @@ var Types;
58931
58935
  crypto_core_hsalsa20(s, n, k, sigma);
58932
58936
  return crypto_stream_salsa20_xor(c, cpos, m, mpos, d, n.subarray(16), s);
58933
58937
  }
58934
- function add1305(h, c) {
58938
+ function add1305(h2, c) {
58935
58939
  var j, u = 0;
58936
58940
  for (j = 0; j < 17; j++) {
58937
- u = u + (h[j] + c[j] | 0) | 0;
58938
- h[j] = u & 255;
58941
+ u = u + (h2[j] + c[j] | 0) | 0;
58942
+ h2[j] = u & 255;
58939
58943
  u >>>= 8;
58940
58944
  }
58941
58945
  }
@@ -58960,8 +58964,8 @@ var Types;
58960
58964
  ]);
58961
58965
  function crypto_onetimeauth(out, outpos, m, mpos, n, k) {
58962
58966
  var s, i, j, u;
58963
- var x = new Uint32Array(17), r = new Uint32Array(17), h = new Uint32Array(17), c = new Uint32Array(17), g = new Uint32Array(17);
58964
- for (j = 0; j < 17; j++) r[j] = h[j] = 0;
58967
+ var x = new Uint32Array(17), r = new Uint32Array(17), h2 = new Uint32Array(17), c = new Uint32Array(17), g = new Uint32Array(17);
58968
+ for (j = 0; j < 17; j++) r[j] = h2[j] = 0;
58965
58969
  for (j = 0; j < 16; j++) r[j] = k[j];
58966
58970
  r[3] &= 15;
58967
58971
  r[4] &= 252;
@@ -58976,43 +58980,43 @@ var Types;
58976
58980
  c[j] = 1;
58977
58981
  mpos += j;
58978
58982
  n -= j;
58979
- add1305(h, c);
58983
+ add1305(h2, c);
58980
58984
  for (i = 0; i < 17; i++) {
58981
58985
  x[i] = 0;
58982
- for (j = 0; j < 17; j++) x[i] = x[i] + h[j] * (j <= i ? r[i - j] : 320 * r[i + 17 - j] | 0) | 0 | 0;
58986
+ for (j = 0; j < 17; j++) x[i] = x[i] + h2[j] * (j <= i ? r[i - j] : 320 * r[i + 17 - j] | 0) | 0 | 0;
58983
58987
  }
58984
- for (i = 0; i < 17; i++) h[i] = x[i];
58988
+ for (i = 0; i < 17; i++) h2[i] = x[i];
58985
58989
  u = 0;
58986
58990
  for (j = 0; j < 16; j++) {
58987
- u = u + h[j] | 0;
58988
- h[j] = u & 255;
58991
+ u = u + h2[j] | 0;
58992
+ h2[j] = u & 255;
58989
58993
  u >>>= 8;
58990
58994
  }
58991
- u = u + h[16] | 0;
58992
- h[16] = u & 3;
58995
+ u = u + h2[16] | 0;
58996
+ h2[16] = u & 3;
58993
58997
  u = 5 * (u >>> 2) | 0;
58994
58998
  for (j = 0; j < 16; j++) {
58995
- u = u + h[j] | 0;
58996
- h[j] = u & 255;
58999
+ u = u + h2[j] | 0;
59000
+ h2[j] = u & 255;
58997
59001
  u >>>= 8;
58998
59002
  }
58999
- u = u + h[16] | 0;
59000
- h[16] = u;
59003
+ u = u + h2[16] | 0;
59004
+ h2[16] = u;
59001
59005
  }
59002
- for (j = 0; j < 17; j++) g[j] = h[j];
59003
- add1305(h, minusp);
59004
- s = -(h[16] >>> 7) | 0;
59005
- for (j = 0; j < 17; j++) h[j] ^= s & (g[j] ^ h[j]);
59006
+ for (j = 0; j < 17; j++) g[j] = h2[j];
59007
+ add1305(h2, minusp);
59008
+ s = -(h2[16] >>> 7) | 0;
59009
+ for (j = 0; j < 17; j++) h2[j] ^= s & (g[j] ^ h2[j]);
59006
59010
  for (j = 0; j < 16; j++) c[j] = k[j + 16];
59007
59011
  c[16] = 0;
59008
- add1305(h, c);
59009
- for (j = 0; j < 16; j++) out[outpos + j] = h[j];
59012
+ add1305(h2, c);
59013
+ for (j = 0; j < 16; j++) out[outpos + j] = h2[j];
59010
59014
  return 0;
59011
59015
  }
59012
- function crypto_onetimeauth_verify(h, hpos, m, mpos, n, k) {
59016
+ function crypto_onetimeauth_verify(h2, hpos, m, mpos, n, k) {
59013
59017
  var x = new Uint8Array(16);
59014
59018
  crypto_onetimeauth(x, 0, m, mpos, n, k);
59015
- return crypto_verify_16(h, hpos, x, 0);
59019
+ return crypto_verify_16(h2, hpos, x, 0);
59016
59020
  }
59017
59021
  function crypto_secretbox(c, m, d, n, k) {
59018
59022
  var i;
@@ -59215,14 +59219,14 @@ var Types;
59215
59219
  return crypto_box_open_afternm(m, c, d, n, k);
59216
59220
  }
59217
59221
  function add64() {
59218
- var a = 0, b = 0, c = 0, d = 0, m16 = 65535, l, h, i;
59222
+ var a = 0, b = 0, c = 0, d = 0, m16 = 65535, l, h2, i;
59219
59223
  for (i = 0; i < arguments.length; i++) {
59220
59224
  l = arguments[i].lo;
59221
- h = arguments[i].hi;
59225
+ h2 = arguments[i].hi;
59222
59226
  a += l & m16;
59223
59227
  b += l >>> 16;
59224
- c += h & m16;
59225
- d += h >>> 16;
59228
+ c += h2 & m16;
59229
+ d += h2 >>> 16;
59226
59230
  }
59227
59231
  b += a >>> 16;
59228
59232
  c += b >>> 16;
@@ -59233,31 +59237,31 @@ var Types;
59233
59237
  return new u64(x.hi >>> c, x.lo >>> c | x.hi << 32 - c);
59234
59238
  }
59235
59239
  function xor64() {
59236
- var l = 0, h = 0, i;
59240
+ var l = 0, h2 = 0, i;
59237
59241
  for (i = 0; i < arguments.length; i++) {
59238
59242
  l ^= arguments[i].lo;
59239
- h ^= arguments[i].hi;
59243
+ h2 ^= arguments[i].hi;
59240
59244
  }
59241
- return new u64(h, l);
59245
+ return new u64(h2, l);
59242
59246
  }
59243
59247
  function R(x, c) {
59244
- var h, l, c1 = 32 - c;
59248
+ var h2, l, c1 = 32 - c;
59245
59249
  if (c < 32) {
59246
- h = x.hi >>> c | x.lo << c1;
59250
+ h2 = x.hi >>> c | x.lo << c1;
59247
59251
  l = x.lo >>> c | x.hi << c1;
59248
59252
  } else if (c < 64) {
59249
- h = x.lo >>> c | x.hi << c1;
59253
+ h2 = x.lo >>> c | x.hi << c1;
59250
59254
  l = x.hi >>> c | x.lo << c1;
59251
59255
  }
59252
- return new u64(h, l);
59256
+ return new u64(h2, l);
59253
59257
  }
59254
59258
  function Ch(x, y, z2) {
59255
- var h = x.hi & y.hi ^ ~x.hi & z2.hi, l = x.lo & y.lo ^ ~x.lo & z2.lo;
59256
- return new u64(h, l);
59259
+ var h2 = x.hi & y.hi ^ ~x.hi & z2.hi, l = x.lo & y.lo ^ ~x.lo & z2.lo;
59260
+ return new u64(h2, l);
59257
59261
  }
59258
59262
  function Maj(x, y, z2) {
59259
- var h = x.hi & y.hi ^ x.hi & z2.hi ^ y.hi & z2.hi, l = x.lo & y.lo ^ x.lo & z2.lo ^ y.lo & z2.lo;
59260
- return new u64(h, l);
59263
+ var h2 = x.hi & y.hi ^ x.hi & z2.hi ^ y.hi & z2.hi, l = x.lo & y.lo ^ x.lo & z2.lo ^ y.lo & z2.lo;
59264
+ return new u64(h2, l);
59261
59265
  }
59262
59266
  function Sigma0(x) {
59263
59267
  return xor64(R(x, 28), R(x, 34), R(x, 39));
@@ -59448,10 +59452,10 @@ var Types;
59448
59452
  121
59449
59453
  ]);
59450
59454
  function crypto_hash(out, m, n) {
59451
- var h = new Uint8Array(64), x = new Uint8Array(256);
59455
+ var h2 = new Uint8Array(64), x = new Uint8Array(256);
59452
59456
  var i, b = n;
59453
- for (i = 0; i < 64; i++) h[i] = iv[i];
59454
- crypto_hashblocks(h, m, n);
59457
+ for (i = 0; i < 64; i++) h2[i] = iv[i];
59458
+ crypto_hashblocks(h2, m, n);
59455
59459
  n %= 128;
59456
59460
  for (i = 0; i < 256; i++) x[i] = 0;
59457
59461
  for (i = 0; i < n; i++) x[i] = m[b - n + i];
@@ -59459,12 +59463,12 @@ var Types;
59459
59463
  n = 256 - 128 * (n < 112 ? 1 : 0);
59460
59464
  x[n - 9] = 0;
59461
59465
  ts64(x, n - 8, new u64(b / 536870912 | 0, b << 3));
59462
- crypto_hashblocks(h, x, n);
59463
- for (i = 0; i < 64; i++) out[i] = h[i];
59466
+ crypto_hashblocks(h2, x, n);
59467
+ for (i = 0; i < 64; i++) out[i] = h2[i];
59464
59468
  return 0;
59465
59469
  }
59466
59470
  function add(p, q) {
59467
- var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf(), g = gf(), h = gf(), t = gf();
59471
+ var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf(), g = gf(), h2 = gf(), t = gf();
59468
59472
  Z(a, p[1], p[0]);
59469
59473
  Z(t, q[1], q[0]);
59470
59474
  M(a, a, t);
@@ -59478,11 +59482,11 @@ var Types;
59478
59482
  Z(e, b, a);
59479
59483
  Z(f, d, c);
59480
59484
  A(g, d, c);
59481
- A(h, b, a);
59485
+ A(h2, b, a);
59482
59486
  M(p[0], e, f);
59483
- M(p[1], h, g);
59487
+ M(p[1], h2, g);
59484
59488
  M(p[2], g, f);
59485
- M(p[3], e, h);
59489
+ M(p[3], e, h2);
59486
59490
  }
59487
59491
  function cswap(p, q, b) {
59488
59492
  var i;
@@ -59609,7 +59613,7 @@ var Types;
59609
59613
  modL(r, x);
59610
59614
  }
59611
59615
  function crypto_sign(sm, m, n, sk) {
59612
- var d = new Uint8Array(64), h = new Uint8Array(64), r = new Uint8Array(64);
59616
+ var d = new Uint8Array(64), h2 = new Uint8Array(64), r = new Uint8Array(64);
59613
59617
  var i, j, x = new Float64Array(64);
59614
59618
  var p = [
59615
59619
  gf(),
@@ -59629,13 +59633,13 @@ var Types;
59629
59633
  scalarbase(p, r);
59630
59634
  pack(sm, p);
59631
59635
  for (i = 32; i < 64; i++) sm[i] = sk[i];
59632
- crypto_hash(h, sm, n + 64);
59633
- reduce(h);
59636
+ crypto_hash(h2, sm, n + 64);
59637
+ reduce(h2);
59634
59638
  for (i = 0; i < 64; i++) x[i] = 0;
59635
59639
  for (i = 0; i < 32; i++) x[i] = r[i];
59636
59640
  for (i = 0; i < 32; i++) {
59637
59641
  for (j = 0; j < 32; j++) {
59638
- x[i + j] += h[i] * d[j];
59642
+ x[i + j] += h2[i] * d[j];
59639
59643
  }
59640
59644
  }
59641
59645
  modL(sm.subarray(32), x);
@@ -59671,7 +59675,7 @@ var Types;
59671
59675
  }
59672
59676
  function crypto_sign_open(m, sm, n, pk) {
59673
59677
  var i;
59674
- var t = new Uint8Array(32), h = new Uint8Array(64);
59678
+ var t = new Uint8Array(32), h2 = new Uint8Array(64);
59675
59679
  var p = [
59676
59680
  gf(),
59677
59681
  gf(),
@@ -59687,9 +59691,9 @@ var Types;
59687
59691
  if (unpackneg(q, pk)) return -1;
59688
59692
  for (i = 0; i < n; i++) m[i] = sm[i];
59689
59693
  for (i = 0; i < 32; i++) m[i + 32] = pk[i];
59690
- crypto_hash(h, m, n);
59691
- reduce(h);
59692
- scalarmult(p, q, h);
59694
+ crypto_hash(h2, m, n);
59695
+ reduce(h2);
59696
+ scalarmult(p, q, h2);
59693
59697
  scalarbase(q, sm.subarray(32));
59694
59698
  add(p, q);
59695
59699
  pack(t, p);
@@ -59930,9 +59934,9 @@ var Types;
59930
59934
  nacl2.sign.signatureLength = crypto_sign_BYTES;
59931
59935
  nacl2.hash = function(msg) {
59932
59936
  checkArrayTypes(msg);
59933
- var h = new Uint8Array(crypto_hash_BYTES);
59934
- crypto_hash(h, msg, msg.length);
59935
- return h;
59937
+ var h2 = new Uint8Array(crypto_hash_BYTES);
59938
+ crypto_hash(h2, msg, msg.length);
59939
+ return h2;
59936
59940
  };
59937
59941
  nacl2.hash.hashLength = crypto_hash_BYTES;
59938
59942
  nacl2.verify = function(x, y) {
@@ -60643,6 +60647,31 @@ function loadAgentFile(path) {
60643
60647
  };
60644
60648
  }
60645
60649
 
60650
+ // ../../packages/core/dist/fs-safe.js
60651
+ var import_node_fs2 = require("node:fs");
60652
+ var import_node_path = require("node:path");
60653
+ function ensureDirNoSymlink(parent, ...segments) {
60654
+ let dir = parent;
60655
+ for (const seg of segments) {
60656
+ dir = (0, import_node_path.join)(dir, seg);
60657
+ let st;
60658
+ try {
60659
+ st = (0, import_node_fs2.lstatSync)(dir);
60660
+ } catch (e) {
60661
+ if (e.code === "ENOENT") {
60662
+ (0, import_node_fs2.mkdirSync)(dir, { mode: 448 });
60663
+ continue;
60664
+ }
60665
+ throw e;
60666
+ }
60667
+ if (st.isSymbolicLink())
60668
+ throw new Error(`refusing to write under "${dir}": it is a symlink`);
60669
+ if (!st.isDirectory())
60670
+ throw new Error(`refusing to write under "${dir}": not a directory`);
60671
+ }
60672
+ return dir;
60673
+ }
60674
+
60646
60675
  // ../../packages/core/dist/secret-fs.js
60647
60676
  var isWin = process.platform === "win32";
60648
60677
 
@@ -62644,11 +62673,11 @@ var CotalEndpoint = class _CotalEndpoint extends import_node_events.EventEmitter
62644
62673
  await this.manager();
62645
62674
  const kv = await this.membersRegistry();
62646
62675
  const existing = await readMember(kv, channel, owner, lifecycleUid);
62647
- const open = existing?.record.state === "durable-active" && existing.record.leaveCursor === void 0;
62648
- if (open && existing.record.activated)
62676
+ const open5 = existing?.record.state === "durable-active" && existing.record.leaveCursor === void 0;
62677
+ if (open5 && existing.record.activated)
62649
62678
  return { durable: true, generation: existing.record.generation };
62650
- const joinCursor = open ? existing.record.joinCursor : await this.chatFrontier();
62651
- const generation = open ? existing.record.generation : (existing?.record.generation ?? 0) + 1;
62679
+ const joinCursor = open5 ? existing.record.joinCursor : await this.chatFrontier();
62680
+ const generation = open5 ? existing.record.generation : (existing?.record.generation ?? 0) + 1;
62652
62681
  const base = {
62653
62682
  channel,
62654
62683
  owner,
@@ -62660,7 +62689,7 @@ var CotalEndpoint = class _CotalEndpoint extends import_node_events.EventEmitter
62660
62689
  writerIdentity: this.card.id,
62661
62690
  updatedAt: Date.now()
62662
62691
  };
62663
- if (!open)
62692
+ if (!open5)
62664
62693
  await commitMember(kv, base);
62665
62694
  const fence = Math.max(await this.chatFrontier(), await this.fanoutDeliveredSeq());
62666
62695
  const cu = await this.catchupCopy(owner, lifecycleUid, channel, joinCursor, fence, generation);
@@ -64255,6 +64284,12 @@ function isAguiFramePart(part) {
64255
64284
  }
64256
64285
  }
64257
64286
 
64287
+ // ../../packages/core/dist/event-channel.js
64288
+ var EVENT_CHANNEL_PREFIX = "events.";
64289
+ function eventChannel(principal) {
64290
+ return `${EVENT_CHANNEL_PREFIX}${principalKey(principal.owner, principal.actor).key}`;
64291
+ }
64292
+
64258
64293
  // ../../packages/core/dist/broker-floor.js
64259
64294
  var BROKER_FLOOR = Object.freeze({ major: 2, minor: 12 });
64260
64295
 
@@ -64328,7 +64363,7 @@ function configFromEnv(env = process.env) {
64328
64363
  userAuth = {
64329
64364
  owner: userVars.owner,
64330
64365
  actor: userVars.actor,
64331
- sentinelCreds: (0, import_node_fs2.readFileSync)(userVars.sentinel, "utf8"),
64366
+ sentinelCreds: (0, import_node_fs3.readFileSync)(userVars.sentinel, "utf8"),
64332
64367
  bearerCmd
64333
64368
  };
64334
64369
  }
@@ -64336,7 +64371,7 @@ function configFromEnv(env = process.env) {
64336
64371
  space: env.COTAL_SPACE?.trim() || link?.space || "demo",
64337
64372
  id: env.COTAL_ID?.trim() || void 0,
64338
64373
  lifecycleUid,
64339
- creds: credsPath ? (0, import_node_fs2.readFileSync)(credsPath, "utf8") : void 0,
64374
+ creds: credsPath ? (0, import_node_fs3.readFileSync)(credsPath, "utf8") : void 0,
64340
64375
  userAuth,
64341
64376
  name,
64342
64377
  role: env.COTAL_ROLE?.trim() || def?.role || void 0,
@@ -64726,13 +64761,13 @@ var MeshAgent = class extends import_node_events2.EventEmitter {
64726
64761
  * holding it has reported — a release from one must not speak for another still in flight. */
64727
64762
  releaseInFlight(ids) {
64728
64763
  for (const id of ids) {
64729
- const held = this.inFlightIds.get(id);
64730
- if (held === void 0)
64764
+ const held2 = this.inFlightIds.get(id);
64765
+ if (held2 === void 0)
64731
64766
  continue;
64732
- if (held <= 1)
64767
+ if (held2 <= 1)
64733
64768
  this.inFlightIds.delete(id);
64734
64769
  else
64735
- this.inFlightIds.set(id, held - 1);
64770
+ this.inFlightIds.set(id, held2 - 1);
64736
64771
  }
64737
64772
  }
64738
64773
  /** Return scoped pending messages and ack them — call only when they're actually surfaced. */
@@ -65221,18 +65256,1821 @@ ${lines.join("\n")}`;
65221
65256
  };
65222
65257
 
65223
65258
  // ../connector-core/dist/launch.js
65224
- function transcriptChannel(name) {
65225
- return `tr-${name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-")}`;
65259
+ function eventChannelForSession(ep) {
65260
+ if (ep.actorIsEphemeral)
65261
+ throw new Error("events are not available for a session with a self-minted identity: this endpoint has no declared id and no credentials, so its actor is a fresh random token per process and its event channel could never match a grant. Launch it with an identity (an authed mesh, or an explicit id) to publish events.");
65262
+ return eventChannel(ep.principal);
65226
65263
  }
65227
65264
 
65228
65265
  // ../connector-core/dist/agui.js
65266
+ var import_node_crypto8 = require("node:crypto");
65267
+ var import_node_path2 = require("node:path");
65268
+ var COTAL_CUSTOM_EVENTS = [];
65269
+ var AGUI_PROTOCOL = "ag-ui/0.0.57";
65270
+ var AguiVocabularyError = class extends Error {
65271
+ constructor(message) {
65272
+ super(message);
65273
+ this.name = "AguiVocabularyError";
65274
+ }
65275
+ };
65276
+ var AguiBrackets = class _AguiBrackets {
65277
+ run;
65278
+ text = /* @__PURE__ */ new Set();
65279
+ reasoning = /* @__PURE__ */ new Set();
65280
+ tools = /* @__PURE__ */ new Set();
65281
+ /**
65282
+ * The machine's whole state, as plain JSON — what the WAL persists so a restart does not lose it.
65283
+ *
65284
+ * Sorted, because this value is written to disk and compared BY A HUMAN reading two documents.
65285
+ * A `Set`'s iteration order is insertion order, so two machines that are semantically identical
65286
+ * would serialize differently depending on the order events happened to arrive, and a diff of two
65287
+ * WALs would show a change where there is none.
65288
+ */
65289
+ snapshot() {
65290
+ return {
65291
+ run: this.run,
65292
+ text: [...this.text].sort(),
65293
+ reasoning: [...this.reasoning].sort(),
65294
+ tools: [...this.tools].sort()
65295
+ };
65296
+ }
65297
+ /** Rebuild a machine from a snapshot. The inverse of {@link snapshot}, and the reason a mid-run
65298
+ * restart can continue instead of refusing its first event. */
65299
+ static restore(s) {
65300
+ const b = new _AguiBrackets();
65301
+ b.run = s.run;
65302
+ for (const id of s.text)
65303
+ b.text.add(id);
65304
+ for (const id of s.reasoning)
65305
+ b.reasoning.add(id);
65306
+ for (const id of s.tools)
65307
+ b.tools.add(id);
65308
+ return b;
65309
+ }
65310
+ /** An independent machine at the same state — used to VALIDATE a batch without advancing the
65311
+ * machine that is in step with the disk. */
65312
+ clone() {
65313
+ return _AguiBrackets.restore(this.snapshot());
65314
+ }
65315
+ /** True while a run is open — i.e. the stream is mid-turn and not at a legal stopping point. */
65316
+ get open() {
65317
+ return this.run !== void 0;
65318
+ }
65319
+ /** The run currently open, for diagnostics and for checking a frame's envelope against it. */
65320
+ get runId() {
65321
+ return this.run;
65322
+ }
65323
+ /** Feed one event. Throws {@link AguiVocabularyError} on the first violation. */
65324
+ accept(event) {
65325
+ const e = event;
65326
+ const t = e.type;
65327
+ if (t === AGUI_EVENT_TYPE.RUN_STARTED) {
65328
+ if (this.run !== void 0)
65329
+ throw new AguiVocabularyError(`RUN_STARTED for "${String(e.runId)}" while run "${this.run}" is still open`);
65330
+ this.run = String(e.runId);
65331
+ return;
65332
+ }
65333
+ if (this.run === void 0)
65334
+ throw new AguiVocabularyError(`${t} emitted outside an open run`);
65335
+ if (t === AGUI_EVENT_TYPE.RUN_FINISHED || t === AGUI_EVENT_TYPE.RUN_ERROR) {
65336
+ if (t === AGUI_EVENT_TYPE.RUN_FINISHED && String(e.runId) !== this.run)
65337
+ throw new AguiVocabularyError(`RUN_FINISHED for "${String(e.runId)}" but the open run is "${this.run}"`);
65338
+ const dangling = [...this.text, ...this.reasoning, ...this.tools];
65339
+ if (dangling.length > 0)
65340
+ throw new AguiVocabularyError(`${t} while still open: ${dangling.join(", ")}`);
65341
+ this.run = void 0;
65342
+ return;
65343
+ }
65344
+ switch (t) {
65345
+ case AGUI_EVENT_TYPE.TEXT_MESSAGE_START:
65346
+ return this.openId(this.text, String(e.messageId), t);
65347
+ case AGUI_EVENT_TYPE.TEXT_MESSAGE_CONTENT:
65348
+ return this.requireOpen(this.text, String(e.messageId), t);
65349
+ case AGUI_EVENT_TYPE.TEXT_MESSAGE_END:
65350
+ return this.closeId(this.text, String(e.messageId), t);
65351
+ case AGUI_EVENT_TYPE.REASONING_MESSAGE_START:
65352
+ return this.openId(this.reasoning, String(e.messageId), t);
65353
+ case AGUI_EVENT_TYPE.REASONING_MESSAGE_CONTENT:
65354
+ return this.requireOpen(this.reasoning, String(e.messageId), t);
65355
+ case AGUI_EVENT_TYPE.REASONING_MESSAGE_END:
65356
+ return this.closeId(this.reasoning, String(e.messageId), t);
65357
+ case AGUI_EVENT_TYPE.TOOL_CALL_START:
65358
+ return this.openId(this.tools, String(e.toolCallId), t);
65359
+ case AGUI_EVENT_TYPE.TOOL_CALL_ARGS:
65360
+ return this.requireOpen(this.tools, String(e.toolCallId), t);
65361
+ case AGUI_EVENT_TYPE.TOOL_CALL_END:
65362
+ return this.closeId(this.tools, String(e.toolCallId), t);
65363
+ case AGUI_EVENT_TYPE.TOOL_CALL_RESULT:
65364
+ if (this.tools.has(String(e.toolCallId)))
65365
+ throw new AguiVocabularyError(`TOOL_CALL_RESULT for "${String(e.toolCallId)}" while its call is still open`);
65366
+ return;
65367
+ case AGUI_EVENT_TYPE.CUSTOM:
65368
+ if (!COTAL_CUSTOM_EVENTS.includes(String(e.name)))
65369
+ throw new AguiVocabularyError(`CUSTOM "${String(e.name)}" is not declared in COTAL_CUSTOM_EVENTS (the v1 table is empty by specification)`);
65370
+ return;
65371
+ default:
65372
+ throw new AguiVocabularyError(`${t} is not in the mapped subset this plane emits`);
65373
+ }
65374
+ }
65375
+ /**
65376
+ * Assert the stream is at a legal stopping point.
65377
+ *
65378
+ * Called at the end of a synthesized sequence and by any consumer checking a writer closed
65379
+ * cleanly. NOT called per frame: mid-turn frames are legally unbalanced.
65380
+ */
65381
+ assertClosed() {
65382
+ if (this.run !== void 0)
65383
+ throw new AguiVocabularyError(`run "${this.run}" was never closed`);
65384
+ }
65385
+ openId(set3, id, t) {
65386
+ if (set3.has(id))
65387
+ throw new AguiVocabularyError(`${t} re-opened "${id}" while already open`);
65388
+ set3.add(id);
65389
+ }
65390
+ requireOpen(set3, id, t) {
65391
+ if (!set3.has(id))
65392
+ throw new AguiVocabularyError(`${t} for "${id}" which is not open`);
65393
+ }
65394
+ closeId(set3, id, t) {
65395
+ if (!set3.delete(id))
65396
+ throw new AguiVocabularyError(`${t} for "${id}" which is not open`);
65397
+ }
65398
+ };
65399
+ function aguiFrame(opts) {
65400
+ for (const [field, value] of [
65401
+ ["threadId", opts.threadId],
65402
+ ["runId", opts.runId],
65403
+ ["epoch", opts.epoch]
65404
+ ])
65405
+ if (typeof value !== "string" || value.length === 0)
65406
+ throw new AguiVocabularyError(`frame ${field} must be a non-empty string`);
65407
+ if (!Number.isSafeInteger(opts.seq) || opts.seq < 0)
65408
+ throw new AguiVocabularyError(`frame seq must be a non-negative safe integer, got ${JSON.stringify(opts.seq)}`);
65409
+ if (!Array.isArray(opts.events) || opts.events.length === 0)
65410
+ throw new AguiVocabularyError("a frame must carry at least one event");
65411
+ return {
65412
+ kind: AGUI_FRAME_KIND,
65413
+ protocol: AGUI_PROTOCOL,
65414
+ threadId: opts.threadId,
65415
+ runId: opts.runId,
65416
+ epoch: opts.epoch,
65417
+ seq: opts.seq,
65418
+ events: opts.events
65419
+ };
65420
+ }
65421
+ function runStarted(o) {
65422
+ return {
65423
+ type: AGUI_EVENT_TYPE.RUN_STARTED,
65424
+ threadId: o.threadId,
65425
+ runId: o.runId,
65426
+ timestamp: o.timestamp,
65427
+ ...o.cotal ? { cotal: o.cotal } : {}
65428
+ };
65429
+ }
65430
+ function assertInterrupts(list) {
65431
+ if (!Array.isArray(list) || list.length === 0)
65432
+ throw new AguiVocabularyError("outcome.interrupts must be a non-empty array when the outcome is an interrupt");
65433
+ for (const [i, entry] of list.entries()) {
65434
+ const e = entry;
65435
+ if (typeof e !== "object" || e === null)
65436
+ throw new AguiVocabularyError(`outcome.interrupts[${i}] is not an object`);
65437
+ if (typeof e.id !== "string")
65438
+ throw new AguiVocabularyError(`outcome.interrupts[${i}].id must be a string`);
65439
+ if (typeof e.reason !== "string")
65440
+ throw new AguiVocabularyError(`outcome.interrupts[${i}].reason must be a string`);
65441
+ }
65442
+ }
65443
+ function runFinished(o) {
65444
+ if (o.outcome?.type === "interrupt")
65445
+ assertInterrupts(o.outcome.interrupts);
65446
+ return {
65447
+ type: AGUI_EVENT_TYPE.RUN_FINISHED,
65448
+ threadId: o.threadId,
65449
+ runId: o.runId,
65450
+ timestamp: o.timestamp,
65451
+ ...o.outcome ? { outcome: o.outcome } : {},
65452
+ ...o.cotal ? { cotal: o.cotal } : {}
65453
+ };
65454
+ }
65455
+ function textMessageStart(o) {
65456
+ return {
65457
+ type: AGUI_EVENT_TYPE.TEXT_MESSAGE_START,
65458
+ messageId: o.messageId,
65459
+ timestamp: o.timestamp,
65460
+ ...o.role ? { role: o.role } : {},
65461
+ ...o.cotal ? { cotal: o.cotal } : {}
65462
+ };
65463
+ }
65464
+ function textMessageContent(o) {
65465
+ return {
65466
+ type: AGUI_EVENT_TYPE.TEXT_MESSAGE_CONTENT,
65467
+ messageId: o.messageId,
65468
+ delta: o.delta,
65469
+ timestamp: o.timestamp,
65470
+ ...o.cotal ? { cotal: o.cotal } : {}
65471
+ };
65472
+ }
65473
+ function textMessageEnd(o) {
65474
+ return {
65475
+ type: AGUI_EVENT_TYPE.TEXT_MESSAGE_END,
65476
+ messageId: o.messageId,
65477
+ timestamp: o.timestamp,
65478
+ ...o.cotal ? { cotal: o.cotal } : {}
65479
+ };
65480
+ }
65481
+ function toolCallStart(o) {
65482
+ return {
65483
+ type: AGUI_EVENT_TYPE.TOOL_CALL_START,
65484
+ toolCallId: o.toolCallId,
65485
+ toolCallName: o.toolCallName,
65486
+ timestamp: o.timestamp,
65487
+ ...o.parentMessageId ? { parentMessageId: o.parentMessageId } : {},
65488
+ ...o.cotal ? { cotal: o.cotal } : {}
65489
+ };
65490
+ }
65491
+ function toolCallArgs(o) {
65492
+ return {
65493
+ type: AGUI_EVENT_TYPE.TOOL_CALL_ARGS,
65494
+ toolCallId: o.toolCallId,
65495
+ delta: o.delta,
65496
+ timestamp: o.timestamp,
65497
+ ...o.cotal ? { cotal: o.cotal } : {}
65498
+ };
65499
+ }
65500
+ function toolCallEnd(o) {
65501
+ return {
65502
+ type: AGUI_EVENT_TYPE.TOOL_CALL_END,
65503
+ toolCallId: o.toolCallId,
65504
+ timestamp: o.timestamp,
65505
+ ...o.cotal ? { cotal: o.cotal } : {}
65506
+ };
65507
+ }
65508
+ function toolCallResult(o) {
65509
+ return {
65510
+ type: AGUI_EVENT_TYPE.TOOL_CALL_RESULT,
65511
+ messageId: o.messageId,
65512
+ toolCallId: o.toolCallId,
65513
+ content: o.content,
65514
+ timestamp: o.timestamp,
65515
+ ...o.cotal ? { cotal: o.cotal } : {}
65516
+ };
65517
+ }
65518
+ function reasoningMessageStart(o) {
65519
+ return {
65520
+ type: AGUI_EVENT_TYPE.REASONING_MESSAGE_START,
65521
+ messageId: o.messageId,
65522
+ role: "reasoning",
65523
+ timestamp: o.timestamp,
65524
+ ...o.cotal ? { cotal: o.cotal } : {}
65525
+ };
65526
+ }
65527
+ function reasoningMessageContent(o) {
65528
+ return {
65529
+ type: AGUI_EVENT_TYPE.REASONING_MESSAGE_CONTENT,
65530
+ messageId: o.messageId,
65531
+ delta: o.delta,
65532
+ timestamp: o.timestamp,
65533
+ ...o.cotal ? { cotal: o.cotal } : {}
65534
+ };
65535
+ }
65536
+ function reasoningMessageEnd(o) {
65537
+ return {
65538
+ type: AGUI_EVENT_TYPE.REASONING_MESSAGE_END,
65539
+ messageId: o.messageId,
65540
+ timestamp: o.timestamp,
65541
+ ...o.cotal ? { cotal: o.cotal } : {}
65542
+ };
65543
+ }
65229
65544
  var TRUNCATABLE_FIELDS = [
65230
65545
  { type: AGUI_EVENT_TYPE.TOOL_CALL_ARGS, field: "delta" },
65231
65546
  { type: AGUI_EVENT_TYPE.TOOL_CALL_RESULT, field: "content" },
65232
65547
  { type: AGUI_EVENT_TYPE.TEXT_MESSAGE_CONTENT, field: "delta" }
65233
65548
  ];
65549
+ var AguiBracketStateLost = class extends AguiVocabularyError {
65550
+ cause;
65551
+ constructor(message, cause) {
65552
+ super(message);
65553
+ this.cause = cause;
65554
+ this.name = "AguiBracketStateLost";
65555
+ }
65556
+ };
65557
+ var AguiEmitterHalted = class extends Error {
65558
+ reason;
65559
+ constructor(reason, message) {
65560
+ super(message);
65561
+ this.reason = reason;
65562
+ this.name = "AguiEmitterHalted";
65563
+ }
65564
+ };
65234
65565
  var SIZING_ID = "S".repeat(64);
65235
65566
  var SIZING_EXPECTATION = Number.MAX_SAFE_INTEGER;
65567
+ function packUnits(opts) {
65568
+ const { threadId, epoch, measure, limit } = opts;
65569
+ if (!Number.isSafeInteger(limit) || limit <= 0)
65570
+ throw new AguiVocabularyError(`pack limit must be a positive safe integer, got ${JSON.stringify(limit)}`);
65571
+ const out = [];
65572
+ let seq = opts.firstSeq;
65573
+ let batch = [];
65574
+ let batchRun;
65575
+ let batchCursor;
65576
+ const flush = () => {
65577
+ if (batch.length === 0)
65578
+ return;
65579
+ out.push({ frame: aguiFrame({ threadId, runId: batchRun, epoch, seq, events: batch }), cursor: batchCursor });
65580
+ seq += 1;
65581
+ batch = [];
65582
+ batchRun = void 0;
65583
+ batchCursor = void 0;
65584
+ };
65585
+ for (const unit of opts.units) {
65586
+ if (unit.events.length === 0)
65587
+ throw new AguiVocabularyError("packUnits was handed an empty unit; a record that maps to nothing advances the cursor and never becomes a frame");
65588
+ if (batchRun !== void 0 && unit.runId !== batchRun)
65589
+ flush();
65590
+ const candidate2 = [...batch, ...unit.events];
65591
+ const fits = measure(aguiFrame({ threadId, runId: batchRun ?? unit.runId, epoch, seq, events: candidate2 })) <= limit;
65592
+ if (fits) {
65593
+ batch = candidate2;
65594
+ batchRun = batchRun ?? unit.runId;
65595
+ batchCursor = unit.cursor;
65596
+ continue;
65597
+ }
65598
+ flush();
65599
+ const alone = aguiFrame({ threadId, runId: unit.runId, epoch, seq, events: unit.events });
65600
+ const aloneBytes = measure(alone);
65601
+ if (aloneBytes > limit)
65602
+ throw new AguiVocabularyError(`a single source observation does not fit in one frame (${aloneBytes} > ${limit} bytes, ${unit.events.length} event(s), run ${unit.runId}). One source observation is one frame, and that rule requires this to fail loud rather than be truncated at a frame boundary: a frame that ends mid-record has no cursor it can honestly store, and a dropped boundary with no gap marker is worse than a halt.`);
65603
+ batch = [...unit.events];
65604
+ batchRun = unit.runId;
65605
+ batchCursor = unit.cursor;
65606
+ }
65607
+ flush();
65608
+ return out;
65609
+ }
65610
+ var AguiEmitter = class _AguiEmitter {
65611
+ ep;
65612
+ wal;
65613
+ source;
65614
+ map;
65615
+ channel;
65616
+ threadId;
65617
+ /**
65618
+ * The bracket machine AT THE FOLDED POSITION — deliberately not "wherever validation got to".
65619
+ *
65620
+ * It advances one frame at a time, immediately before that frame's `beginSend`, so the state
65621
+ * frozen with a pending frame is the state that belongs to it. A machine advanced by the whole
65622
+ * batch up front would freeze a state describing events that had not been sent.
65623
+ */
65624
+ brackets;
65625
+ halted;
65626
+ /** True once THIS process has fed an event through the bracket machine. It is the half of the
65627
+ * restart diagnosis that keeps a genuine mid-stream violation from being blamed on a restart. */
65628
+ fedAnyEvent = false;
65629
+ constructor(ep, wal, source, map2, channel, threadId) {
65630
+ this.ep = ep;
65631
+ this.wal = wal;
65632
+ this.source = source;
65633
+ this.map = map2;
65634
+ this.channel = channel;
65635
+ this.threadId = threadId;
65636
+ this.brackets = wal.brackets ? AguiBrackets.restore(wal.brackets) : new AguiBrackets();
65637
+ }
65638
+ /**
65639
+ * Start an emitter: resolve the channel, run the single-replica preflight, and settle any pending
65640
+ * frame.
65641
+ *
65642
+ * **THIS IS THAT PREFLIGHT'S PRODUCTION CALL SITE, AND UNTIL THIS FUNCTION EXISTED THERE WAS
65643
+ * NONE.** `CotalEndpoint.assertExpectationSemantics()` had zero production callers: it was a
65644
+ * check that shipped, was covered by its own suite, and never ran outside one. That is why it is
65645
+ * called HERE, before recovery and therefore before any publish — a serialized append on an
65646
+ * unverified stream is the exact case it exists to prevent, and doing it after recovery would
65647
+ * leave the one publish that matters most, the re-publish of a frozen frame, outside the guard.
65648
+ */
65649
+ static async start(opts) {
65650
+ const { endpoint, wal } = opts;
65651
+ const channel = eventChannelForSession(endpoint);
65652
+ const live = principalKey(endpoint.principal.owner, endpoint.principal.actor).key;
65653
+ if (wal.principal !== live)
65654
+ throw new Error(`event WAL belongs to principal ${wal.principal}, but this endpoint is ${live} \u2014 refusing to publish under one identity from another's write-ahead log`);
65655
+ await endpoint.assertExpectationSemantics();
65656
+ if (!opts.subjectFrontier || typeof opts.subjectFrontier.advance !== "function")
65657
+ throw new Error(`event emitter for ${channel}: a subject frontier is required \u2014 the subject is shared by every thread of this principal, so the publish expectation cannot come from one thread's log`);
65658
+ await wal.bindSubjectFrontier(opts.subjectFrontier);
65659
+ const em = new _AguiEmitter(endpoint, wal, opts.source, opts.map, channel, wal.threadId);
65660
+ await em.recover();
65661
+ return em;
65662
+ }
65663
+ /** True once the emitter has stopped for good. */
65664
+ get stopped() {
65665
+ return this.halted !== void 0;
65666
+ }
65667
+ /**
65668
+ * Boot recovery, branching on the WAL's tag.
65669
+ *
65670
+ * `acked` NEVER republishes: the frame landed and we know it, so the only remaining work is to
65671
+ * fold. `sent_unacked` is the genuinely uncertain case and republishes with the SAME frozen `id`
65672
+ * and `E` — never the current tip, because re-deriving either is what turns an uncertain publish
65673
+ * into a second, different message.
65674
+ */
65675
+ async recover() {
65676
+ const p = this.wal.pending;
65677
+ if (!p)
65678
+ return;
65679
+ if (p.state === "acked") {
65680
+ await this.wal.fold();
65681
+ return;
65682
+ }
65683
+ await this.attempt({ id: p.id, E: p.E, body: p.body, retry: true });
65684
+ }
65685
+ /**
65686
+ * Read forward, map, pack, and publish. Returns what it did, so a caller can distinguish "nothing
65687
+ * to do" from "did work" without inspecting the WAL.
65688
+ */
65689
+ async pump() {
65690
+ if (this.halted)
65691
+ throw this.halted;
65692
+ if (this.wal.pending)
65693
+ throw new Error(`event emitter for ${this.channel}: a frame is still pending; recovery must settle it before a new read`);
65694
+ const read = await this.source.read(this.wal.frontier.sourceCursor);
65695
+ const units = [];
65696
+ for (const rec of read.records) {
65697
+ const mapped = this.map(rec.value);
65698
+ if (mapped === null || mapped.events.length === 0) {
65699
+ const last = units[units.length - 1];
65700
+ if (last)
65701
+ last.cursor = rec.cursor;
65702
+ continue;
65703
+ }
65704
+ units.push({ runId: mapped.runId, events: mapped.events, cursor: rec.cursor });
65705
+ }
65706
+ if (units.length === 0) {
65707
+ if (read.cursor !== this.wal.frontier.sourceCursor)
65708
+ await this.wal.advanceCursorOnly(read.cursor);
65709
+ return { frames: 0, events: 0 };
65710
+ }
65711
+ const probe = this.brackets.clone();
65712
+ for (const u of units)
65713
+ for (const e of u.events) {
65714
+ try {
65715
+ probe.accept(e);
65716
+ } catch (err2) {
65717
+ throw this.diagnoseBracket(err2);
65718
+ }
65719
+ }
65720
+ this.fedAnyEvent = true;
65721
+ const frames = packUnits({
65722
+ threadId: this.threadId,
65723
+ epoch: this.wal.epoch,
65724
+ firstSeq: this.wal.frontier.seq + 1,
65725
+ units,
65726
+ measure: (f) => this.measure(f),
65727
+ limit: this.ep.maxPayload
65728
+ });
65729
+ let events2 = 0;
65730
+ for (const { frame, cursor } of frames) {
65731
+ await this.publish(frame, cursor);
65732
+ events2 += frame.events.length;
65733
+ }
65734
+ if (read.cursor !== this.wal.frontier.sourceCursor)
65735
+ await this.wal.advanceCursorOnly(read.cursor);
65736
+ return { frames: frames.length, events: events2 };
65737
+ }
65738
+ /**
65739
+ * Close the run this stream currently has open, at a boundary the RECORD STREAM CANNOT SEE.
65740
+ *
65741
+ * **This exists because the two halves of the mapping were specified against different inputs.**
65742
+ * The plan sources `RUN_FINISHED` from a harness lifecycle hook, and the durable plane reads a
65743
+ * FILE: a hook fires in another process and writes no record, so a hook-sourced terminal has no
65744
+ * vehicle into a record-sourced stream. Deriving the terminal from records instead is possible but
65745
+ * lies about time in two ways that matter to a live view: the finish lands only when the NEXT turn
65746
+ * starts, so a finished agent renders as still running, and the last run of a session never closes
65747
+ * at all, because there is no later record to close it on. This is that vehicle.
65748
+ *
65749
+ * It is a FRAME LIKE ANY OTHER: same epoch, same `seq` line, same write-ahead discipline, same
65750
+ * halt rules. The single thing that differs is the cursor, which is republished UNCHANGED, because
65751
+ * this frame consumes no source record. A frame that advanced the cursor here would mark records
65752
+ * consumed that were never mapped.
65753
+ *
65754
+ * Idempotent by construction rather than by a flag: the bracket machine is the only state it
65755
+ * reads, so once the run is closed there is nothing open to close and it answers `null`. That also
65756
+ * makes it safe on a stream whose run was opened by a PREVIOUS process, since the machine is
65757
+ * restored from the WAL.
65758
+ *
65759
+ * @returns the run that was closed, or `null` when the stream was already at a stopping point.
65760
+ */
65761
+ async closeRun(o) {
65762
+ if (this.halted)
65763
+ throw this.halted;
65764
+ if (this.wal.pending)
65765
+ throw new Error(`event emitter for ${this.channel}: a frame is still pending; recovery must settle it before a run can be closed`);
65766
+ const runId = this.brackets.runId;
65767
+ if (runId === void 0)
65768
+ return null;
65769
+ const cursor = this.wal.frontier.sourceCursor;
65770
+ if (cursor === void 0)
65771
+ throw new Error(`event emitter for ${this.channel}: run "${runId}" is open on a frontier that carries no source cursor. A run can only be open because a frame published it, and a frame that published cannot leave the cursor unset, so this WAL disagrees with itself. Refusing to invent a cursor for the closing frame.`);
65772
+ const event = runFinished({
65773
+ threadId: this.threadId,
65774
+ runId,
65775
+ timestamp: o.timestamp,
65776
+ ...o.cotal ? { cotal: o.cotal } : {}
65777
+ });
65778
+ const probe = this.brackets.clone();
65779
+ try {
65780
+ probe.accept(event);
65781
+ } catch (err2) {
65782
+ throw this.diagnoseBracket(err2);
65783
+ }
65784
+ this.fedAnyEvent = true;
65785
+ const frames = packUnits({
65786
+ threadId: this.threadId,
65787
+ epoch: this.wal.epoch,
65788
+ firstSeq: this.wal.frontier.seq + 1,
65789
+ units: [{ runId, events: [event], cursor }],
65790
+ measure: (f) => this.measure(f),
65791
+ limit: this.ep.maxPayload
65792
+ });
65793
+ for (const { frame, cursor: c } of frames)
65794
+ await this.publish(frame, c);
65795
+ return runId;
65796
+ }
65797
+ /** Measure a candidate frame EXACTLY as the wire will, at an upper bound over id and expectation. */
65798
+ measure(frame) {
65799
+ return this.ep.encodedSize({
65800
+ channel: this.channel,
65801
+ parts: [frame],
65802
+ id: SIZING_ID,
65803
+ expectedLastSubjectSeq: SIZING_EXPECTATION
65804
+ });
65805
+ }
65806
+ /** Transition 1 then the first network attempt. */
65807
+ async publish(frame, cursor) {
65808
+ for (const e of frame.events)
65809
+ this.brackets.accept(e);
65810
+ const brackets = this.brackets.snapshot();
65811
+ const id = (0, import_node_crypto8.randomUUID)();
65812
+ const E = this.wal.expectedTip;
65813
+ const body = [frame];
65814
+ await this.wal.beginSend({ id, E, seq: frame.seq, sourceCursor: cursor, body, brackets });
65815
+ await this.attempt({ id, E, body, retry: false });
65816
+ }
65817
+ /**
65818
+ * One publish attempt — first or retry — with the FROZEN id and the FROZEN `E`. Never the tip.
65819
+ *
65820
+ * The three outcomes are not symmetric and the asymmetry is the design:
65821
+ * - `!duplicate` → transition 2 then 3. Success becomes durable before the frontier moves.
65822
+ * - `duplicate` → HALT. On a first attempt it means a body WE DID NOT WRITE holds our id, and
65823
+ * folding its `ackSeq` would advance the frontier and the source cursor past events that were
65824
+ * never published. On a retry it cannot happen on a single-replica stream at all, because such a
65825
+ * stream evaluates the expectation before the dedup cache, so observing it proves the stream is
65826
+ * not single-replica. Both are
65827
+ * fail-loud, and neither is a case where guessing is better than stopping.
65828
+ * - CAS loss → HALT. Someone else moved the tip on a subject only this principal may write, or
65829
+ * the subject was purged. Uncertainty plus a moved tip is exactly what must not be guessed at.
65830
+ *
65831
+ * A NETWORK error is deliberately none of these: it leaves `pending` as `sent_unacked`, which is
65832
+ * the state that means "we do not know", and the next boot retries the same frozen frame.
65833
+ */
65834
+ async attempt(o) {
65835
+ let ack;
65836
+ try {
65837
+ ({ ack } = await this.ep.multicastExpecting({
65838
+ channel: this.channel,
65839
+ parts: o.body,
65840
+ id: o.id,
65841
+ expectedLastSubjectSeq: o.E
65842
+ }));
65843
+ } catch (e) {
65844
+ if (isCasLoss(e))
65845
+ throw this.halt("cas-loss", `event emitter for ${this.channel}: the subject tip is no longer ${o.E} (${e.message}). The broker ACL confines this subject to one principal, so the tip moved for one of: a CONCURRENT emitter under this same principal. The per-principal lock refuses a second one, but the lock FILE lives under a workspace root, so an emitter started against a DIFFERENT root, or by a path that never takes the lock, meets no lock at all. Another host and a stale pid do not get past it; they refuse the start instead, loudly; a subject frontier record that disagrees with the stream, which is what an interrupted upgrade or a restored backup leaves behind; a RESTORED stream; or a FILTERED PURGE, which returns the tip to 0 for every thread on the channel. One more cause is not a second writer at all: this log's OWN last ack. The shared record advances before the log records the ack, so a crash between those two writes leaves the record ahead of the frozen expectation this frame carries, and the retry publishes a sequence the subject has already passed. On disk it reads as a pending frame in state sent_unacked whose E is BEHIND the record's tip, which a restored record can also look like, so it narrows the search rather than ending it. None of these is resolvable by re-reading the tip, which agent credentials cannot read in any case. Clearing it is an explicit abandonment of epoch, seq, E, cursor and the shared subject record together, and it is VALID ONLY ONCE THE SUBJECT IS ACTUALLY EMPTY, which of the causes above is true of the FILTERED PURGE alone. On any other cause the tip is still where it is, so removing this state does not clear the halt: the next session opens virgin, expects 0, halts on the same tip, and the sibling logs a tip could have been rebuilt from are gone. Purge the channel first, or find the second writer, or match the signature above and stop looking for one. Once the subject really is back to 0, no command performs the abandonment, so by hand it means removing ${(0, import_node_path2.dirname)((0, import_node_path2.dirname)(this.wal.path))} whole, and removing less than that leaves a mixed state the next start refuses.`);
65846
+ throw e;
65847
+ }
65848
+ if (ack.duplicate)
65849
+ throw this.halt("duplicate-ack", `event emitter for ${this.channel}: the broker answered ${o.retry ? "a RETRY" : "a FIRST attempt"} for id ${o.id} with duplicate:true. ` + (o.retry ? `Under the SINGLE-REPLICA RETRY RULE this cannot happen on an R1 stream, which evaluates the subject expectation before the dedup cache \u2014 so either the stream is not R1 or a foreign body holds our stream-wide id. ` : `We have never published this id, so a body we did not write holds it. `) + `Folding this ack would advance the frontier and the source cursor past events that were never published: silent loss of real events. The frontier and cursor are unchanged.`);
65850
+ await this.wal.recordAck(ack.seq);
65851
+ await this.wal.fold();
65852
+ }
65853
+ /**
65854
+ * Decide whether a bracket refusal is the WRITER's fault or OURS, and say which.
65855
+ *
65856
+ * Ours iff ALL THREE hold, and each is load-bearing:
65857
+ * - this process has fed NO event through the machine yet, so the machine cannot have been put
65858
+ * into a bad state by anything we did in this run; and
65859
+ * - the frontier is non-virgin, so frames — and therefore possibly an open `RUN_STARTED` — were
65860
+ * published by a PREVIOUS process; and
65861
+ * - the WAL cannot say what was open. Since v2 the machine is PERSISTED, so an ordinary restart
65862
+ * restores it and never reaches here at all; `null` means the document was migrated from v1 and
65863
+ * genuinely never recorded the state. Without this condition the diagnosis would survive as a
65864
+ * permanent excuse for a case the migration fixed.
65865
+ *
65866
+ * Drop the first condition and a genuine mid-stream violation by the writer gets blamed on a
65867
+ * restart that happened an hour ago. Drop the second and a violation on a virgin thread, where
65868
+ * nothing was ever published and nothing could have been lost, gets blamed on a restart that never
65869
+ * happened. Each condition alone produces a confident, wrong diagnosis — which is worse than the
65870
+ * undiagnosed error it replaced, because a named cause stops the search.
65871
+ */
65872
+ diagnoseBracket(err2) {
65873
+ if (this.fedAnyEvent || this.wal.frontier.seq === 0 || this.wal.brackets !== null)
65874
+ return err2;
65875
+ return new AguiBracketStateLost(`event emitter for ${this.channel}: bracket state was LOST ACROSS A RESTART \u2014 this is not a protocol violation by the writer. This process has emitted nothing yet, but the WAL says frame ${this.wal.frontier.seq} already went out, and the document records NO bracket state (it was migrated from v1, which never stored one), so any run or message the previous process left open is invisible to this one. Resuming from the source cursor therefore lands mid-run and the first event is refused. A WAL written by this build persists the machine and does not reach this path. The underlying refusal was: ${err2.message}`, err2);
65876
+ }
65877
+ halt(reason, message) {
65878
+ this.halted = new AguiEmitterHalted(reason, message);
65879
+ return this.halted;
65880
+ }
65881
+ };
65882
+
65883
+ // ../connector-core/dist/durable-source.js
65884
+ var import_node_crypto9 = require("node:crypto");
65885
+ var import_node_fs4 = require("node:fs");
65886
+ var import_promises = require("node:fs/promises");
65887
+ var JsonlFileSource = class _JsonlFileSource {
65888
+ path;
65889
+ kind = "jsonl-file";
65890
+ constructor(path) {
65891
+ this.path = path;
65892
+ }
65893
+ /**
65894
+ * A cursor is `<dev>:<ino>:<offset>` — canonical, and BOUND TO THE FILE'S IDENTITY.
65895
+ *
65896
+ * The offset alone is not enough: a source replaced by an unrelated file of the same size or
65897
+ * larger reads as an ordinary append, and the reader resumes at a byte offset inside a document
65898
+ * it has never seen. Carrying `dev`/`ino` makes replacement DETECTABLE, which is the only thing
65899
+ * that lets it fail loud instead of returning fabricated records.
65900
+ *
65901
+ * Parsed strictly: `Number()` coerces `" "` to 0 — replaying all history — and accepts `"1e0"`,
65902
+ * `"01"`, `"+1"`. A cursor is persisted state handed back to us later, so a non-canonical one
65903
+ * means something upstream is wrong, not something to guess at.
65904
+ */
65905
+ static parseCursor(cursor) {
65906
+ const m = /^(\d+):(\d+):(0|[1-9]\d*):([0-9a-f]{16})$/.exec(cursor);
65907
+ if (!m)
65908
+ throw new Error(`JsonlFileSource: malformed cursor ${JSON.stringify(cursor)} (want <dev>:<ino>:<offset>:<seal>)`);
65909
+ const offset = Number(m[3]);
65910
+ if (!Number.isSafeInteger(offset))
65911
+ throw new Error(`JsonlFileSource: cursor offset out of range in ${JSON.stringify(cursor)}`);
65912
+ return { dev: m[1], ino: m[2], offset, seal: m[4] };
65913
+ }
65914
+ /**
65915
+ * A seal over **the last 512 bytes before the cursor** — a BOUNDED form of the invariant a
65916
+ * resumable offset wants: *the bytes immediately before my cursor are still the bytes that were
65917
+ * there when I stopped.*
65918
+ *
65919
+ * **THE BOUND IS PART OF THE GUARANTEE AND IS STATED HERE BECAUSE IT IS NOT THE WHOLE PREFIX.**
65920
+ * A rewrite confined to bytes EARLIER than `offset - 512` that preserves the sealed window, the
65921
+ * inode and the size is **not detected** — reproduced independently by three reviewers, including
65922
+ * with an ordinary same-length in-place PII scrub of earlier transcript lines. That is a real
65923
+ * limitation with known edges: a scrub via temp-file+rename changes the inode and IS caught; one
65924
+ * that changes length trips the offset/identity path and IS caught; one touching the sealed window
65925
+ * IS caught. Only same-inode, same-size, in-place, wholly-outside-the-window escapes.
65926
+ *
65927
+ * What that costs is bounded and worth naming precisely: the emit path stays correct, because the
65928
+ * cursor never moves backwards and forward records are read from bytes the rewrite did not touch.
65929
+ * What is lost is the ability to detect that already-consumed on-disk history drifted after we
65930
+ * read it. If whole-prefix integrity is ever required, this span must cover it — or the cursor has
65931
+ * to carry a rolling hash instead of a window.
65932
+ *
65933
+ * `dev`/`ino` catch unlink-and-recreate, but **not an in-place rewrite** (`writeFileSync` with no
65934
+ * unlink keeps the inode), and that case resumes at a byte offset inside a different document and
65935
+ * emits fragments of it as records (`fmae-rev-eng`, CONFIRMED). Size cannot catch it either when
65936
+ * the replacement is larger.
65937
+ *
65938
+ * Note what this deliberately does NOT flag: a rewrite that reproduces the same preceding bytes.
65939
+ * There the consumed prefix is genuinely unchanged, so resuming is correct — the seal states an
65940
+ * invariant rather than guessing at intent.
65941
+ */
65942
+ static async sealAt(fh, offset) {
65943
+ const span = Math.min(offset, 512);
65944
+ const buf = Buffer.allocUnsafe(span);
65945
+ if (span > 0)
65946
+ await fh.read(buf, 0, span, offset - span);
65947
+ return (0, import_node_crypto9.createHash)("sha256").update(buf.subarray(0, span)).digest("hex").slice(0, 16);
65948
+ }
65949
+ /** Offset just past the last COMPLETE line at or before `limit` — a safe boundary to resume at. */
65950
+ static async lastCompleteBoundary(fh, limit) {
65951
+ if (limit === 0)
65952
+ return 0;
65953
+ const window2 = 64 * 1024;
65954
+ let searched = 0;
65955
+ while (searched < limit) {
65956
+ const len = Math.min(window2, limit - searched);
65957
+ const at = limit - searched - len;
65958
+ const buf = Buffer.allocUnsafe(len);
65959
+ const { bytesRead } = await fh.read(buf, 0, len, at);
65960
+ const idx = buf.subarray(0, bytesRead).lastIndexOf(10);
65961
+ if (idx !== -1)
65962
+ return at + idx + 1;
65963
+ searched += len;
65964
+ }
65965
+ return 0;
65966
+ }
65967
+ async read(cursor) {
65968
+ const fh = await (0, import_promises.open)(this.path, import_node_fs4.constants.O_RDONLY | (import_node_fs4.constants.O_NOFOLLOW ?? 0));
65969
+ try {
65970
+ const st = await fh.stat();
65971
+ const size = st.size;
65972
+ const dev = String(st.dev), ino = String(st.ino);
65973
+ const here = async (offset) => `${dev}:${ino}:${offset}:${await _JsonlFileSource.sealAt(fh, offset)}`;
65974
+ if (cursor === void 0)
65975
+ return { records: [], cursor: await here(await _JsonlFileSource.lastCompleteBoundary(fh, size)) };
65976
+ const from = _JsonlFileSource.parseCursor(cursor);
65977
+ if (from.dev !== dev || from.ino !== ino)
65978
+ throw new Error(`JsonlFileSource: ${this.path} is not the file this cursor came from (cursor ${from.dev}:${from.ino}, now ${dev}:${ino}) \u2014 it was replaced or rotated`);
65979
+ if (from.offset > size)
65980
+ throw new Error(`JsonlFileSource: cursor offset ${from.offset} is past end ${size} for ${this.path} \u2014 the file was truncated`);
65981
+ const seal = await _JsonlFileSource.sealAt(fh, from.offset);
65982
+ if (seal !== from.seal)
65983
+ throw new Error(`JsonlFileSource: the bytes before offset ${from.offset} in ${this.path} have changed (seal ${from.seal} -> ${seal}) \u2014 the file was rewritten in place, not appended to`);
65984
+ if (from.offset === size)
65985
+ return { records: [], cursor: await here(from.offset) };
65986
+ const len = size - from.offset;
65987
+ const buf = Buffer.allocUnsafe(len);
65988
+ const { bytesRead } = await fh.read(buf, 0, len, from.offset);
65989
+ const chunk = buf.subarray(0, bytesRead);
65990
+ const lastNl = chunk.lastIndexOf(10);
65991
+ if (lastNl === -1)
65992
+ return { records: [], cursor: await here(from.offset) };
65993
+ let complete;
65994
+ try {
65995
+ complete = new TextDecoder("utf-8", { fatal: true }).decode(chunk.subarray(0, lastNl));
65996
+ } catch (e) {
65997
+ throw new Error(`JsonlFileSource: invalid UTF-8 at offset ~${from.offset} in ${this.path}: ${e.message}`);
65998
+ }
65999
+ const records = [];
66000
+ let at = from.offset;
66001
+ for (const line of complete.split("\n")) {
66002
+ at += Buffer.byteLength(line, "utf8") + 1;
66003
+ if (line.trim() === "")
66004
+ continue;
66005
+ let value;
66006
+ try {
66007
+ value = JSON.parse(line);
66008
+ } catch (e) {
66009
+ throw new Error(`JsonlFileSource: unparseable complete line at offset ~${from.offset} in ${this.path}: ${e.message}`);
66010
+ }
66011
+ records.push({ value, cursor: await here(at) });
66012
+ }
66013
+ const end = from.offset + lastNl + 1;
66014
+ if (at !== end)
66015
+ throw new Error(`JsonlFileSource: internal cursor walk ended at ${at} but the batch ends at ${end} in ${this.path}`);
66016
+ return { records, cursor: await here(end) };
66017
+ } finally {
66018
+ await fh.close();
66019
+ }
66020
+ }
66021
+ };
66022
+
66023
+ // ../connector-core/dist/event-wal.js
66024
+ var import_node_crypto10 = require("node:crypto");
66025
+ var import_node_fs5 = require("node:fs");
66026
+ var import_promises2 = require("node:fs/promises");
66027
+ var import_node_path3 = require("node:path");
66028
+ var EVENT_WAL_VERSION = 3;
66029
+ var WalCorruptError = class extends Error {
66030
+ path;
66031
+ invariant;
66032
+ constructor(path, invariant, detail) {
66033
+ super(`event WAL at ${path} is unusable (${invariant}): ${detail}`);
66034
+ this.path = path;
66035
+ this.invariant = invariant;
66036
+ this.name = "WalCorruptError";
66037
+ }
66038
+ };
66039
+ var WalStaleWriterError = class extends Error {
66040
+ path;
66041
+ expectedGen;
66042
+ foundGen;
66043
+ constructor(path, expectedGen, foundGen) {
66044
+ super(`event WAL at ${path} was written by another handle since this one read it (this handle expects generation ${expectedGen}, the file is at ${foundGen === void 0 ? "no file at all" : `generation ${foundGen}`}) \u2014 refusing to overwrite it. A second emitter or a stale handle is writing this principal's log.`);
66045
+ this.path = path;
66046
+ this.expectedGen = expectedGen;
66047
+ this.foundGen = foundGen;
66048
+ this.name = "WalStaleWriterError";
66049
+ }
66050
+ };
66051
+ var isSafeNonNegInt = (n) => Number.isSafeInteger(n) && n >= 0;
66052
+ function docGeneration(path, doc) {
66053
+ if (typeof doc.v === "number" && doc.v < 3)
66054
+ return 0;
66055
+ if (!isSafeNonNegInt(doc.gen))
66056
+ throw new WalCorruptError(path, "gen is a safe non-negative integer", `found gen=${String(doc.gen)} on a v${String(doc.v)} document`);
66057
+ return doc.gen;
66058
+ }
66059
+ async function openExclusiveNoFollow(path) {
66060
+ return (0, import_promises2.open)(path, import_node_fs5.constants.O_WRONLY | import_node_fs5.constants.O_CREAT | import_node_fs5.constants.O_EXCL | (import_node_fs5.constants.O_NOFOLLOW ?? 0), 384);
66061
+ }
66062
+ function assertPendingVintage(path, p, f) {
66063
+ if (p.E !== f.lastSubjectSeq)
66064
+ throw new WalCorruptError(path, "pending.E === frontier.lastSubjectSeq", `pending.E=${p.E} frontier.lastSubjectSeq=${f.lastSubjectSeq} \u2014 the frozen expectation is not the tip this WAL believes, so retrying it can only CAS-halt or append at the wrong position`);
66065
+ if (p.seq !== f.seq + 1)
66066
+ throw new WalCorruptError(path, "pending.seq === frontier.seq + 1", `pending.seq=${p.seq} frontier.seq=${f.seq} \u2014 a pending frame must be exactly the frontier's successor`);
66067
+ if (p.state === "sent_unacked") {
66068
+ if (p.ackSeq !== void 0)
66069
+ throw new WalCorruptError(path, "sent_unacked has no ackSeq", `state is "sent_unacked" but ackSeq=${String(p.ackSeq)} \u2014 the document contradicts its own tag`);
66070
+ return;
66071
+ }
66072
+ if (!isSafeNonNegInt(p.ackSeq))
66073
+ throw new WalCorruptError(path, "acked.ackSeq present", `state is "acked" but ackSeq is ${String(p.ackSeq)}`);
66074
+ if (!(p.ackSeq > f.lastSubjectSeq))
66075
+ throw new WalCorruptError(path, "acked.ackSeq > frontier.lastSubjectSeq", `ackSeq=${p.ackSeq} frontier.lastSubjectSeq=${f.lastSubjectSeq} \u2014 the frontier is of a later vintage than the acked frame, so its sourceCursor may already have passed events that were never published; resuming would drop them with no gap for a consumer to see`);
66076
+ }
66077
+ function parseBrackets(path, field, v, nullable2) {
66078
+ if (v === null && nullable2)
66079
+ return null;
66080
+ if (typeof v !== "object" || v === null)
66081
+ throw new WalCorruptError(path, `${field} is an object${nullable2 ? " or null" : ""}`, JSON.stringify(v));
66082
+ const b = v;
66083
+ if (b.run !== void 0 && typeof b.run !== "string")
66084
+ throw new WalCorruptError(path, `${field}.run is a string or absent`, JSON.stringify(b.run));
66085
+ const lists = { text: b.text, reasoning: b.reasoning, tools: b.tools };
66086
+ for (const [k, list] of Object.entries(lists)) {
66087
+ if (!Array.isArray(list) || list.some((x) => typeof x !== "string"))
66088
+ throw new WalCorruptError(path, `${field}.${k} is an array of strings`, JSON.stringify(list));
66089
+ }
66090
+ if (b.run === void 0 && b.text.concat(b.reasoning, b.tools).length > 0)
66091
+ throw new WalCorruptError(path, `${field} has no open run while messages or tool calls are open`, JSON.stringify(b));
66092
+ return { run: b.run, text: b.text, reasoning: b.reasoning, tools: b.tools };
66093
+ }
66094
+ function parseDoc(path, raw, space, threadId, principal) {
66095
+ let d;
66096
+ try {
66097
+ d = JSON.parse(raw);
66098
+ } catch (e) {
66099
+ throw new WalCorruptError(path, "parseable JSON", e.message);
66100
+ }
66101
+ if (typeof d !== "object" || d === null)
66102
+ throw new WalCorruptError(path, "document is an object", typeof d);
66103
+ const doc = d;
66104
+ if (typeof doc.v !== "number" || !Number.isSafeInteger(doc.v) || doc.v < 1)
66105
+ throw new WalCorruptError(path, "v is a positive integer", `found v=${String(doc.v)}`);
66106
+ if (doc.v > EVENT_WAL_VERSION)
66107
+ throw new WalCorruptError(path, `v <= ${EVENT_WAL_VERSION}`, `this WAL is v${doc.v} and this build understands v${EVENT_WAL_VERSION} \u2014 THE STATE IS NEWER THAN THE CODE, which is what a code rollback across the v2 migration (persisted bracket state) or the v3 one (the write generation) leaves behind. The migration is forward-only by design: there is no downgrade, because a lossy one would silently discard state this file exists to preserve. Run the newer build, or move this WAL aside and accept that the thread restarts from a new epoch.`);
66108
+ if (doc.space !== space)
66109
+ throw new WalCorruptError(path, "space matches", `WAL space=${String(doc.space)} caller=${space}`);
66110
+ if (doc.threadId !== threadId)
66111
+ throw new WalCorruptError(path, "threadId matches", `WAL threadId=${String(doc.threadId)} caller=${threadId}`);
66112
+ if (doc.principal !== principal)
66113
+ throw new WalCorruptError(path, "principal matches", `WAL principal=${String(doc.principal)} caller=${principal}`);
66114
+ if (typeof doc.epoch !== "string" || doc.epoch.length === 0)
66115
+ throw new WalCorruptError(path, "epoch is a non-empty string", String(doc.epoch));
66116
+ const f = doc.frontier;
66117
+ if (!f || !isSafeNonNegInt(f.seq) || !isSafeNonNegInt(f.lastSubjectSeq))
66118
+ throw new WalCorruptError(path, "frontier is well-formed", JSON.stringify(doc.frontier));
66119
+ if (f.sourceCursor !== void 0 && typeof f.sourceCursor !== "string")
66120
+ throw new WalCorruptError(path, "frontier.sourceCursor is a string or absent", typeof f.sourceCursor);
66121
+ if (f.seq === 0 !== (f.lastSubjectSeq === 0))
66122
+ throw new WalCorruptError(path, "frontier.seq and lastSubjectSeq are both zero or both nonzero", JSON.stringify(f));
66123
+ if ((f.seq > 0 || f.lastSubjectSeq > 0) && typeof f.sourceCursor !== "string")
66124
+ throw new WalCorruptError(path, "a nonzero frontier carries its sourceCursor", JSON.stringify(f));
66125
+ if (doc.pending !== null && typeof doc.pending !== "object")
66126
+ throw new WalCorruptError(path, "pending is present (null or an object)", doc.pending === void 0 ? "the key is absent, which is not the same as null" : typeof doc.pending);
66127
+ let pending = null;
66128
+ if (doc.pending !== null) {
66129
+ const p = doc.pending;
66130
+ if (p.state !== "sent_unacked" && p.state !== "acked")
66131
+ throw new WalCorruptError(path, "pending.state is a known tag", String(p.state));
66132
+ if (typeof p.id !== "string" || p.id.length === 0)
66133
+ throw new WalCorruptError(path, "pending.id is a non-empty string", String(p.id));
66134
+ try {
66135
+ assertIdToken(p.id, "event WAL pending.id");
66136
+ } catch (e) {
66137
+ throw new WalCorruptError(path, "pending.id satisfies the wire id grammar", e.message);
66138
+ }
66139
+ if (!isSafeNonNegInt(p.E) || !isSafeNonNegInt(p.seq))
66140
+ throw new WalCorruptError(path, "pending E/seq are safe non-negative integers", JSON.stringify(p));
66141
+ if (typeof p.sourceCursor !== "string")
66142
+ throw new WalCorruptError(path, "pending.sourceCursor is a string", typeof p.sourceCursor);
66143
+ if (!Array.isArray(p.body) || p.body.length === 0)
66144
+ throw new WalCorruptError(path, "pending.body is a non-empty array of parts", JSON.stringify(p.body));
66145
+ if (doc.v === 1)
66146
+ throw new WalCorruptError(path, "a v1 WAL has no frame in flight", `this v1 document holds a ${String(p.state)} frame, and v2 requires the bracket state that belongs to it \u2014 which v1 never recorded and nothing here can reconstruct. Let the older build settle this frame first, then start the newer one.`);
66147
+ p.brackets = parseBrackets(path, "pending.brackets", p.brackets, false);
66148
+ pending = p;
66149
+ assertPendingVintage(path, pending, f);
66150
+ }
66151
+ const brackets = doc.v === 1 ? null : parseBrackets(path, "brackets", doc.brackets, true);
66152
+ return {
66153
+ v: EVENT_WAL_VERSION,
66154
+ // MIGRATED IN MEMORY; it reaches disk on the next durable write.
66155
+ // The generation as the FILE states it, so the first write from this handle compares against
66156
+ // what it actually read rather than against its own migrated shape.
66157
+ gen: docGeneration(path, doc),
66158
+ space,
66159
+ epoch: doc.epoch,
66160
+ threadId,
66161
+ principal,
66162
+ frontier: f,
66163
+ pending,
66164
+ brackets
66165
+ };
66166
+ }
66167
+ var EventWal = class _EventWal {
66168
+ path;
66169
+ doc;
66170
+ /**
66171
+ * Every mutation runs one-at-a-time on this chain.
66172
+ *
66173
+ * Without it, two concurrent `beginSend` calls both read `this.doc.pending === null` before either
66174
+ * durable replace finishes — the guard is an in-memory read that is NOT atomic with the write
66175
+ * across its `await` points. Reviewers reproduced the split: one call fulfils, one rejects, and
66176
+ * the process is left holding `pending.id === "A"` in memory while the disk says `"B"`. Recovery
66177
+ * would then resume the frame on disk while the live emitter retries the other, which breaks the
66178
+ * one thing this file exists to guarantee — that `id` and `E` are frozen and agreed.
66179
+ *
66180
+ * A per-instance chain is sufficient and honest about its scope, and the scope is narrower than it
66181
+ * once claimed: it serializes THIS INSTANCE's callers. It does nothing about a SECOND `EventWal`
66182
+ * on the same file, in this process or another — the chain is per object, so two objects are two
66183
+ * chains and both of them "succeed". That gap was described here as "solved upstream by the
66184
+ * principal-level lock" while no lock was ever acquired. Two things close it now, and neither is
66185
+ * this chain: `acquirePrincipalLock` refuses a second emitter for the principal at start, and
66186
+ * {@link EventWal.assertNotClobbering} refuses a stale handle's write even when it got past that.
66187
+ */
66188
+ chain = Promise.resolve();
66189
+ /** Run `op` after every previously-queued mutation, whether they resolved or threw. */
66190
+ serialize(op) {
66191
+ const next = this.chain.then(op, op);
66192
+ this.chain = next.catch(() => void 0);
66193
+ return next;
66194
+ }
66195
+ constructor(path, doc) {
66196
+ this.path = path;
66197
+ this.doc = doc;
66198
+ }
66199
+ get epoch() {
66200
+ return this.doc.epoch;
66201
+ }
66202
+ /** The principal this WAL was loaded FOR — exposed so a consumer can prove it is holding its own.
66203
+ * `open()` already refuses a document whose stored principal disagrees, but that check protects
66204
+ * the FILE, not the caller: an emitter handed the wrong WAL object entirely would sail past it. */
66205
+ get principal() {
66206
+ return this.doc.principal;
66207
+ }
66208
+ get threadId() {
66209
+ return this.doc.threadId;
66210
+ }
66211
+ /** The bracket machine at the folded position, or `null` when the document cannot say (migrated
66212
+ * from v1). The two are different facts; see {@link WalDoc.brackets}. */
66213
+ get brackets() {
66214
+ return this.doc.brackets;
66215
+ }
66216
+ get frontier() {
66217
+ return { ...this.doc.frontier };
66218
+ }
66219
+ get pending() {
66220
+ return this.doc.pending ? { ...this.doc.pending } : null;
66221
+ }
66222
+ /**
66223
+ * Load an existing WAL, or start a virgin one.
66224
+ *
66225
+ * `subjectMayExist` is the caller's honest statement about whether this principal+thread could
66226
+ * already have published. It is NOT a convenience flag: with it true, a missing or empty WAL is a
66227
+ * refusal, because the tip cannot be inferred — agent creds hold no read shape over the subject,
66228
+ * and guessing `E := 0` either CAS-halts forever or appends under a stale expectation. Recovery
66229
+ * from that state is an explicit operator act, never a startup heuristic.
66230
+ */
66231
+ /** Bound by {@link bindSubjectFrontier}; absent for a WAL nothing publishes from. */
66232
+ subject;
66233
+ static async open(path, opts) {
66234
+ let raw;
66235
+ let bytes;
66236
+ try {
66237
+ bytes = await (0, import_promises2.readFile)(path);
66238
+ } catch (e) {
66239
+ if (e.code !== "ENOENT")
66240
+ throw e;
66241
+ }
66242
+ if (bytes !== void 0) {
66243
+ try {
66244
+ raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
66245
+ } catch {
66246
+ throw new WalCorruptError(path, "the file is valid UTF-8", "invalid UTF-8 bytes; refusing rather than substituting U+FFFD");
66247
+ }
66248
+ }
66249
+ if (raw === void 0) {
66250
+ if (opts.subjectMayExist)
66251
+ throw new WalCorruptError(path, "WAL exists when the subject may", "no WAL file, but this thread may already have published");
66252
+ return new _EventWal(path, _EventWal.virgin(opts.space, opts.threadId, opts.principal));
66253
+ }
66254
+ if (raw.length === 0)
66255
+ throw new WalCorruptError(path, "WAL is non-empty", "the file is zero bytes \u2014 distinct from missing and never treated as a virgin thread");
66256
+ return new _EventWal(path, parseDoc(path, raw, opts.space, opts.threadId, opts.principal));
66257
+ }
66258
+ static virgin(space, threadId, principal) {
66259
+ return {
66260
+ v: EVENT_WAL_VERSION,
66261
+ // Nothing has been written yet, so the first durable replace stamps generation 1 and must find
66262
+ // NO FILE. A file present at generation 0 means somebody else created it while this handle
66263
+ // believed the thread was virgin, and that is refused rather than overwritten.
66264
+ gen: 0,
66265
+ space,
66266
+ epoch: (0, import_node_crypto10.randomUUID)(),
66267
+ threadId,
66268
+ principal,
66269
+ frontier: { seq: 0, lastSubjectSeq: 0, sourceCursor: void 0 },
66270
+ pending: null,
66271
+ // KNOWN empty, not unknown: a virgin thread has published nothing, so "nothing is open" is an
66272
+ // observation this writer can actually make.
66273
+ brackets: { run: void 0, text: [], reasoning: [], tools: [] }
66274
+ };
66275
+ }
66276
+ /**
66277
+ * Bind the PRINCIPAL-scoped subject frontier this thread publishes onto.
66278
+ *
66279
+ * **THE TIP IS NOT THIS THREAD'S TO REMEMBER, AND THAT IS THE WHOLE CORRECTION.**
66280
+ * `frontier.lastSubjectSeq` records the last sequence THIS thread was assigned, which is a true
66281
+ * fact about this log and was mistaken for the subject's tip. The subject is per principal, so a
66282
+ * second session of the same agent opened virgin, expected an empty subject its own predecessor
66283
+ * had filled, and halted forever. Once bound, the bound record is authoritative for the
66284
+ * expectation and this document's own number is history.
66285
+ *
66286
+ * Called once, by {@link AguiEmitter.start}, which is the only thing that drives a WAL toward a
66287
+ * publish. An UNBOUND log still opens, replays and reports its own frontier, so a caller that
66288
+ * only READS one needs no record; but every step toward a publish reads the subject's tip, so
66289
+ * `expectedTip`, `beginSend`, `recordAck` and `abandon` all throw until this has been called.
66290
+ * An earlier version of this sentence said an unbound WAL behaved exactly as it did before,
66291
+ * which was true when it was written and stopped being true in the same change that made the
66292
+ * unbound expectation throw.
66293
+ */
66294
+ async bindSubjectFrontier(frontier) {
66295
+ if (this.subject === frontier)
66296
+ return;
66297
+ if (this.subject)
66298
+ throw new Error(`event WAL ${this.path}: a DIFFERENT subject frontier is already bound`);
66299
+ this.subject = frontier;
66300
+ }
66301
+ /**
66302
+ * The sequence a publish must expect, which is the SUBJECT's tip and not this thread's.
66303
+ *
66304
+ * **UNBOUND IT THROWS, AND AN EARLIER VERSION OF THIS RETURNED THIS DOCUMENT'S OWN LAST ACK.**
66305
+ * That number is the defect's own shape: per session, while the subject is per principal. The
66306
+ * argument for returning it was that no shipped path can reach it, because
66307
+ * {@link AguiEmitter.start} is the only route from a log to a publish and it binds before the
66308
+ * emitter exists. The argument was true, and it is the same argument the released seam shipped
66309
+ * on: two correct components with an assumption standing where a guard belongs, recorded in
66310
+ * prose. So the assumption is a guard now. A caller that drives a log toward a publish without a
66311
+ * frontier fails here rather than republishing an expectation that was never the subject's.
66312
+ */
66313
+ get expectedTip() {
66314
+ if (!this.subject)
66315
+ throw new Error(`event WAL ${this.path}: no subject frontier is bound, so there is no expectation to publish. The subject is shared by every thread of this principal, so this document's own last ack is not it; bind the principal's record with bindSubjectFrontier first.`);
66316
+ return this.subject.tip;
66317
+ }
66318
+ /** Transition 1 — record the frame, with `id` and `E` frozen, BEFORE any publish. */
66319
+ async beginSend(frame) {
66320
+ return this.serialize(async () => {
66321
+ if (this.doc.pending)
66322
+ throw new Error(`event WAL ${this.path}: a frame is already pending; one emit unit is one pending frame`);
66323
+ assertIdToken(frame.id, "event WAL pending id");
66324
+ if (frame.E !== this.expectedTip)
66325
+ throw new Error(`event WAL ${this.path}: E=${frame.E} is not the subject's tip ${this.expectedTip}`);
66326
+ if (frame.seq !== this.doc.frontier.seq + 1)
66327
+ throw new Error(`event WAL ${this.path}: seq=${frame.seq} is not the frontier's successor ${this.doc.frontier.seq + 1}`);
66328
+ if (!Array.isArray(frame.body) || frame.body.length === 0)
66329
+ throw new Error(`event WAL ${this.path}: a frame body must be a non-empty array of parts`);
66330
+ await this.write({ ...this.doc, pending: { state: "sent_unacked", ...frame } });
66331
+ });
66332
+ }
66333
+ /**
66334
+ * Transition 2 — a NON-duplicate ack becomes durable before the frontier moves.
66335
+ * A duplicate ack must never reach here; the caller fails loud on one.
66336
+ */
66337
+ async recordAck(ackSeq) {
66338
+ return this.serialize(async () => {
66339
+ const p = this.doc.pending;
66340
+ if (!p || p.state !== "sent_unacked")
66341
+ throw new Error(`event WAL ${this.path}: no sent_unacked frame to ack`);
66342
+ if (!isSafeNonNegInt(ackSeq))
66343
+ throw new Error(`event WAL ${this.path}: ackSeq must be a safe non-negative integer, got ${String(ackSeq)}`);
66344
+ if (ackSeq <= this.expectedTip)
66345
+ throw new Error(`event WAL ${this.path}: ackSeq=${ackSeq} is not ahead of the subject's tip ${this.expectedTip}`);
66346
+ const subject = this.subject;
66347
+ if (!subject)
66348
+ throw new Error(`event WAL ${this.path}: no subject frontier is bound, so this ack has no shared record to advance`);
66349
+ await this.assertNotClobbering();
66350
+ await subject.advance(ackSeq);
66351
+ await this.write({ ...this.doc, pending: { ...p, state: "acked", ackSeq } });
66352
+ });
66353
+ }
66354
+ /** Transition 3 — fold the acked frame into the frontier and clear pending. */
66355
+ async fold() {
66356
+ return this.serialize(async () => {
66357
+ const p = this.doc.pending;
66358
+ if (!p || p.state !== "acked" || p.ackSeq === void 0)
66359
+ throw new Error(`event WAL ${this.path}: no acked frame to fold`);
66360
+ await this.write({
66361
+ ...this.doc,
66362
+ frontier: { seq: p.seq, lastSubjectSeq: p.ackSeq, sourceCursor: p.sourceCursor },
66363
+ pending: null,
66364
+ // The frame's frozen state becomes the document's, so `brackets` always describes exactly the
66365
+ // events that are PUBLISHED AND FOLDED — never a batch that was validated and not yet sent.
66366
+ brackets: p.brackets
66367
+ });
66368
+ });
66369
+ }
66370
+ /**
66371
+ * Transition 4 — a bounded source range that mapped to NOTHING.
66372
+ *
66373
+ * A mapper SUCCESS returning zero events advances the cursor atomically and alone: no
66374
+ * `seq` consumed, no pending written, no publish. A mapper ERROR never advances it. Empty and
66375
+ * failed must not share a path: conflating them turns a parser bug into silently skipped history.
66376
+ */
66377
+ async advanceCursorOnly(rangeEnd) {
66378
+ return this.serialize(async () => {
66379
+ if (this.doc.pending)
66380
+ throw new Error(`event WAL ${this.path}: cannot advance the cursor while a frame is pending`);
66381
+ await this.write({ ...this.doc, frontier: { ...this.doc.frontier, sourceCursor: rangeEnd } });
66382
+ });
66383
+ }
66384
+ /**
66385
+ * Abandonment — explicit, destructive and TOTAL. Mints a new epoch AND resets `seq`,
66386
+ * `lastSubjectSeq` and `sourceCursor` together, reusing the same subject; the new epoch is what
66387
+ * tells a consumer the chain broke. Partial abandonment is not a state: either all four move or
66388
+ * the emitter stays halted. Required after a filtered channel purge, which returns the subject
66389
+ * tip to 0 while the WAL still holds a non-zero `E`, permanently CAS-failing every later publish.
66390
+ */
66391
+ async abandon() {
66392
+ return this.serialize(async () => {
66393
+ await this.assertNotClobbering();
66394
+ if (!this.subject)
66395
+ throw new Error(`event WAL ${this.path}: no subject frontier is bound, so an abandonment here would clear this log and leave the principal's shared tip standing, which is the partial abandonment this method refuses to produce; bind the principal's record with bindSubjectFrontier first.`);
66396
+ await this.subject.reset();
66397
+ await this.write({
66398
+ ...this.doc,
66399
+ epoch: (0, import_node_crypto10.randomUUID)(),
66400
+ frontier: { seq: 0, lastSubjectSeq: 0, sourceCursor: void 0 },
66401
+ pending: null,
66402
+ // Abandonment is TOTAL, and the bracket machine is part of the total. A new epoch tells a
66403
+ // consumer the chain broke, so carrying the old chain's open runs into it would be a partial
66404
+ // abandonment — and partial abandonment is not a state.
66405
+ brackets: { run: void 0, text: [], reasoning: [], tools: [] }
66406
+ });
66407
+ });
66408
+ }
66409
+ /**
66410
+ * Durable replace: write a sibling temp file, fsync it, then rename over the target. The rename
66411
+ * is what makes a reader see either the whole old document or the whole new one and never a torn
66412
+ * prefix — which is precisely why a zero-byte WAL is treated as corruption rather than as virgin.
66413
+ *
66414
+ * **MODE 0600, AND THE REASON IS NOT TIDINESS: `pending.id` IS A PRE-PUBLICATION SECRET.**
66415
+ * The dedup cache the frozen id is checked against is STREAM-WIDE, so anyone who learns an id
66416
+ * BEFORE its frame is published can pre-seed it and make the real publish come back
66417
+ * `duplicate: true`. The design attributes the safety of that entirely to `randomUUID()` entropy —
66418
+ * which holds only while the id is unguessable AND unread. An id already on the wire is harmless
66419
+ * (that message has landed); the only window where it is dangerous is exactly the window this
66420
+ * file holds it in, between transition 1 and the ack.
66421
+ *
66422
+ * So the residual a reviewer raised as "gated on a local disk read rather than mesh access" is
66423
+ * gated on a read OF THIS FILE. A world-readable WAL would convert a property the design credits
66424
+ * to entropy into one credited to filesystem luck. Under our own rules the attack yields a LOUD
66425
+ * halt rather than silent loss — a duplicate ack on a retry fails loud with the frontier and
66426
+ * cursor unmoved — so this is denial of service, not corruption. Closing it by construction is
66427
+ * cheap enough that naming it as an accepted residual would be the worse trade.
66428
+ */
66429
+ async write(next) {
66430
+ await this.assertNotClobbering();
66431
+ const stamped = { ...next, gen: this.doc.gen + 1 };
66432
+ const tmp = (0, import_node_path3.join)((0, import_node_path3.dirname)(this.path), `.${(0, import_node_crypto10.createHash)("sha256").update(this.path).digest("hex").slice(0, 12)}.${process.pid}.${(0, import_node_crypto10.randomUUID)().slice(0, 8)}.wal.tmp`);
66433
+ const body = JSON.stringify(stamped);
66434
+ const fh = await openExclusiveNoFollow(tmp);
66435
+ try {
66436
+ await fh.writeFile(body, "utf8");
66437
+ await fh.sync();
66438
+ } finally {
66439
+ await fh.close();
66440
+ }
66441
+ try {
66442
+ await (0, import_promises2.rename)(tmp, this.path);
66443
+ } catch (e) {
66444
+ await (0, import_promises2.unlink)(tmp).catch(() => {
66445
+ });
66446
+ throw e;
66447
+ }
66448
+ this.doc = stamped;
66449
+ }
66450
+ /**
66451
+ * Refuse to replace a document this handle did not read.
66452
+ *
66453
+ * **THE FAILURE THIS EXISTS FOR WAS EXECUTED, NOT IMAGINED.** Two `EventWal` objects were opened
66454
+ * on one file. A ran the full cycle and folded a frontier of `{seq:1, lastSubjectSeq:5}`. B, whose
66455
+ * in-memory document was frozen back at the pending write, then called `recordAck(99)` and
66456
+ * `fold()` — both SUCCEEDED, each replacing the whole file, and the WAL came back up claiming a
66457
+ * durable tip of 99: a subject sequence the broker never assigned. The next publish freezes
66458
+ * `E := 99` against a stream whose real tip is 5, so the emitter either CAS-halts forever or
66459
+ * recovers a frontier that never existed. Nothing about that is loud; it reads as a healthy WAL.
66460
+ *
66461
+ * The per-instance `serialize` chain cannot see it (two instances, two chains) and neither can the
66462
+ * principal lock (B's handle predates any lock B would take, and a lock is not held against a
66463
+ * process's own second object). The guard has to be HERE, on the write, where the two views
66464
+ * finally meet.
66465
+ *
66466
+ * **This is a check, not a transaction, and the difference is stated rather than glossed.** The
66467
+ * read and the `rename` are separate syscalls, so a writer that lands in between is not caught by
66468
+ * this; what is caught is every stale handle — the case that actually occurs, because a stale
66469
+ * handle stays stale for as long as it exists rather than for a syscall's width. The lock is what
66470
+ * keeps a second live writer from starting; this is what keeps one that already exists from
66471
+ * winning.
66472
+ */
66473
+ async assertNotClobbering() {
66474
+ let bytes;
66475
+ try {
66476
+ bytes = await (0, import_promises2.readFile)(this.path);
66477
+ } catch (e) {
66478
+ if (e.code !== "ENOENT")
66479
+ throw e;
66480
+ }
66481
+ if (bytes === void 0) {
66482
+ if (this.doc.gen !== 0)
66483
+ throw new WalStaleWriterError(this.path, this.doc.gen, void 0);
66484
+ return;
66485
+ }
66486
+ let raw;
66487
+ try {
66488
+ raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
66489
+ } catch {
66490
+ throw new WalCorruptError(this.path, "the file is valid UTF-8", "invalid UTF-8 bytes on the file about to be replaced; refusing rather than substituting U+FFFD");
66491
+ }
66492
+ let d;
66493
+ try {
66494
+ d = JSON.parse(raw);
66495
+ } catch (e) {
66496
+ throw new WalCorruptError(this.path, "parseable JSON", e.message);
66497
+ }
66498
+ if (typeof d !== "object" || d === null)
66499
+ throw new WalCorruptError(this.path, "document is an object", typeof d);
66500
+ const onDisk = docGeneration(this.path, d);
66501
+ if (onDisk !== this.doc.gen)
66502
+ throw new WalStaleWriterError(this.path, this.doc.gen, onDisk);
66503
+ }
66504
+ };
66505
+
66506
+ // ../connector-core/dist/subject-frontier.js
66507
+ var import_node_crypto12 = require("node:crypto");
66508
+ var import_node_fs6 = require("node:fs");
66509
+ var import_promises4 = require("node:fs/promises");
66510
+ var import_node_path5 = require("node:path");
66511
+
66512
+ // ../connector-core/dist/agui-wal-path.js
66513
+ var import_node_crypto11 = require("node:crypto");
66514
+ var import_promises3 = require("node:fs/promises");
66515
+ var import_node_os3 = require("node:os");
66516
+ var import_node_path4 = require("node:path");
66517
+ var EventsStateRootMissing = class extends Error {
66518
+ constructor(message) {
66519
+ super(message);
66520
+ this.name = "EventsStateRootMissing";
66521
+ }
66522
+ };
66523
+ function resolveEventsStateRoot(env) {
66524
+ const root = env.COTAL_WORKSPACE_ROOT;
66525
+ if (typeof root !== "string" || root.trim() === "")
66526
+ throw new EventsStateRootMissing("events are enabled for this session but COTAL_WORKSPACE_ROOT is not set, so there is nowhere to put the event write-ahead log. The launcher forwards it from LaunchOpts.workspaceRoot; a session started outside a manager has no workspace root and must not publish events. Refusing rather than defaulting to the working directory, which would put the WAL somewhere no later start looks.");
66527
+ return root;
66528
+ }
66529
+ function h(value) {
66530
+ return (0, import_node_crypto11.createHash)("sha256").update(value).digest("hex").slice(0, 16);
66531
+ }
66532
+ function eventWalLocation(opts) {
66533
+ const principalDir = (0, import_node_path4.join)(opts.workspaceRoot, ".cotal", "events", h(opts.space), h(opts.principal));
66534
+ const threadDir = (0, import_node_path4.join)(principalDir, h(opts.threadId));
66535
+ return {
66536
+ principalDir,
66537
+ lockPath: (0, import_node_path4.join)(principalDir, ".lock"),
66538
+ subjectPath: (0, import_node_path4.join)(principalDir, "subject.json"),
66539
+ threadDir,
66540
+ walPath: (0, import_node_path4.join)(threadDir, "wal.json")
66541
+ };
66542
+ }
66543
+ var PrincipalLockError = class extends Error {
66544
+ path;
66545
+ invariant;
66546
+ constructor(path, invariant, detail) {
66547
+ super(`event WAL principal lock at ${path} (${invariant}): ${detail}`);
66548
+ this.path = path;
66549
+ this.invariant = invariant;
66550
+ this.name = "PrincipalLockError";
66551
+ }
66552
+ };
66553
+ var held = /* @__PURE__ */ new Map();
66554
+ function ownerIsAlive(pid) {
66555
+ try {
66556
+ process.kill(pid, 0);
66557
+ return true;
66558
+ } catch (e) {
66559
+ return e.code !== "ESRCH";
66560
+ }
66561
+ }
66562
+ async function createLockFile(path) {
66563
+ try {
66564
+ return await openExclusiveNoFollow(path);
66565
+ } catch (e) {
66566
+ if (e.code === "EEXIST")
66567
+ return void 0;
66568
+ throw e;
66569
+ }
66570
+ }
66571
+ async function reclaimIfOwnerIsGone(path) {
66572
+ let raw;
66573
+ try {
66574
+ raw = await (0, import_promises3.readFile)(path, "utf8");
66575
+ } catch (e) {
66576
+ if (e.code === "ENOENT")
66577
+ return;
66578
+ throw e;
66579
+ }
66580
+ let record2;
66581
+ try {
66582
+ record2 = JSON.parse(raw);
66583
+ } catch {
66584
+ throw new PrincipalLockError(path, "the lock names its owner", "the file is not readable JSON, so its owner cannot be checked; refusing rather than reclaiming a lock that may be held");
66585
+ }
66586
+ const r = record2;
66587
+ if (!Number.isSafeInteger(r.pid) || r.pid <= 0 || typeof r.host !== "string" || r.host.length === 0)
66588
+ throw new PrincipalLockError(path, "the lock names its owner", `the record carries pid=${String(r.pid)} host=${String(r.host)}, which names nobody checkable`);
66589
+ const here = (0, import_node_os3.hostname)();
66590
+ if (r.host !== here)
66591
+ throw new PrincipalLockError(path, "the recorded owner is on THIS host", `held by pid ${r.pid} on ${r.host} while this process runs on ${here}; liveness on another machine is not observable from here`);
66592
+ if (ownerIsAlive(r.pid))
66593
+ throw new PrincipalLockError(path, "the recorded owner is gone", `pid ${r.pid} on ${here} is still running and holds this principal's emitter`);
66594
+ await (0, import_promises3.unlink)(path).catch((e) => {
66595
+ if (e.code !== "ENOENT")
66596
+ throw e;
66597
+ });
66598
+ }
66599
+ async function acquirePrincipalLock(lockPath) {
66600
+ const already = held.get(lockPath);
66601
+ if (already)
66602
+ return already;
66603
+ let fh = await createLockFile(lockPath);
66604
+ if (fh === void 0) {
66605
+ await reclaimIfOwnerIsGone(lockPath);
66606
+ fh = await createLockFile(lockPath);
66607
+ if (fh === void 0)
66608
+ throw new PrincipalLockError(lockPath, "the reclaimed lock is free when this process takes it", "another process created the lock between the reclaim and this open, and now holds this principal");
66609
+ }
66610
+ const record2 = JSON.stringify({ pid: process.pid, host: (0, import_node_os3.hostname)(), token: (0, import_node_crypto11.randomUUID)(), acquiredAt: (/* @__PURE__ */ new Date()).toISOString() });
66611
+ try {
66612
+ await fh.writeFile(record2, "utf8");
66613
+ await fh.sync();
66614
+ } catch (e) {
66615
+ await fh.close().catch(() => {
66616
+ });
66617
+ await (0, import_promises3.unlink)(lockPath).catch(() => {
66618
+ });
66619
+ throw e;
66620
+ }
66621
+ const lock = {
66622
+ path: lockPath,
66623
+ async release() {
66624
+ if (held.get(lockPath) !== lock)
66625
+ return;
66626
+ held.delete(lockPath);
66627
+ await fh.close().catch(() => {
66628
+ });
66629
+ await (0, import_promises3.unlink)(lockPath).catch((e) => {
66630
+ if (e.code !== "ENOENT")
66631
+ throw e;
66632
+ });
66633
+ }
66634
+ };
66635
+ held.set(lockPath, lock);
66636
+ return lock;
66637
+ }
66638
+ async function fsyncDir(dir) {
66639
+ let fh;
66640
+ try {
66641
+ fh = await (0, import_promises3.open)(dir, "r");
66642
+ } catch (e) {
66643
+ const code = e.code;
66644
+ if (code === "EPERM" || code === "EACCES")
66645
+ return;
66646
+ throw e;
66647
+ }
66648
+ try {
66649
+ await fh.sync();
66650
+ } catch (e) {
66651
+ const code = e.code;
66652
+ if (code !== "EBADF" && code !== "EINVAL" && code !== "EPERM" && code !== "EISDIR")
66653
+ throw e;
66654
+ } finally {
66655
+ await fh.close();
66656
+ }
66657
+ }
66658
+ async function ensureEventWalDir(opts) {
66659
+ const loc = eventWalLocation(opts);
66660
+ ensureDirNoSymlink(opts.workspaceRoot, ".cotal", "events", h(opts.space), h(opts.principal), h(opts.threadId));
66661
+ for (let dir = loc.threadDir; ; dir = (0, import_node_path4.dirname)(dir)) {
66662
+ await fsyncDir(dir);
66663
+ if (dir === opts.workspaceRoot || (0, import_node_path4.dirname)(dir) === dir)
66664
+ break;
66665
+ }
66666
+ const lock = await acquirePrincipalLock(loc.lockPath);
66667
+ return { ...loc, lock };
66668
+ }
66669
+
66670
+ // ../connector-core/dist/subject-frontier.js
66671
+ var SUBJECT_FRONTIER_VERSION = 1;
66672
+ var SubjectFrontierCorruptError = class extends Error {
66673
+ path;
66674
+ invariant;
66675
+ constructor(path, invariant, detail) {
66676
+ super(`subject frontier ${path}: expected ${invariant} \u2014 ${detail}`);
66677
+ this.path = path;
66678
+ this.invariant = invariant;
66679
+ this.name = "SubjectFrontierCorruptError";
66680
+ }
66681
+ };
66682
+ var SubjectFrontierMovedError = class extends Error {
66683
+ path;
66684
+ viewTip;
66685
+ diskTip;
66686
+ constructor(path, viewTip, diskTip) {
66687
+ super(`subject frontier ${path}: the record moved under this writer (this view holds ${viewTip}, the file holds ${diskTip === void 0 ? "no record at all" : diskTip}). The tip is shared by every thread of the principal, so writing this view's number would take the record backwards to a sequence the broker has already passed, and every later publish would expect a tip the subject no longer has.`);
66688
+ this.path = path;
66689
+ this.viewTip = viewTip;
66690
+ this.diskTip = diskTip;
66691
+ this.name = "SubjectFrontierMovedError";
66692
+ }
66693
+ };
66694
+ var isSafeNonNegInt2 = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
66695
+ var FileSubjectFrontier = class _FileSubjectFrontier {
66696
+ path;
66697
+ doc;
66698
+ constructor(path, doc) {
66699
+ this.path = path;
66700
+ this.doc = doc;
66701
+ }
66702
+ get tip() {
66703
+ return this.doc.tip;
66704
+ }
66705
+ /**
66706
+ * Open, or create a virgin record.
66707
+ *
66708
+ * A MISSING file is virgin and legal: this principal has never published, which is the ordinary
66709
+ * state on a first run and after a fresh install. A ZERO-BYTE file is NOT, for the same reason
66710
+ * the write-ahead log refuses one: an atomic temp-and-rename never produces it, so it is a
66711
+ * filesystem that lost the tail, and reading it as "never published" is the guess this whole
66712
+ * mechanism exists to remove.
66713
+ */
66714
+ static async open(path, opts) {
66715
+ let bytes;
66716
+ try {
66717
+ bytes = await (0, import_promises4.readFile)(path);
66718
+ } catch (e) {
66719
+ if (e.code !== "ENOENT")
66720
+ throw e;
66721
+ }
66722
+ if (bytes === void 0) {
66723
+ const recovered = await _FileSubjectFrontier.recoverTipFromThreadLogs((0, import_node_path5.dirname)(path), opts.principal);
66724
+ const fresh = new _FileSubjectFrontier(path, { v: SUBJECT_FRONTIER_VERSION, space: opts.space, principal: opts.principal, tip: 0 });
66725
+ if (recovered > 0)
66726
+ await fresh.write({ ...fresh.doc, tip: recovered });
66727
+ return fresh;
66728
+ }
66729
+ return new _FileSubjectFrontier(path, _FileSubjectFrontier.parse(path, bytes, opts));
66730
+ }
66731
+ /**
66732
+ * Bytes to a validated document, or a refusal.
66733
+ *
66734
+ * SHARED BY `open` AND BY THE RE-READ IN {@link advance} on purpose. A record that went corrupt
66735
+ * underneath a live writer has to meet the same wall as one that was corrupt at boot; validating
66736
+ * only on the way in would let a writer that opened a good file overwrite a bad one, which
66737
+ * destroys the evidence of whatever produced it.
66738
+ */
66739
+ static parse(path, bytes, opts) {
66740
+ let raw;
66741
+ try {
66742
+ raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
66743
+ } catch {
66744
+ throw new SubjectFrontierCorruptError(path, "valid UTF-8", "invalid UTF-8 bytes; refusing rather than substituting U+FFFD");
66745
+ }
66746
+ if (raw.length === 0)
66747
+ throw new SubjectFrontierCorruptError(path, "a non-empty file", "the file is zero bytes \u2014 distinct from missing, and never treated as virgin");
66748
+ let parsed;
66749
+ try {
66750
+ parsed = JSON.parse(raw);
66751
+ } catch (e) {
66752
+ throw new SubjectFrontierCorruptError(path, "parseable JSON", e.message);
66753
+ }
66754
+ const d = parsed;
66755
+ if (d?.v !== SUBJECT_FRONTIER_VERSION)
66756
+ throw new SubjectFrontierCorruptError(path, `v === ${SUBJECT_FRONTIER_VERSION}`, String(d?.v));
66757
+ if (d.space !== opts.space)
66758
+ throw new SubjectFrontierCorruptError(path, "space matches", `file=${String(d.space)} caller=${opts.space}`);
66759
+ if (d.principal !== opts.principal)
66760
+ throw new SubjectFrontierCorruptError(path, "principal matches", `file=${String(d.principal)} caller=${opts.principal}`);
66761
+ if (!isSafeNonNegInt2(d.tip))
66762
+ throw new SubjectFrontierCorruptError(path, "tip is a safe non-negative integer", String(d.tip));
66763
+ return { v: d.v, space: d.space, principal: d.principal, tip: d.tip };
66764
+ }
66765
+ async advance(seq) {
66766
+ return this.serialize(async () => {
66767
+ if (!isSafeNonNegInt2(seq))
66768
+ throw new Error(`subject frontier ${this.path}: seq must be a safe non-negative integer, got ${String(seq)}`);
66769
+ if (seq <= this.doc.tip)
66770
+ throw new Error(`subject frontier ${this.path}: seq=${seq} does not advance the tip ${this.doc.tip}`);
66771
+ const disk = await this.readDiskTip();
66772
+ if (disk === void 0 ? this.doc.tip !== 0 : disk !== this.doc.tip)
66773
+ throw new SubjectFrontierMovedError(this.path, this.doc.tip, disk);
66774
+ await this.write({ ...this.doc, tip: seq });
66775
+ });
66776
+ }
66777
+ /**
66778
+ * The tip the FILE holds, or `undefined` when no record exists yet.
66779
+ *
66780
+ * Fully validated, not a bare `JSON.parse().tip`: the disagreement this feeds is decided on a
66781
+ * number, and a number taken from a document that failed its own shape checks is not evidence.
66782
+ */
66783
+ async readDiskTip() {
66784
+ let bytes;
66785
+ try {
66786
+ bytes = await (0, import_promises4.readFile)(this.path);
66787
+ } catch (e) {
66788
+ if (e.code === "ENOENT")
66789
+ return void 0;
66790
+ throw e;
66791
+ }
66792
+ return _FileSubjectFrontier.parse(this.path, bytes, { space: this.doc.space, principal: this.doc.principal }).tip;
66793
+ }
66794
+ /**
66795
+ * One mutation at a time on THIS instance.
66796
+ *
66797
+ * The re-read above is a read-modify-write, so two callers that interleave between the read and
66798
+ * the rename would both pass a check neither still satisfies. One frontier is legitimately bound
66799
+ * to SEVERAL logs (the pinning runs the other way: a log may not change which record it
66800
+ * publishes onto), so concurrent callers on one instance are an ordinary state, not a misuse.
66801
+ *
66802
+ * It serializes this instance and nothing else. Two instances have two chains, which is the case
66803
+ * the re-read exists for.
66804
+ */
66805
+ chain = Promise.resolve();
66806
+ serialize(op) {
66807
+ const next = this.chain.then(op, op);
66808
+ this.chain = next.catch(() => void 0);
66809
+ return next;
66810
+ }
66811
+ /**
66812
+ * Recover the tip from the THREAD LOGS beside this record, for an installation upgrading from a
66813
+ * release where this record did not exist.
66814
+ *
66815
+ * **THIS IS THE WHOLE UPGRADE PATH AND LEAVING IT OUT MAKES THE FIX APPLY TO NOBODY WHO ALREADY
66816
+ * RAN THE BROKEN VERSION.** My first attempt seeded from the log of the thread being opened, which
66817
+ * is empty in the case that matters: upgrading restarts the seat, so the first session after the
66818
+ * upgrade is a NEW thread with a virgin log, while the sequence it needs sits in the PREVIOUS
66819
+ * thread's log. A cell in `smoke:agui-multi-session` failed on exactly that and is the reason this
66820
+ * function exists rather than the reasoning that produced the first version.
66821
+ *
66822
+ * **ONLY WHEN THE RECORD IS ABSENT, NEVER WHEN IT READS ZERO.** A record holding zero is what
66823
+ * abandonment writes after a filtered purge, and re-seeding it from a thread log would silently
66824
+ * undo the abandonment and restore an expectation the subject no longer has. Missing and zero are
66825
+ * different states and this is the second place in this plane where conflating them is the bug.
66826
+ *
66827
+ * A sibling that cannot be read or does not parse is FATAL rather than skipped. Skipping it
66828
+ * under-counts the tip, which produces a permanent halt later with a message about a moved tip,
66829
+ * pointing at everything except the file that was quietly ignored here.
66830
+ */
66831
+ static async recoverTipFromThreadLogs(principalDir, principal) {
66832
+ let entries;
66833
+ try {
66834
+ entries = await (0, import_promises4.readdir)(principalDir, { withFileTypes: true });
66835
+ } catch (e) {
66836
+ if (e.code === "ENOENT")
66837
+ return 0;
66838
+ throw e;
66839
+ }
66840
+ let best = 0;
66841
+ for (const ent of entries) {
66842
+ if (ent.isSymbolicLink())
66843
+ throw new SubjectFrontierCorruptError((0, import_node_path5.join)(principalDir, ent.name), "a real directory beside the record, never a symlink", "following it would carry this scan outside the principal directory, and the writer that creates these directories refuses a symlinked component for the same reason");
66844
+ if (!ent.isDirectory())
66845
+ continue;
66846
+ const walPath = (0, import_node_path5.join)(principalDir, ent.name, "wal.json");
66847
+ let st;
66848
+ try {
66849
+ st = await (0, import_promises4.lstat)(walPath);
66850
+ } catch (e) {
66851
+ const code = e.code;
66852
+ if (code === "ENOENT" || code === "ENOTDIR")
66853
+ continue;
66854
+ throw e;
66855
+ }
66856
+ if (st.isSymbolicLink())
66857
+ throw new SubjectFrontierCorruptError(walPath, "a real thread log, never a symlink", "following it would read a log this principal's writer never wrote");
66858
+ if (st.nlink > 1)
66859
+ throw new SubjectFrontierCorruptError(walPath, "a thread log with exactly one name", `it has ${st.nlink}, so the same file is reachable from outside this principal's directory and its tip is not this principal's to read`);
66860
+ let raw;
66861
+ try {
66862
+ const fh = await (0, import_promises4.open)(walPath, import_node_fs6.constants.O_RDONLY | (import_node_fs6.constants.O_NOFOLLOW ?? 0));
66863
+ try {
66864
+ raw = await fh.readFile();
66865
+ } finally {
66866
+ await fh.close();
66867
+ }
66868
+ } catch (e) {
66869
+ const code = e.code;
66870
+ if (code === "ENOENT" || code === "ENOTDIR")
66871
+ continue;
66872
+ if (code === "ELOOP")
66873
+ throw new SubjectFrontierCorruptError(walPath, "a real thread log, never a symlink", "the file became a symlink between the check and the open");
66874
+ throw e;
66875
+ }
66876
+ let doc;
66877
+ try {
66878
+ doc = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(raw));
66879
+ } catch (e) {
66880
+ throw new SubjectFrontierCorruptError(walPath, "a readable thread log while recovering the subject tip", e.message);
66881
+ }
66882
+ if (doc.principal !== principal)
66883
+ throw new SubjectFrontierCorruptError(walPath, `a thread log for principal ${principal}`, `found ${String(doc.principal)}`);
66884
+ const seq = doc.frontier?.lastSubjectSeq;
66885
+ if (!isSafeNonNegInt2(seq))
66886
+ throw new SubjectFrontierCorruptError(walPath, "frontier.lastSubjectSeq is a safe non-negative integer", String(seq));
66887
+ if (seq > best)
66888
+ best = seq;
66889
+ const pending = doc.pending;
66890
+ if (pending && pending.state === "acked") {
66891
+ const acked = pending.ackSeq;
66892
+ if (!isSafeNonNegInt2(acked))
66893
+ throw new SubjectFrontierCorruptError(walPath, "an acked pending carries a safe non-negative ackSeq", String(acked));
66894
+ if (!(acked > seq))
66895
+ throw new SubjectFrontierCorruptError(walPath, "an acked pending is ahead of the frontier it will fold into", `ackSeq=${acked} frontier.lastSubjectSeq=${seq}`);
66896
+ if (acked > best)
66897
+ best = acked;
66898
+ }
66899
+ }
66900
+ return best;
66901
+ }
66902
+ // `seedFromThread` used to live here, and it is GONE rather than kept for a caller that might
66903
+ // want it. Recovery moved into `open`, which is the only place that can see every sibling log,
66904
+ // and what was left behind was a public method that writes a tip into a record whose only
66905
+ // precondition is that the record reads 0. A record reading 0 is exactly what abandonment writes
66906
+ // after a channel purge, so the leftover was a supported route back into the state this file
66907
+ // exists to prevent, with no shipped caller to justify it.
66908
+ async reset() {
66909
+ return this.serialize(async () => {
66910
+ await this.write({ ...this.doc, tip: 0 });
66911
+ });
66912
+ }
66913
+ /** Atomic replace: sibling temp, fsync, rename, fsync the directory. */
66914
+ async write(next) {
66915
+ const tmp = `${this.path}.${(0, import_node_crypto12.randomUUID)()}.tmp`;
66916
+ const fh = await (0, import_promises4.open)(tmp, import_node_fs6.constants.O_WRONLY | import_node_fs6.constants.O_CREAT | import_node_fs6.constants.O_EXCL, 384);
66917
+ try {
66918
+ await fh.writeFile(JSON.stringify(next));
66919
+ await fh.sync();
66920
+ } finally {
66921
+ await fh.close();
66922
+ }
66923
+ try {
66924
+ await (0, import_promises4.rename)(tmp, this.path);
66925
+ } catch (e) {
66926
+ await (0, import_promises4.unlink)(tmp).catch(() => {
66927
+ });
66928
+ throw e;
66929
+ }
66930
+ await fsyncDir((0, import_node_path5.dirname)(this.path));
66931
+ this.doc = next;
66932
+ }
66933
+ };
66934
+
66935
+ // ../connector-core/dist/agui-holder.js
66936
+ var AguiEmitterHolder = class {
66937
+ startEmitter;
66938
+ onError;
66939
+ onRunClosed;
66940
+ emitter;
66941
+ /** The path this holder is BOUND to. Set before `starting`, so a second path is refused even
66942
+ * while the first start is still in flight. */
66943
+ boundPath;
66944
+ /**
66945
+ * Terminal. Once set, this holder never starts, pumps, or reports success again.
66946
+ *
66947
+ * It does not retry, and that is a decision rather than an omission. A retry on the next hook
66948
+ * would re-run that preflight and a WAL recovery against a stream this holder has already
66949
+ * failed to establish itself on, on a timer set by how often the user happens to type. The
66950
+ * emitter's own answer to an uncertain publish is to halt rather than to limp, and a holder that
66951
+ * quietly reconnected underneath it would reintroduce, one layer up, exactly the silence the
66952
+ * emitter refuses.
66953
+ */
66954
+ dead;
66955
+ /** ALL mutation runs on this chain. Hook events arrive concurrently on the control socket, so
66956
+ * without it two flushes could read the source at the same cursor. */
66957
+ chain = Promise.resolve();
66958
+ /**
66959
+ * @param startEmitter Builds and starts the emitter for an adopted path. Injected rather than
66960
+ * assembled here: the WAL location, the source, and the record mapper are all connector
66961
+ * decisions, and a holder that made them would be a second place they are decided.
66962
+ * @param onError Where a failure goes. Required, and not defaulted to a swallow: this class runs
66963
+ * behind a hook that must not throw, so the only way a failure reaches a human is if the caller
66964
+ * is made to say where it goes.
66965
+ * @param onRunClosed Told which run {@link closeRun} closed, so the connector's own mapper can
66966
+ * forget the run it will no longer attribute records to. Optional, and it is NOT the holder
66967
+ * doing the forgetting: which state a record mapper keeps is the connector's business, and a
66968
+ * holder that reached into it would be a second place that decides what a run is. Without it a
66969
+ * mapper that still believes the run is open would emit under a `runId` the published stream has
66970
+ * already closed, and the emitter would refuse the batch.
66971
+ */
66972
+ constructor(startEmitter, onError, onRunClosed) {
66973
+ this.startEmitter = startEmitter;
66974
+ this.onError = onError;
66975
+ this.onRunClosed = onRunClosed;
66976
+ }
66977
+ /** True once an emitter is running here. False while a start is still in flight — it reports what
66978
+ * IS, never what is about to be. */
66979
+ get running() {
66980
+ return this.emitter !== void 0 && !this.emitter.stopped;
66981
+ }
66982
+ /** The failure that killed this holder, if one did. */
66983
+ get failure() {
66984
+ return this.dead;
66985
+ }
66986
+ /** The path this holder bound to on first adopt, if it has adopted. */
66987
+ get path() {
66988
+ return this.boundPath;
66989
+ }
66990
+ /**
66991
+ * Adopt a transcript path, starting the emitter if this is the first one.
66992
+ *
66993
+ * Synchronous and non-throwing by contract, because a hook calls it. The work lands on the chain.
66994
+ */
66995
+ adopt(path) {
66996
+ this.enqueue(() => this.ensureStarted(path).then(() => void 0));
66997
+ }
66998
+ /**
66999
+ * Adopt if necessary, then drain the source into frames.
67000
+ *
67001
+ * Same contract as {@link adopt}: synchronous, non-throwing, work on the chain.
67002
+ */
67003
+ flush(path) {
67004
+ this.enqueue(async () => {
67005
+ const emitter = await this.ensureStarted(path);
67006
+ if (!emitter || emitter.stopped)
67007
+ return;
67008
+ await emitter.pump();
67009
+ });
67010
+ }
67011
+ /**
67012
+ * Start at most once, bind the path once.
67013
+ *
67014
+ * Returns `undefined` when there is nothing to run against — a dead holder or a path this holder
67015
+ * cannot take — rather than throwing, so a caller cannot mistake "no emitter" for "pumped".
67016
+ */
67017
+ /**
67018
+ * Close the open run at a turn boundary the record stream cannot see.
67019
+ *
67020
+ * Same contract as {@link adopt} and {@link flush}: synchronous, non-throwing, work on the chain,
67021
+ * because a lifecycle hook calls it and a hook must not be made to wait or to fail.
67022
+ *
67023
+ * It deliberately does NOT start an emitter. A session that never adopted a transcript has nothing
67024
+ * open and nothing to close, and starting one here would reach the broker on the way OUT of a
67025
+ * turn that published nothing.
67026
+ */
67027
+ closeRun(timestamp) {
67028
+ this.enqueue(async () => {
67029
+ const emitter = this.emitter;
67030
+ if (this.dead || !emitter || emitter.stopped)
67031
+ return;
67032
+ const runId = await emitter.closeRun({ timestamp });
67033
+ if (runId !== null)
67034
+ this.onRunClosed?.(runId);
67035
+ });
67036
+ }
67037
+ async ensureStarted(path) {
67038
+ if (this.dead)
67039
+ return void 0;
67040
+ if (typeof path !== "string" || path.length === 0)
67041
+ return void 0;
67042
+ if (this.boundPath !== void 0 && this.boundPath !== path) {
67043
+ this.die(new Error(`AG-UI emitter is bound to transcript ${this.boundPath}; refusing to re-adopt ${path} \u2014 a second session needs its own emitter and its own write-ahead log`));
67044
+ return void 0;
67045
+ }
67046
+ if (this.emitter)
67047
+ return this.emitter;
67048
+ if (this.boundPath === void 0) {
67049
+ this.boundPath = path;
67050
+ try {
67051
+ this.emitter = await this.startEmitter(path);
67052
+ return this.emitter;
67053
+ } catch (e) {
67054
+ this.die(e);
67055
+ return void 0;
67056
+ }
67057
+ }
67058
+ return this.emitter;
67059
+ }
67060
+ die(e) {
67061
+ if (this.dead)
67062
+ return;
67063
+ this.dead = e;
67064
+ this.onError(e);
67065
+ }
67066
+ enqueue(step) {
67067
+ this.chain = this.chain.then(step).catch((e) => this.die(e));
67068
+ }
67069
+ /** Await the queued work. For callers that need a settled point — a shutdown, or a cell. */
67070
+ async settled() {
67071
+ await this.chain;
67072
+ }
67073
+ };
65236
67074
 
65237
67075
  // ../connector-core/dist/agui-render.js
65238
67076
  var str = (v) => typeof v === "string" ? v : void 0;
@@ -65242,7 +67080,7 @@ var THINK_PREFIX = "(thinking) ";
65242
67080
  var TOOL_PREFIX = "\u2699 ";
65243
67081
  var RESULT_PREFIX = " \u21B3 ";
65244
67082
  var BLOCK_START_CHARS = /* @__PURE__ */ new Set([..."#>|`~=_*+-[<0123456789", " "]);
65245
- function renderEvents(events) {
67083
+ function renderEvents(events2) {
65246
67084
  const lines = [];
65247
67085
  const text = /* @__PURE__ */ new Map();
65248
67086
  const reasoning = /* @__PURE__ */ new Map();
@@ -65260,7 +67098,7 @@ function renderEvents(events) {
65260
67098
  emit(first, cont, acc + suffix);
65261
67099
  map2.delete(id);
65262
67100
  };
65263
- for (const e of events) {
67101
+ for (const e of events2) {
65264
67102
  const type = typeof e === "object" && e !== null ? str(e.type) : void 0;
65265
67103
  switch (type) {
65266
67104
  case AGUI_EVENT_TYPE.RUN_STARTED:
@@ -65343,11 +67181,11 @@ var aguiFramePartRenderer = Object.freeze({
65343
67181
  render(part) {
65344
67182
  if (!isAguiFramePart(part))
65345
67183
  return `[not an AG-UI frame]`;
65346
- const events = part.events;
65347
- if (!Array.isArray(events) || events.length === 0)
67184
+ const events2 = part.events;
67185
+ if (!Array.isArray(events2) || events2.length === 0)
65348
67186
  return `[AG-UI frame carrying no events]`;
65349
- const lines = renderEvents(events);
65350
- return lines.length > 0 ? lines.join("\n") : `[AG-UI frame with ${events.length} event(s) and nothing to show]`;
67187
+ const lines = renderEvents(events2);
67188
+ return lines.length > 0 ? lines.join("\n") : `[AG-UI frame with ${events2.length} event(s) and nothing to show]`;
65351
67189
  }
65352
67190
  });
65353
67191
  function registerAguiFramePartRenderer() {
@@ -65362,7 +67200,7 @@ var import_node_child_process2 = require("node:child_process");
65362
67200
 
65363
67201
  // ../connector-core/dist/docs-bundle.generated.js
65364
67202
  var DOCS_BUNDLE = {
65365
- "version": "0.21.0",
67203
+ "version": "0.23.0",
65366
67204
  "generatedFrom": "docs/*.md + SPEC.md + spec/cotal.schema.json",
65367
67205
  "pages": [
65368
67206
  {
@@ -65419,42 +67257,42 @@ var DOCS_BUNDLE = {
65419
67257
  "title": "Authoring a connector",
65420
67258
  "kind": "Reference: describes the TypeScript reference implementation, not the wire contract.",
65421
67259
  "summary": "A connector teaches Cotal how to launch one agent harness (Claude Code, OpenCode, your own) as a mesh node.",
65422
- "body": '# Authoring a connector\n\n> **Reference**: describes the TypeScript reference implementation, not the wire contract. \xB7 **For:** integrators adding a new agent harness \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\nA **connector** teaches Cotal how to launch one agent harness (Claude Code, OpenCode, your own) as a\nmesh node. Connectors are ordinary [extensions](cli.md#ext): you publish an npm package, the operator\nruns `cotal ext add <your-package>`, and it plugs in exactly like the first-party connectors,\nwhich are themselves just connectors seeded on first run. There is no special-casing for built-ins,\nso anything the built-ins can do, yours can too.\n\n## The contract\n\nImplement `Connector` from `@cotal-ai/core` and self-register it on import:\n\n```ts\nimport { registry, type Connector } from "@cotal-ai/core";\n\nconst myConnector: Connector = {\n kind: "connector",\n name: "myagent", // the --agent value; must be unique, never "cotal"\n requires: ["myagent"], // external CLIs the launch needs on PATH (preflighted)\n buildLaunch(opts) { // opts \u2192 the process + env that joins the mesh\n return {\n command: "myagent",\n args: ["--serve"],\n env: { /* COTAL_* wiring from opts */ },\n };\n },\n // optional: listModels, supportsModelVariant, supportsResume, transcriptChannel,\n // eventChannel, pluginRoot\n};\n\nregistry.register(myConnector); // runs on import \u2014 that\'s what makes it "plug in"\n```\n\n`buildLaunch(opts)` is the whole job: given a `LaunchOpts` (space, name, role, creds, channels,\nmodel, prompt\u2026), return a `LaunchSpec` (the command, args, and environment) whose process connects to\nthe broker as that mesh node. Everything else on the interface is optional and default-deny: declare\n`supportsModelVariant`/`supportsResume` only if you honor them (a request for one you don\'t declare\nfails loud before any provisioning), list `requires` so a missing CLI fails with a clear message, and\nimplement `listModels` only if you want a selector catalog. Implement `eventChannel` only if your\nsession publishes a structured event plane: it names the channel the manager grants that session\npublish rights on, so the grant and the subject the session publishes to come from one function\nrather than two that can drift, and `--events` refuses a connector that does not implement it. See\nthe `Connector` interface in\n[`packages/core/src/connector.ts`](../packages/core/src/connector.ts) and the OpenCode connector in\n[`extensions/connector-opencode/`](../extensions/connector-opencode/) for a complete worked example.\n\n## Packaging rules (enforced at `ext add`)\n\n`cotal ext add` verifies these and fails loud otherwise, because they are what keep every extension\nsharing the binary\'s single `@cotal-ai/core` registry instance:\n\n- **`@cotal-ai/core` is a `peerDependency`, never a regular dependency.** A regular dep vendors a\n second copy of core, whose separate registry would swallow your `registry.register` call \u2014 the add\n would import your package cleanly but see zero contributions and refuse it. Any other `@cotal-ai/*`\n you use is a peer too. `ext add` junction-links each `@cotal-ai/*` peer to the binary\'s own copy;\n lazy materialization verifies and rebinds those links for the registry-facing entry\'s initial import,\n so global installs and source worktrees can share the machine extension prefix. Import every host peer\n in that initial graph; launcher/child artifacts that run later must bundle their dependencies rather\n than resolving a mutable host-peer link after another Cotal process may have rebound it.\n- **Bundle core as external.** If you bundle (esbuild/rollup), mark `@cotal-ai/core` (and any other\n `@cotal-ai/*`) `--external` so the runtime `import` resolves the host\'s copy, not an inlined one.\n- **Importing the package must self-register.** Your entry (`main`/`exports`) must run\n `registry.register(...)` as a side effect of import (e.g. `export * from "./extension.js"`), so the\n lazy materialize path can bring you online without a bespoke hook.\n- **Name yourself.** The connector `name` is the `--agent` value; it must be unique across installed\n extensions and must not be the reserved name `cotal`.\n\nA minimal `package.json`:\n\n```jsonc\n{\n "name": "@you/cotal-connector-myagent",\n "type": "module",\n "main": "./dist/index.js",\n "files": ["dist"], // whatever `ext add` needs to install + import\n "peerDependencies": { "@cotal-ai/core": ">=0.1.0" }\n}\n```\n\n## Install, use, remove\n\n```bash\ncotal ext add @you/cotal-connector-myagent # installs + verifies + caches its contribution\ncotal spawn --agent myagent # or `agent: myagent` in a manifest\ncotal ext remove @you/cotal-connector-myagent # gone; nothing static-imported it\n```\n\nSet `COTAL_DEFAULT_AGENT=myagent` to make it the default for a bare `cotal spawn`. Your connector\nresolves through the same lazy-materialize path as the built-ins (in the CLI\'s launch preflight and in\nthe manager), so a live `cotal up` will seed nothing extra: it imports your package, reads `requires`,\nand launches. For runtimes (how a node is hosted: pty/tmux/\u2026) rather than harnesses, the same\nextension model applies via the `Runtime` contract; see [define a team](define-a-team.md) and\n[the CLI reference](cli.md).\n'
67260
+ "body": '# Authoring a connector\n\n> **Reference**: describes the TypeScript reference implementation, not the wire contract. \xB7 **For:** integrators adding a new agent harness \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\nA **connector** teaches Cotal how to launch one agent harness (Claude Code, OpenCode, your own) as a\nmesh node. Connectors are ordinary [extensions](cli.md#ext): you publish an npm package, the operator\nruns `cotal ext add <your-package>`, and it plugs in exactly like the first-party connectors,\nwhich are themselves just connectors seeded on first run. There is no special-casing for built-ins,\nso anything the built-ins can do, yours can too.\n\n## The contract\n\nImplement `Connector` from `@cotal-ai/core` and self-register it on import:\n\n```ts\nimport { registry, type Connector } from "@cotal-ai/core";\n\nconst myConnector: Connector = {\n kind: "connector",\n name: "myagent", // the --agent value; must be unique, never "cotal"\n requires: ["myagent"], // external CLIs the launch needs on PATH (preflighted)\n buildLaunch(opts) { // opts \u2192 the process + env that joins the mesh\n return {\n command: "myagent",\n args: ["--serve"],\n env: { /* COTAL_* wiring from opts */ },\n };\n },\n // optional: listModels, supportsModelVariant, supportsResume, eventChannel, pluginRoot\n};\n\nregistry.register(myConnector); // runs on import \u2014 that\'s what makes it "plug in"\n```\n\n`buildLaunch(opts)` is the whole job: given a `LaunchOpts` (space, name, role, creds, channels,\nmodel, prompt\u2026), return a `LaunchSpec` (the command, args, and environment) whose process connects to\nthe broker as that mesh node. Everything else on the interface is optional and default-deny: declare\n`supportsModelVariant`/`supportsResume` only if you honor them (a request for one you don\'t declare\nfails loud before any provisioning), list `requires` so a missing CLI fails with a clear message, and\nimplement `listModels` only if you want a selector catalog. Implement `eventChannel` only if your\nsession publishes a structured event plane: it names the channel the manager grants that session\npublish rights on, so the grant and the subject the session publishes to come from one function\nrather than two that can drift, and `--events` refuses a connector that does not implement it. See\nthe `Connector` interface in\n[`packages/core/src/connector.ts`](../packages/core/src/connector.ts) and the OpenCode connector in\n[`extensions/connector-opencode/`](../extensions/connector-opencode/) for a complete worked example.\n\n## Packaging rules (enforced at `ext add`)\n\n`cotal ext add` verifies these and fails loud otherwise, because they are what keep every extension\nsharing the binary\'s single `@cotal-ai/core` registry instance:\n\n- **`@cotal-ai/core` is a `peerDependency`, never a regular dependency.** A regular dep vendors a\n second copy of core, whose separate registry would swallow your `registry.register` call \u2014 the add\n would import your package cleanly but see zero contributions and refuse it. Any other `@cotal-ai/*`\n you use is a peer too. `ext add` junction-links each `@cotal-ai/*` peer to the binary\'s own copy;\n lazy materialization verifies and rebinds those links for the registry-facing entry\'s initial import,\n so global installs and source worktrees can share the machine extension prefix. Import every host peer\n in that initial graph; launcher/child artifacts that run later must bundle their dependencies rather\n than resolving a mutable host-peer link after another Cotal process may have rebound it.\n- **Bundle core as external.** If you bundle (esbuild/rollup), mark `@cotal-ai/core` (and any other\n `@cotal-ai/*`) `--external` so the runtime `import` resolves the host\'s copy, not an inlined one.\n- **Importing the package must self-register.** Your entry (`main`/`exports`) must run\n `registry.register(...)` as a side effect of import (e.g. `export * from "./extension.js"`), so the\n lazy materialize path can bring you online without a bespoke hook.\n- **Name yourself.** The connector `name` is the `--agent` value; it must be unique across installed\n extensions and must not be the reserved name `cotal`.\n\nA minimal `package.json`:\n\n```jsonc\n{\n "name": "@you/cotal-connector-myagent",\n "type": "module",\n "main": "./dist/index.js",\n "files": ["dist"], // whatever `ext add` needs to install + import\n "peerDependencies": { "@cotal-ai/core": ">=0.1.0" }\n}\n```\n\n## Install, use, remove\n\n```bash\ncotal ext add @you/cotal-connector-myagent # installs + verifies + caches its contribution\ncotal spawn --agent myagent # or `agent: myagent` in a manifest\ncotal ext remove @you/cotal-connector-myagent # gone; nothing static-imported it\n```\n\nSet `COTAL_DEFAULT_AGENT=myagent` to make it the default for a bare `cotal spawn`. Your connector\nresolves through the same lazy-materialize path as the built-ins (in the CLI\'s launch preflight and in\nthe manager), so a live `cotal up` will seed nothing extra: it imports your package, reads `requires`,\nand launches. For runtimes (how a node is hosted: pty/tmux/\u2026) rather than harnesses, the same\nextension model applies via the `Runtime` contract; see [define a team](define-a-team.md) and\n[the CLI reference](cli.md).\n'
65423
67261
  },
65424
67262
  {
65425
67263
  "slug": "build-a-client",
65426
67264
  "title": "Build a Cotal client",
65427
67265
  "kind": "Guide (informative)",
65428
67266
  "summary": "This page is the reading order for implementing a Cotal client in another language (Go, Python, Rust, or anything with a NATS client library) against the spec, without reimplementing the protocol.",
65429
- "body": "# Build a Cotal client\n\n> **Guide** (informative) \xB7 **For:** spec implementers \xB7 **Normative:** [SPEC](../SPEC.md). Where this guide and the spec disagree, the spec wins.\n\nThis page is the reading order for implementing a Cotal client in another language (Go,\nPython, Rust, or anything with a NATS client library) against the spec, without\nreimplementing the protocol.\n\n## What you are implementing\n\nCotal is two layers, and a client sits astride both:\n\n- **The transport-agnostic contract** ([SPEC \xA73](../SPEC.md#3-subject-layout) through\n [\xA77](../SPEC.md#7-channels)): the subject layout, delivery modes, envelopes, presence, and\n channels. This is the standard; it does not mention NATS.\n- **The NATS + JetStream binding** ([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding) through\n [\xA710](../SPEC.md#10-connection-and-onboarding)): how those abstractions map onto streams,\n durables, KV, subject-scoped auth, and the join link. It is the only binding defined today.\n\nA client is a **thin layer over a NATS client library**: the library owns the connection,\nJetStream, and KV; your code owns subject construction and parsing, envelope validation, the\nreceive-side authenticity checks, and the presence/channel loops. See\n[transport.md](transport.md) for the split and the capabilities a binding must provide.\n\n## Prerequisites\n\n- A **NATS client library with JetStream + KV support** in your language (the official\n `nats.go`, `nats.py`, `async-nats` for Rust, etc.).\n- A **local mesh to test against**. From this repo:\n\n ```bash\n cotal up # broker + auth + control plane on 127.0.0.1:4222\n cotal mint <name> --profile agent # write an agent creds file to join with\n ```\n\n `cotal mint <name> --profile <agent|observer|admin>` also takes `--allow-subscribe a,b`\n and `--allow-publish a,b` to scope the read/post ACLs, and `--out <path>`. The creds file\n binds your principal (`owner.actor`, [SPEC \xA72](../SPEC.md#2-identity)) and your channel\n grants; see\n [identity-and-auth.md](identity-and-auth.md) and [run-a-mesh.md](run-a-mesh.md).\n If your client will **receive** DMs or role anycasts (step 6), mint with `--provision`\n (`--role <role>` for the anycast queue): the DM/task consumers are pre-created and\n bind-only, and the command prints the lifecycle uid your client binds them under.\n\n## Build order\n\nEach step names what to build, the section that governs it, and how to watch it work against a\nlocal mesh. The [SPEC \xA712](../SPEC.md#12-conformance) conformance list is the checklist these\nmap to.\n\n1. **Identity + connection**: [SPEC \xA72](../SPEC.md#2-identity),\n [\xA710](../SPEC.md#10-connection-and-onboarding),\n [\xA713.12](../SPEC.md#1312-nats--jetstream-binding). Read the server version from the\n **pre-auth INFO** and **fail loud below nats-server 2.12** (the v0.4 control surface relies\n on 2.12 schedule/CAS semantics); treat a repeated pre-auth drop as a possible\n oversized-CONNECT diagnostic, not an infinite retry loop. Then connect with the minted creds\n and adopt the principal bound to the credential; set the inbox prefix to your connection's\n reply inbox (`_INBOX_<connId>`) before any request, pull, or KV watch. *See it:* a wrong or missing cred is refused at connect, so a clean connect\n confirms identity and creds are wired correctly.\n\n2. **Subject construction + parsing**: [SPEC \xA73](../SPEC.md#3-subject-layout). Build the three\n messaging subject shapes plus the v0.4 endpoint control rails\n ([\xA713.2](../SPEC.md#132-grammar)), and a parser that locates the sender principal (its two\n adjacent owner + actor tokens) by kind (the sender-position asymmetry). *See it:* run the five subject-parsing vectors in\n [SPEC \xA712](../SPEC.md#12-conformance) and match every result, including the malformed row.\n\n3. **Envelopes + schema validation**: [SPEC \xA75](../SPEC.md#5-envelopes). Emit and parse\n `CotalMessage` with exactly one routing field set. *See it:* validate your encoder's output\n against [`spec/cotal.schema.json`](../spec/cotal.schema.json) and the two sample messages in\n [SPEC \xA712](../SPEC.md#12-conformance).\n\n4. **Presence heartbeat**: [SPEC \xA76](../SPEC.md#6-presence-and-discovery). Write your own\n presence key on the heartbeat interval and derive peers' `offline` from stale timestamps and\n KV deletes. From v0.4 your AgentCard MUST advertise `protocolVersion: \"0.4\"`, and in auth mode\n your presence record MUST carry your `lifecycleUid` (\xA76; advisory for display, since authority\n checks use the trusted lifecycle mapping, not presence); a peer that omits `protocolVersion`\n reads as pre-0.4 and is not addressed on the control-surface rails\n ([SPEC \xA76](../SPEC.md#6-presence-and-discovery),\n [\xA713.11](../SPEC.md#1311-the-hard-cut)). *See it:* run [`cotal console`](watch-a-mesh.md) and watch your endpoint appear\n in the roster and go stale when you stop heartbeating.\n\n5. **Multicast + channel join/replay**: [SPEC \xA77](../SPEC.md#7-channels). Publish to a concrete\n channel; join by subscribing under your read ACL; on join, record the watermark, backfill\n history if replay is on, and mark backfilled messages `historical`. *See it:* post from your\n client and receive it on a reference peer (or `cotal console`); a late join replays with\n `historical=true` and no live/backfill duplicates.\n\n6. **DM + anycast**: [SPEC \xA78](../SPEC.md#8-nats--jetstream-binding). Bind (do not create) your\n `dm_<owner>-<actor>-<lifecycleUid>` and, if you hold a role, `svc_<role>` durable, and ack consumed copies. *See it:*\n a reference peer unicasts to you and anycasts to your role; exactly one anycast consumer wins.\n\n7. **Receive-side checks**: [SPEC \xA74](../SPEC.md#4-delivery-modes),\n [\xA75](../SPEC.md#5-envelopes), [\xA78](../SPEC.md#8-nats--jetstream-binding). Reject any message\n whose `from.id` does not match the subject sender; derive the delivery kind\n (channel/dm/anycast) from the subject, not payload fields; ack only after surfacing, and\n terminate the permanent anomalies (`malformed-subject`, `sender-mismatch`, `malformed-json`)\n instead of redelivering them.\n\n8. **Delivery classes + backstop tolerance**: [SPEC \xA74](../SPEC.md#4-delivery-modes),\n [\xA77](../SPEC.md#7-channels). Resolve a channel's effective `live`/`durable` class from channel\n config and use one resolution everywhere. On a `durable` channel, tolerate the at-most-once\n `live` gap, catch up from the durable backstop, and deduplicate by `id` across the live,\n backfill, and durable copies. If durable membership can't be established, report *joined live\n with the backstop unestablished*, never *joined durable*. See\n [delivery-daemon.md](delivery-daemon.md) and [presence-and-delivery.md](presence-and-delivery.md).\n\n## Testing conformance\n\n[SPEC \xA712](../SPEC.md#12-conformance) is the gate: its numbered list is the set of behaviors a\nconformant authenticated NATS client implements. Two artifacts there are language-agnostic and\nreusable directly:\n\n- The **subject-parsing table** and the **sample multicast/unicast messages**: fixed vectors\n you can assert against.\n- [`spec/cotal.schema.json`](../spec/cotal.schema.json) (draft-07): validate every delivery\n message you emit against it.\n\nThe end-to-end test is the **\xA712 interop scenario** run against a **local reference mesh**:\nprovision a space, connect two clients, exchange multicast/unicast/anycast, and check a late\njoiner's replay. The repository's own smoke suite (`packages/core/smoke/`, `bin/smoke/`) is\nTypeScript, driven through `tsx` and the reference endpoint; it is the reference\nimplementation's regression harness, **not** a cross-language conformance runner. So for a\nclient in another language, the interop scenario against a local `cotal up` mesh (with a\nreference agent as the other party; spawn one via [run-a-mesh.md](run-a-mesh.md) or\n[define-a-team.md](define-a-team.md)) is the current conformance test.\n\n## What not to build\n\n- **No transport abstraction layer.** There is one binding. Bind straight to your NATS client;\n do not invent a pluggable transport interface. If you ever bind to a non-NATS substrate, the\n capability contract in [transport.md](transport.md) is what you implement against, and you\n supply durability and presence yourself, since a live-only pipe has neither.\n- **No orchestrator.** Cotal peers are lateral. A client connects, presents itself, and\n exchanges messages; it does not schedule or supervise other agents. Spawning and supervision\n live in separate tooling (the [manager](run-a-mesh.md), [mcp-tools.md](mcp-tools.md)), not in\n the wire client.\n\nKeep it thin: a NATS client, subject build/parse, envelope validation, the receive-side checks,\nand the presence/channel loops. Everything else is the reference implementation's business, not\nthe protocol's.\n"
67267
+ "body": "# Build a Cotal client\n\n> **Guide** (informative) \xB7 **For:** spec implementers \xB7 **Normative:** [SPEC](../SPEC.md). Where this guide and the spec disagree, the spec wins.\n\nThis page is the reading order for implementing a Cotal client in another language (Go,\nPython, Rust, or anything with a NATS client library) against the spec, without\nreimplementing the protocol.\n\n## What you are implementing\n\nCotal is two layers, and a client sits astride both:\n\n- **The transport-agnostic contract** ([SPEC \xA73](../SPEC.md#3-subject-layout) through\n [\xA77](../SPEC.md#7-channels)): the subject layout, delivery modes, envelopes, presence, and\n channels. This is the standard; it does not mention NATS.\n- **The NATS + JetStream binding** ([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding) through\n [\xA710](../SPEC.md#10-connection-and-onboarding)): how those abstractions map onto streams,\n durables, KV, subject-scoped auth, and the join link. It is the only binding defined today.\n\nA client is a **thin layer over a NATS client library**: the library owns the connection,\nJetStream, and KV; your code owns subject construction and parsing, envelope validation, the\nreceive-side authenticity checks, and the presence/channel loops. See\n[transport.md](transport.md) for the split and the capabilities a binding must provide.\n\n## Prerequisites\n\n- A **NATS client library with JetStream + KV support** in your language (the official\n `nats.go`, `nats.py`, `async-nats` for Rust, etc.).\n- A **local mesh to test against**. From this repo:\n\n ```bash\n cotal up # broker + auth + control plane on 127.0.0.1:4222\n cotal mint <name> --profile agent # write an agent creds file to join with\n ```\n\n `cotal mint <name> --profile agent` also takes `--allow-subscribe a,b` and\n `--allow-publish a,b` to scope the read/post ACLs, and `--out <path>`. Those two flags apply to\n the **agent** profile only: `observer` and `admin` carry a fixed read set (`observer` reads the\n whole chat plane) and `mint` refuses both flags there, so scope a reader with the agent profile. The creds file\n binds your principal (`owner.actor`, [SPEC \xA72](../SPEC.md#2-identity)) and your channel\n grants; see\n [identity-and-auth.md](identity-and-auth.md) and [run-a-mesh.md](run-a-mesh.md).\n If your client will **receive** DMs or role anycasts (step 6), mint with `--provision`\n (`--role <role>` for the anycast queue): the DM/task consumers are pre-created and\n bind-only, and the command prints the lifecycle uid your client binds them under.\n\n## Build order\n\nEach step names what to build, the section that governs it, and how to watch it work against a\nlocal mesh. The [SPEC \xA712](../SPEC.md#12-conformance) conformance list is the checklist these\nmap to.\n\n1. **Identity + connection**: [SPEC \xA72](../SPEC.md#2-identity),\n [\xA710](../SPEC.md#10-connection-and-onboarding),\n [\xA713.12](../SPEC.md#1312-nats--jetstream-binding). Read the server version from the\n **pre-auth INFO** and **fail loud below nats-server 2.12** (the v0.4 control surface relies\n on 2.12 schedule/CAS semantics); treat a repeated pre-auth drop as a possible\n oversized-CONNECT diagnostic, not an infinite retry loop. Then connect with the minted creds\n and adopt the principal bound to the credential; set the inbox prefix to your connection's\n reply inbox (`_INBOX_<connId>`) before any request, pull, or KV watch. *See it:* a wrong or missing cred is refused at connect, so a clean connect\n confirms identity and creds are wired correctly.\n\n2. **Subject construction + parsing**: [SPEC \xA73](../SPEC.md#3-subject-layout). Build the three\n messaging subject shapes plus the v0.4 endpoint control rails\n ([\xA713.2](../SPEC.md#132-grammar)), and a parser that locates the sender principal (its two\n adjacent owner + actor tokens) by kind (the sender-position asymmetry). *See it:* run the five subject-parsing vectors in\n [SPEC \xA712](../SPEC.md#12-conformance) and match every result, including the malformed row.\n\n3. **Envelopes + schema validation**: [SPEC \xA75](../SPEC.md#5-envelopes). Emit and parse\n `CotalMessage` with exactly one routing field set. *See it:* validate your encoder's output\n against [`spec/cotal.schema.json`](../spec/cotal.schema.json) and the two sample messages in\n [SPEC \xA712](../SPEC.md#12-conformance).\n\n4. **Presence heartbeat**: [SPEC \xA76](../SPEC.md#6-presence-and-discovery). Write your own\n presence key on the heartbeat interval and derive peers' `offline` from stale timestamps and\n KV deletes. From v0.4 your AgentCard MUST advertise `protocolVersion: \"0.4\"`, and in auth mode\n your presence record MUST carry your `lifecycleUid` (\xA76; advisory for display, since authority\n checks use the trusted lifecycle mapping, not presence); a peer that omits `protocolVersion`\n reads as pre-0.4 and is not addressed on the control-surface rails\n ([SPEC \xA76](../SPEC.md#6-presence-and-discovery),\n [\xA713.11](../SPEC.md#1311-the-hard-cut)). *See it:* run [`cotal console`](watch-a-mesh.md) and watch your endpoint appear\n in the roster and go stale when you stop heartbeating.\n\n5. **Multicast + channel join/replay**: [SPEC \xA77](../SPEC.md#7-channels). Publish to a concrete\n channel; join by subscribing under your read ACL; on join, record the watermark, backfill\n history if replay is on, and mark backfilled messages `historical`. *See it:* post from your\n client and receive it on a reference peer (or `cotal console`); a late join replays with\n `historical=true` and no live/backfill duplicates.\n\n6. **DM + anycast**: [SPEC \xA78](../SPEC.md#8-nats--jetstream-binding). Bind (do not create) your\n `dm_<owner>-<actor>-<lifecycleUid>` and, if you hold a role, `svc_<role>` durable, and ack consumed copies. *See it:*\n a reference peer unicasts to you and anycasts to your role; exactly one anycast consumer wins.\n\n7. **Receive-side checks**: [SPEC \xA74](../SPEC.md#4-delivery-modes),\n [\xA75](../SPEC.md#5-envelopes), [\xA78](../SPEC.md#8-nats--jetstream-binding). Reject any message\n whose `from.id` does not match the subject sender; derive the delivery kind\n (channel/dm/anycast) from the subject, not payload fields; ack only after surfacing, and\n terminate the permanent anomalies (`malformed-subject`, `sender-mismatch`, `malformed-json`)\n instead of redelivering them.\n\n8. **Delivery classes + backstop tolerance**: [SPEC \xA74](../SPEC.md#4-delivery-modes),\n [\xA77](../SPEC.md#7-channels). Resolve a channel's effective `live`/`durable` class from channel\n config and use one resolution everywhere. On a `durable` channel, tolerate the at-most-once\n `live` gap, catch up from the durable backstop, and deduplicate by `id` across the live,\n backfill, and durable copies. If durable membership can't be established, report *joined live\n with the backstop unestablished*, never *joined durable*. See\n [delivery-daemon.md](delivery-daemon.md) and [presence-and-delivery.md](presence-and-delivery.md).\n\n## Testing conformance\n\n[SPEC \xA712](../SPEC.md#12-conformance) is the gate: its numbered list is the set of behaviors a\nconformant authenticated NATS client implements. Two artifacts there are language-agnostic and\nreusable directly:\n\n- The **subject-parsing table** and the **sample multicast/unicast messages**: fixed vectors\n you can assert against.\n- [`spec/cotal.schema.json`](../spec/cotal.schema.json) (draft-07): validate every delivery\n message you emit against it.\n\nThe end-to-end test is the **\xA712 interop scenario** run against a **local reference mesh**:\nprovision a space, connect two clients, exchange multicast/unicast/anycast, and check a late\njoiner's replay. The repository's own smoke suite (`packages/core/smoke/`, `bin/smoke/`) is\nTypeScript, driven through `tsx` and the reference endpoint; it is the reference\nimplementation's regression harness, **not** a cross-language conformance runner. So for a\nclient in another language, the interop scenario against a local `cotal up` mesh (with a\nreference agent as the other party; spawn one via [run-a-mesh.md](run-a-mesh.md) or\n[define-a-team.md](define-a-team.md)) is the current conformance test.\n\n## What not to build\n\n- **No transport abstraction layer.** There is one binding. Bind straight to your NATS client;\n do not invent a pluggable transport interface. If you ever bind to a non-NATS substrate, the\n capability contract in [transport.md](transport.md) is what you implement against, and you\n supply durability and presence yourself, since a live-only pipe has neither.\n- **No orchestrator.** Cotal peers are lateral. A client connects, presents itself, and\n exchanges messages; it does not schedule or supervise other agents. Spawning and supervision\n live in separate tooling (the [manager](run-a-mesh.md), [mcp-tools.md](mcp-tools.md)), not in\n the wire client.\n\nKeep it thin: a NATS client, subject build/parse, envelope validation, the receive-side checks,\nand the presence/channel loops. Everything else is the reference implementation's business, not\nthe protocol's.\n"
65430
67268
  },
65431
67269
  {
65432
67270
  "slug": "cli",
65433
67271
  "title": "`cotal` CLI reference",
65434
67272
  "kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract.",
65435
67273
  "summary": "cotal is the operator command line for the reference implementation: bring a mesh up, mint identities, launch agents, watch what they do, and tear it all down.",
65436
- "body": "# `cotal` CLI reference\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\n`cotal` is the operator command line for the reference implementation: bring a mesh up, mint\nidentities, launch agents, watch what they do, and tear it all down. It is a thin client over the\nwire contract: the normative subjects and schemas live in the [SPEC](../SPEC.md); this page is\nlookup material for the commands, not a walkthrough; if you are new, start with\n[Getting started](getting-started.md).\n\n## Running it\n\n```bash\nnpm install -g cotal-ai # puts `cotal` on your PATH (needs Node 22+)\ncotal --help # every command, grouped\ncotal --version # cotal-ai version + each installed extension's (also `cotal -v`)\ncotal <command> --help # one command's flags and usage\n```\n\n`npx cotal-ai <command>` runs it without a global install; in a dev clone, `pnpm cotal <command>`\nruns it through `tsx` with no build step. Bare `cotal` prints help. Every command generates its own\n`--help`, usage, and shell completion from its declared flags.\n\nCommands come from the surfaces the binary composes: the base mesh CLI, the manager\n(`supervise`), and the delivery daemon (`deliver`), plus any operator-installed extensions.\n`cotal ext add <npm-package>` installs any registry providers a package contributes: commands,\nruntimes, and local process lifecycle descriptors. The `web` dashboard and optional manager\nruntimes ship this way.\n\n## Commands\n\n| Area | Command | Purpose |\n|---|---|---|\n| Set up & lifecycle | [`setup`](#setup) | Guided, configure-only setup (installs, seeds personas; launches nothing) |\n| Set up & lifecycle | [`update`](#update) | Reconcile first-party extensions and check or opt into a coherent CLI upgrade |\n| Set up & lifecycle | [`up`](#up) | Start a local mesh (nats-server + JetStream), or boot a whole manifest with `-f` |\n| Set up & lifecycle | [`down`](#down) | Stop the whole stack, selected registered components, or a manifest deploy |\n| Set up & lifecycle | [`backup`](#backup-and-restore) | Create an offline full-space or registry-only artifact from a preserved cut |\n| Set up & lifecycle | [`clean`](#clean) | Configurable cleanup: purge history (live), or wipe the local store / identity (stopped) |\n| Set up & lifecycle | [`meshes`](#meshes-use-status) | List the running meshes on this machine |\n| Set up & lifecycle | [`use`](#meshes-use-status) | Set the default mesh a bare `cotal spawn` joins |\n| Set up & lifecycle | [`status`](#meshes-use-status) | Read-only diagnostics for setup, processes, and the selected mesh |\n| Agents & personas | [`spawn`](#spawn) | Launch an agent from a persona (foreground, or `--detach` via the manager) |\n| Agents & personas | [`models`](#models) | List connector model catalogs and variants from the manager |\n| Agents & personas | [`ps`](#ps-stop-attach) | List managed agents and their mesh status |\n| Agents & personas | [`stop`](#ps-stop-attach) | Ask the manager to stop a managed agent |\n| Agents & personas | [`attach`](#ps-stop-attach) | Stream and drive a managed agent's terminal (pty runtime) |\n| Agents & personas | [`input`](#input) | Type one line into a managed agent's terminal without attaching |\n| Agents & personas | [`personas`](#personas) | List, show, edit, create, or remove local personas |\n| Agents & personas | [`supervise`](#supervise) | Run a manager daemon (the agent supervisor / control plane) |\n| Agents & personas | [`runtimes`](#runtimes) | List the agent runtimes the manager can spawn through and whether each is reachable |\n| Agents & personas | [`reconcile-gate`](#reconcile-gate) | Unfreeze an issuance gate left frozen by a crashed manager restart, after verifying the holder is gone |\n| Messaging & watching | [`endpoints`](#endpoints) | List every endpoint in the live presence roster, including infrastructure |\n| Messaging & watching | [`describe` / `invoke`](#describe-invoke) | Resolve a v0.4 service's command surface off the wire; invoke one command by name |\n| Messaging & watching | [`send`](#send) | Send one message, then exit: DM a peer, post a channel, or ask a role |\n| Messaging & watching | [`channels`](#channels) | Inspect or set the channel registry |\n| Messaging & watching | [`history`](#history) | Clear retained message history |\n| Messaging & watching | [`console`](#console) | Live protocol view for a space (TUI, or `--plain` line stream) |\n| Messaging & watching | [`web`](#web) | Browser dashboard (installed as the `@cotal-ai/web` extension) |\n| Auth & meshes | [`mint`](#mint) | Mint a creds file for a space (static auth mode) |\n| Auth & meshes | [`login`](#login-logout) | Sign in to a per-user-auth mesh's IdP (once per machine) |\n| Auth & meshes | [`logout`](#login-logout) | Revoke the IdP session and clear the cached login |\n| Auth & meshes | [`actor`](#actor) | Manage a user-auth space's actor ledger (grant / revoke / list) |\n| Auth & meshes | [`doctor`](#doctor) | Credential-health diagnosis and repair (`doctor auth`) |\n| Auth & meshes | [`join`](#join) | Join a space as your own presence (interactive) |\n| Manifest | [`topology`](#manifest-deploys) | Validate and view a mesh manifest's access graph (read-only) |\n| Extensions & misc | [`ext`](#ext) | Install / remove operator CLI extensions |\n| Extensions & misc | [`completion`](#completion) | Print or install shell completion |\n| Extensions & misc | [`feedback`](#feedback) | Send feedback to the Cotal developers |\n| Extensions & misc | [`deliver`](#server-daemons) | Run the server-side Plane-3 delivery daemon |\n| Extensions & misc | [`feedback-intake`](#server-daemons) | Run a self-hosted feedback intake server |\n\nThe manifest modes of `up`, `spawn`, and `down` (`-f <cotal.yaml>`) plus `topology` are covered\ntogether under [Manifest deploys](#manifest-deploys).\n\n## setup\n\n```bash\ncotal setup [--full] [--demo] [--yes]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--full` | off | Redo the full guided flow (implies `--demo`) |\n| `--demo` | off | Also seed the guided expert team (`david`, `sven`, `me`) |\n| `--yes`, `-y` | off | Non-interactive accept-all (for agents / CI) |\n\nGuided setup is **configure-only**: it checks prerequisites, installs the Claude Code plugin, and\nseeds persona files, and it launches nothing (no mesh, no web, no manager). First run gets the\nnarrated flow; later runs print a status card. By default it seeds one `default` persona; the\n`david`/`sven`/`me` team is opt-in via `--demo`. See [Getting started](getting-started.md) and, for\nmaintainers, [setup internals](setup-internals.md).\n\n## update\n\n```bash\ncotal update [--self]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--self` | off | If a newer release exists, install that exact validated `cotal-ai` version globally and reconcile through the newly installed binary |\n\nWithout `--self`, `update` keeps the installed first-party surfaces coherent with the running\nbinary: it force-reconciles the four built-in connectors, then reinstalls other `@cotal-ai/*`\noperator extensions at the binary's exact version. Each extension runs in an isolated child, so one\nfailure cannot poison later replays. It then checks npm; a newer binary is an informational notice\nwith `cotal update --self` as the next command, not an automatic install.\n\nWith `--self`, the npm check happens first. When a newer release exists, Cotal installs the exact\nversion it validated, resolves and verifies that package in npm's global root, then launches that\nbinary to reconcile connectors and first-party extensions to the new generation. An npx or dev-clone\ninvocation therefore installs and continues through a separate global copy; it never claims the\nalready-running process changed. If the binary is current, `--self` performs the normal local\nreconcile without reinstalling it.\n\nThird-party extensions are listed with their installed version and recorded spec but are not\nauto-updated in v1. Floating third-party updates require `@cotal-ai/*` peer-range validation and are\na future follow-up. A failed connector/extension install, npm metadata check, or requested global\ninstall is reported and makes the command exit nonzero. Independent extension attempts continue so\nthe output includes every failure; an unavailable npm registry does not undo a completed local\nreconcile, but the command still exits nonzero because it could not establish that the install is\ncurrent.\n\n## up\n\n```bash\ncotal up [--detach] [--open] [--space <s>] [--server <url>] [--channels <path>] [--runtime <name>]\ncotal up --tls-cert <cert.pem> --tls-key <key.pem> # serve TLS (both, or neither)\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\ncotal up -f <cotal.yaml> [--dry-run] [--runtime <name>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--server <url>` | auto (free local port) | Listen URL override |\n| `--host <host>` | \u2014 | Bind host override. With no `--server`, the broker URL is derived from it, so `--host <addr>` alone is enough to make a mesh reachable at that address; a `--host`/`--server` pair naming different addresses is refused. A wildcard bind (`0.0.0.0`, `::`) keeps a dialable loopback URL. Recorded on the mesh and reused by every later manager launch, so a repair or resume keeps remote [`attach`](#ps-stop-attach) working |\n| `--space <s>` | the folder's name | Space name |\n| `--store-dir <dir>` | \u2014 | JetStream store directory |\n| `--channels <path>` | `.cotal/channels.json` if present | Channel-registry seed file (JSON). An explicit path that is missing is an error |\n| `--restore <dir>` | \u2014 | Restore a completed offline backup before exposing the normal listener |\n| `--restore-only registry` | artifact selection | Restore only the registry component |\n| `--accept-missing-source` | off | Explicit disaster consent when the inode-bound preserved source is absent |\n| `--open` | off (auth) | Unauthenticated dev mesh: no JWT, no ACLs |\n| `--user-auth` | off | Per-user auth: people `cotal login`; connects are authorized against the actor ledger |\n| `--idp <url>` | \u2014 | With `--user-auth`: the IdP auth base URL to pin on first enable |\n| `--detach` | off | Run in the background (stop with `cotal down`) |\n| `--tls-cert <path>` | \u2014 | PEM certificate to serve TLS with. Must be given together with `--tls-key`. The pair is validated **before** the broker starts \u2014 readability, private-key mode, that the two match, the validity window, and that the certificate covers the host clients will dial \u2014 because `nats-server` starts happily on an expired certificate and only the client then fails. The decision is recorded, so a later bare `cotal up` after a `cotal down` keeps serving TLS rather than silently reverting to cleartext |\n| `--tls-key <path>` | \u2014 | PEM private key for `--tls-cert`. Refused if group- or other-readable (tighten to `600`) |\n| `--file <cotal.yaml>`, `-f` | \u2014 | Launch a whole mesh from a manifest |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing |\n| `--runtime <name>` | `pty` (or the manifest's, with `-f`) | Agent runtime for the mesh manager (`pty` built in; others are installed extensions, explicit-only). Resolved + probed before the broker starts; an uninstalled/unreachable runtime fails loud. With `-f`, overrides the manifest's runtime |\n| `--rotate-sys` | off | Rotate the space's system account and re-mint its two `$SYS` creds. Needs a stopped mesh; refused with `--open` |\n\n`cotal up` boots a local nats-server with JetStream and, in auth mode (the default), JWT auth and\nper-agent ACLs; `--detach` records the mesh so `cotal spawn` from any directory can find it. With no\n`--server`, it auto-selects a free port if the default address is taken; an explicit `--server`\nstays fail-loud on collision. `--detach` also brings up the control plane (delivery daemon in auth\nmode, then the manager). The `-f` form is a [manifest deploy](#manifest-deploys); see\n[Run a mesh](run-a-mesh.md).\n\n`--user-auth --idp <url>` starts the space's auth service alongside the broker (the NATS\nauth callout plus the loopback token exchange); it is torn down with `cotal down`, and a\nre-run of `cotal up` heals a dead service on a running broker. `--user-auth` and `--open`\ncontradict each other and are refused loudly; a running broker cannot change auth mode\nwithout a `cotal down` first. See [identity & auth](identity-and-auth.md).\n\n`--rotate-sys` renews the two `$SYS` credentials (`membership-observer`, `connection-evictor`).\nThey carry a 30-day expiry and nothing re-signs them in place, because the system-account seed is\nnever persisted, so they are renewed by issuing a **new system account** under the same broker\noperator and minting fresh creds against it. A plain re-`up` does **not** do this: it reuses the\nexisting trust record, and its `$SYS` creds along with it.\n\nThe rotation is safe to run on a real space, with one operational cost. The data account, the account\nsigning key, every agent credential minted from it, and the JetStream store are all untouched; what\ndies is the retired system account, and with it any out-of-band copy of the old `$SYS` creds, on every\nbroker that loads the rotated config. The cost is that **earlier full backups stop being restorable**\n(see below), so this is not a no-consequence operation. It needs the broker to restart on the rewritten\nconfig, so it runs as part of a boot:\n\n```bash\ncotal down\ncotal up --rotate-sys --detach # agents reconnect; nothing is re-provisioned\ncotal doctor auth # both $SYS creds healthy again, 30 days out\n```\n\nA rotation is a stopped, fresh boot, and anything that is not one refuses it, all for the same reason\n(the on-disk material and the broker it runs on must never end up on different generations):\n\n- a live mesh, because the running broker would keep serving the retired account;\n- an open mesh, whether that comes from `--open` or from `broker.auth: false` in a manifest, which\n has no system account at all;\n- `--restore`, because reinstating a trust root and superseding it in one command leaves no way to\n say which authority the mesh came up on;\n- an unfinished restore or resume attempt on this root, including one `cotal up` would recover on\n its own, because those paths can adopt a live listener and return without booting a broker;\n- a root that hosts more than one space, because the system account lives in the shared broker\n record and a rotation would retire every tenant's, while the root holds one `$SYS` cred pair\n pinned to one data account.\n\nTwo things to know before you run it:\n\n- **The retirement is config-load-bound.** Old `$SYS` creds are refused by any broker that loads the\n rotated config. A stale `nats-server` still running the *previous* config in memory would keep\n honouring them, so stop every broker for this root first. `--rotate-sys` refuses if this root's\n mesh is recorded as running, if anything unidentified is answering at the address it was given, or\n if the root's pid file names a live (or unreadable) process. Those are Cotal's own ownership\n records, not a scan of the process table: a `nats-server` you started by hand against this root's\n `server.conf` on some other port writes none of them and will not be seen. Do not run one.\n- **It invalidates earlier full backups.** A full artifact binds to the trust chain it was taken\n against, and that commitment covers the operator JWT and the system account. Every full backup\n taken before a rotation refuses to restore afterwards, so take a fresh `cotal backup` once the\n rotated mesh is up. `cotal up --restore` names this case when the data account still matches.\n\nThe commit is not atomic (a trust-record write plus two credential writes), so an interrupted\nrotation leaves the record ahead of the creds. That split is detected rather than silent: every\n`cotal up` on an auth mesh, and every `cotal doctor auth`, compares each `$SYS` cred's issuer against\nthe persisted record and names the retired account. `up` warns rather than refusing, because these\ncreds power the membership graph and live eviction, both of which degrade fail-soft; the mesh is not\nworth taking down over them. Re-running the rotation heals it, at the cost of one generation.\n\nWhile those creds are expired the mesh keeps delivering messages, but the\n[membership feed](delivery-daemon.md) and live connection eviction stay down; `cotal doctor auth`\nand the manager's log both name the credential and this repair.\n\n## down\n\n```bash\ncotal down\ncotal down --preserve-state [--store-dir <dir>]\ncotal down manager [delivery auth web nats ...]\ncotal down web [--space <name>]\ncotal down -f <cotal.yaml> | --run <id> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--file <cotal.yaml>`, `-f` | \u2014 | Tear down this manifest's deploy |\n| `--run <id>` | \u2014 | Tear down one `spawn -f` run by id |\n| `--space <name>` | current mesh | With components: the mesh whose target-addressed components (e.g. `web`) to stop |\n| `--dry-run` | off | Print the manifest teardown or selected components, mutate nothing |\n| `--preserve-state` | off | Bare whole stack only: fence the manager, retain principals and durable state, stop and prove the stack down, then publish `ready` |\n| `--store-dir <dir>` | `.cotal/nats` | With `--preserve-state`: the actual store path (required for a custom store) |\n\nBare `cotal down` stops the whole local stack in dependency order. Positional component names stop\nonly those self-registered local processes; for example, `cotal down manager` leaves delivery and\nthe broker running, and `cotal down web` is available when the web extension is installed. A\ncomponent that starts target-resolved (the web dashboard) is stopped the same way: `cotal down web`\nresolves the mesh exactly like `cotal web` (registry current mesh first, `--space` to name one), so\nit works from any directory; the other components always stop under the folder you run it in. The\n`-f` / `--run` forms tear down a [manifest deploy](#manifest-deploys) without stopping the whole mesh\nand cannot be combined with component names. Stopping `nats` alone is refused while an unselected\nregistered daemon is still live; include those components or use bare `cotal down`.\n\nNormal `down` remains destructive at the logical identity/durable layer. `--preserve-state` is a\ndifferent maintenance transition: it suppresses leave/deprovision cleanup, persists the manager's\nsame-principal resume inventory, stops the entire stack without removing run/auth artifacts, and\npublishes a stable inode-bound cut only after every recorded process is proven stopped and the exact\nrecorded NATS endpoint is unreachable. A missing or stale broker pidfile never counts as stopped. The\nattempt is bound durably before the manager is fenced, the resume document and attempt-bound\n`cut-intent` are fsynced before manager commit, and the manager's commitment itself is journaled\n(`cut-committed`) before any process stops. A retry after a crash at any of those boundaries reuses\nthe exact recorded attempt and finishes the remaining stop and endpoint proofs idempotently, without\nneeding the (by then intentionally dead) manager. A partial cut never publishes `ready`. It cannot\nbe combined with component names, manifest teardown, or `--dry-run`.\n\n## clean\n\n```bash\ncotal clean <history|store|all> --force\ncotal clean restore-attempt --attempt <id> --force\ncotal clean restore-fallback --attempt <id> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | `history`: target mesh |\n| `--dms` | off | `history`: also clear DM history |\n| `--store-dir <dir>` | `.cotal/nats` | `store`/`all`: JetStream store directory |\n| `--force` | \u2014 | Required: destructive, no prompting |\n| `--attempt <id>` | \u2014 | `restore-attempt`: exact stale pre-commit attempt; `restore-fallback`: matching healthy committed restore |\n\nOne configurable cleanup verb; every target requires `--force`.\n\n- `history` purges the retained message backlog on the **running** broker (channels, plus DMs\n with `--dms`). The same operation as [`history clear`](#history), which stays as an alias.\n- `store` deletes the **stopped** mesh's JetStream store (`.cotal/nats`): streams, durable\n consumers, and messages. This is the reset for stale on-disk broker state, e.g. durables\n minted by an older, incompatible Cotal generation surviving a `down`/`up` cycle.\n- `all` is `store` plus the space identity (`.cotal/auth`), the local creds and markers tied to\n it, any crash residue a normal `down` would have swept (stale pidfiles, `run/`), and the mesh's\n registry entry; the next `cotal up` mints a fresh identity.\n\n`history` needs the mesh up; `store` and `all` refuse while any recorded mesh process is still\nalive or any same-root recorded broker endpoint remains reachable (run `cotal down` first). They\nalso refuse outright on a root that holds accounts for several spaces: the store and the broker\ntrust record are shared by every space on the broker, so both targets would take out all of them\nand no `--space` can narrow that. `down`, `backup` and `up --restore` refuse there for the same\nreason. `cotal status` lists the tenants on such a root. Personas\n(`.cotal/agents`) and logs are never touched. A custom\nstore location is not recorded anywhere, so `--store-dir` must repeat whatever the mesh was\nlaunched with. Custom cleanup targets must contain either the Cotal store-generation marker or a\nreal `jetstream/` store directory; filesystem roots, project roots, and Cotal auth/maintenance trees\nare always refused.\n\n`store` and `all` also refuse every maintenance journal state. After a healthy committed restore,\n`restore-fallback` is the only supported way to remove the recorded unchanged old-store inode; it\nnever deletes the active target, requires both the exact attempt id and `--force`, and retires the\ncompleted restore journal so a later `down --preserve-state` can start a new backup cycle.\n\n## backup and restore\n\n```bash\ncotal down --preserve-state [--store-dir <dir>]\ncotal backup create <dir> [--only full|registry] [--store-dir <dir>]\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\n```\n\nBackup is offline-only. It requires the stable `ready` record from `down --preserve-state`, an exact\nstore match, no live recorded process, and an unreachable exact endpoint from the recorded cut.\nThat endpoint is probed immediately before cloning, so a live broker with a missing or stale pidfile\nis still refused. It claims the cut, reflink/copies the stopped source to a\nprivate attempt clone, and opens only that clone on a random loopback bootstrap broker with an\nindependent parent/deadline watchdog. It validates the canonical stream and pull-consumer inventory,\nwrites native snapshots with consumers excluded, and stores conservative contiguous ACK-floor\ncheckpoints separately. The original store is never opened by the backup broker, and the stack is\nnot restarted implicitly. Artifact destinations must not overlap the preserved source or maintenance\nattempt tree. Restore artifacts and targets likewise cannot nest inside or contain each other, the\npreserved source, or the maintenance attempt tree.\n\n`full` is the default and indivisible: channel registry, CHAT/DM/TASK/INBOX/DLV, ACL, MEMBERS, and\nvalidated durable checkpoints. `registry` is the sole partial artifact. Presence, derived membership\nfeed, leases, native ephemeral/history consumers, credentials, keys, tokens, owner secrets, and actor\nledger files are excluded. Artifacts are exclusively created `0700`; snapshot/checkpoint files and\nthe manifest are `0600`; `manifest.json` is written last with exact sizes and SHA-256 values. The\ndirectory is trusted operator input: hashes detect corruption, not malicious rewriting.\n\nRestore validates and stages the exact allowlisted artifact bytes before moving or creating a store.\nIt requires the same space and existing trust state. The whole pre-commit window holds a journaled\nliveness claim (coordinator, watchdogs, brokers, absolute deadline): ordinary `up` and a repeated\n`up --restore` refuse while the claim is live, and a stale attempt is recovered only after the\ndeadline has elapsed and every recorded owner is proven dead \u2014 automatically by a retried\n`up --restore`, or explicitly with `cotal clean restore-attempt --attempt <id> --force`. Nothing\never rolls back a live attempt. A registry-only artifact restores as registry-only whether or not\n`--restore-only registry` is passed; omitted infrastructure is always created and the exact\npost-restore stream inventory is asserted before commit intent. Ordinary `up` from a preserved cut\nresumes only the exact recorded source store and runtime; a contradicting `--store-dir` or\n`--runtime` fails in preflight. Authenticated restores validate the complete\nspace trust bundle before staging, including nkeys, seed matches, JWTs, signers, and space binding;\nfull restores commit to the validated operator, system-account, data-account, and active-signer root\nchain in addition to the static/user authority fingerprint. Because the system account is part of that\ncommitment, a [`cotal up --rotate-sys`](#up) makes every full artifact taken before it unrestorable\nagainst this root: take a fresh full backup after each rotation. The composed commitment is revalidated\nimmediately before store mutation and never includes secret seeds. Restore never creates fresh auth.\nSame-path restores atomically retain the old\nsource at the journaled fallback path; alternate targets retain it in place; a missing canonical\nsource needs explicit `--accept-missing-source`. Quarantine and target restores use current canonical\nconfigs on isolated random-loopback brokers, never expose native snapshot consumers, and publish a\ncommit-intent immediately before the normal listener starts. Archive bytes never instantiate the real\ntarget: after quarantine validation, every stream is re-snapshotted from the validated quarantine\nstate into attempt-owned sanitized files, and the target is restored solely from those. Before that boundary, failure rolls back\nthe attempt-owned target; after it, ambiguity preserves both stores and records forward-repair\nrecourse. The cooperative maintenance lock excludes Cotal commands, not arbitrary raw NATS processes.\n\nBootstrap brokers in every auth mode \u2014 including open \u2014 mount the store under a local account with\nrandom operation-specific logins only, each carrying the exact per-phase subject permission matrix;\nnormal static credentials and user-auth sentinel/bearer connections are rejected, and no auth\nservice or callout starts. Open mode differs only in its account label, never in authority. Inventory, each stream snapshot,\nrestore initiation, exact upload id, validation, and each checkpoint recreation use separate exact\nauthorities. Every checkpoint carries the source stream's message/first/last sequence state and must\nmatch its snapshot record before mutation; core then derives and validates the only allowed start\npolicy. TASK is not a CLI exception: the same core checkpoint API recreates its canonical `DeliverAll`\nWorkQueue durable because acknowledged tasks are absent from retention and NATS forbids a\nstart-sequence policy there. Registry-only restore creates every omitted canonical stream and transient\nbucket on the isolated target before the normal listener is exposed. It deliberately does not resume\nretained agents or recreate their DM/DLV/TASK/ACL state; their identity material stays retained and\nstopped rather than being reprovisioned into a partial restore.\n\nAfter listener readiness, the manager starts attempt-bound, validates retained credentials/tokens\nwithout granting or reprovisioning, and resumes the exact persisted principals under cleanup\nsuppression. Registry-only restore uses the same flow with an empty agent set. `commitResume` is an\nidempotent validation barrier only: success must be `awaitingFinalize` with an attempt-bound 64-hex\ncommit token and does not release suppression. Under the workspace lock, the CLI first fsyncs that\nexact evidence as `manager-committed` (restore) or `resume-committed` (ordinary resume), then calls\ntoken-bound `finalizeResume`; only an `active` response for the exact token releases suppression. The\nCLI records the same token in finalization evidence before a restore becomes `active`, or before an\nordinary resume retires and consumes the marker. Re-entry from either committed state skips the prior\nidempotent activation/commit phases, retries finalization with the durable token, and finishes the\nworkspace transition. Failure before finalization preserves the committed state and cleanup\nsuppression; it is not rewritten through a degraded transition. Re-entry between any two earlier\nboundaries reuses the same attempt and may retry the idempotent phases without deleting retained state. A missing or\nchanged per-agent dependency is a named fail-closed result; the journal becomes degraded and remains\navailable for forward repair. A retry from `resume-intent`,\n`resume-active`, or `resume-degraded` reuses the same attempt and inventory after the prior listener is\nproven stopped. Every normal restore listener has an unguessable attempt-bound NATS server name. The\nCLI fsyncs its exact name/nonce, canonical endpoint, process owner, and generation-bound target identity\nimmediately after spawn. Re-entry accepts a surviving listener only when its INFO server name, live PID\nrecord, endpoint, and target identity all match that proof; degraded restore repair then moves through\nthe guarded workspace transition only after manager commit. If an uncommitted bound owner is provably\ndead, recovery retires that exact proof under the maintenance lock and binds a fresh listener for the\nsame attempt, endpoint, and target with a new nonce and server name. A live foreign/mismatched listener\nor ambiguous owner is preserved and refused, never adopted by reachability alone. A reconstructed\ncommit/degraded attempt without either the exact bound proof or a durable dead-listener replacement\nrecord fails closed even when the recorded port is free. A later ordinary startup may pass an `active`\nrestore only when its details prove manager commit and its exact recorded listener is dead.\n\n## meshes, use, status\n\n```bash\ncotal meshes\ncotal meshes add # guided, on a terminal\ncotal meshes add <space> --server <url> [--root <dir>] [--mode auth|open] [--force]\ncotal meshes rm <space> [<space> \u2026] [--force]\ncotal use <space>\ncotal status [--space <s>] [--server <url>]\n```\n\n`meshes` lists the meshes this machine knows; a `*` marks the `current` default a bare\n`cotal spawn` joins.\n\nRun on a terminal with the space or `--server` missing, **`meshes add` is guided**: it asks for the\none thing that cannot be derived (the broker URL), probes it, and tells you what answered - open or\nrequiring credentials. It then offers the spaces your `--root` already holds credentials for, states\nthe mode as a fact about that broker rather than asking, and shows the exact record before writing\nanything. A broker that does not answer, or a space name already registered, becomes a choice rather\nthan an error. Anything you pass on the command line is taken as given and not asked again. Without\na terminal - a script, an agent, CI - nothing prompts and the flag form's errors stand\n(`COTAL_NO_PROMPT=1` forces that too).\n\n`cotal up` and `cotal down` maintain their own records. `meshes add` registers a mesh they cannot\nspeak for: one running on another machine, a shared broker, a hosted space. `--root` is the folder\nwhose `.cotal/auth` holds that mesh's credentials and whose `.cotal/agents` holds its personas\n(default: the project you run it in) \u2014 the registry stores that path, never a secret. `--mode`\ndefaults to `auth` when the root holds the space's account record and to `open` otherwise; a\nuser-auth space cannot be registered by hand, because its IdP pins are trust that only\n`cotal up --user-auth` establishes. The broker is probed before anything is recorded, so a wrong\naddress, or credentials that mesh will not accept, fails here instead of at the first `spawn`;\n`--force` records without verifying (and replaces an existing record).\n\n`meshes rm` drops records \u2014 it never stops a mesh. For a mesh running on this machine `cotal down`\nis the right verb, and `rm` says so unless you pass `--force`. A record you added by hand is only\nremoved by something that names it \u2014 `meshes rm`, or an `add --force` replacement \u2014 or by a\n`cotal up` that actually starts the broker for that same space, server and root, which becomes that\nmesh and so takes the record over (a `cotal up` for that space anywhere else refuses instead).\nNothing that merely *infers* a record is stale touches it: an\nunreachable broker is listed `offline` and stays, and `cotal down` / `cotal clean all` leave it\nalone even when it shares a root with the project they are tearing down, because nothing on this\nmachine could write it back.\n\n`use <space>` sets that default; the selection applies from every directory,\nincluding inside another mesh's project. `status` is a read-only report: machine prerequisites\n(starting with the installed `cotal-ai` version), the installed extensions and their versions, this\nfolder's `.cotal/`, the recorded meshes, and a live snapshot of the selected mesh (roster, channels,\nmembership feed). `status` takes only `--space` / `--server` to pick the mesh to inspect; it starts\nnothing.\n\n## spawn\n\n```bash\ncotal spawn [<persona>] [--detach] [--name <n>] [--agent <a>] [--model <m>] [--variant <v>] [--prompt <text>] [--cwd <dir>]\ncotal spawn -f <cotal.yaml> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | resolved mesh | Target space |\n| `--server <url>` | registry entry | Broker URL override |\n| `--creds <path>` | \u2014 | Control-caller creds for an off-registry manager (`--detach` only) |\n| `--name <n>` | persona's `name:` | Presence-name override (does not choose the persona) |\n| `--config <persona-or-path>` | \u2014 | Persona catalog name or file path; wins over the positional |\n| `--agent <a>` | `COTAL_DEFAULT_AGENT`, else `claude` | Connector type (`claude`, `opencode`, `hermes`, \u2026) |\n| `--role <r>` | persona's `role:` | Role override |\n| `--model <m>` | persona's `model:` | Model override |\n| `--variant <v>` | persona's `variant:` | Model variant override (connector-defined; e.g. OpenCode reasoning tiers) |\n| `--cwd <dir>` | this cwd | Working directory to root the agent at |\n| `--prompt <text>` | \u2014 | Initial prompt auto-submitted at start |\n| `--resume <id>` | \u2014 | Fork an existing session id into the mesh (claude only) |\n| `--transcript` / `--no-transcript` | off | Mirror the session transcript to `tr-<name>` |\n| `--events` / `--no-events` | off | Publish the session's structured event plane to its own event channel |\n| `--share-tools <sel>` | none | Share named operator MCP servers with the agent |\n| `--subscribe <a,b>` | persona's | Channel read-set override |\n| `--allow-subscribe <a,b>` | = subscribe | Read-ACL override |\n| `--allow-publish <a,b>` | deny | Post-ACL override |\n| `--detach`, `-d` | off | Launch via the manager into a detached PTY (reattach with `cotal attach`) |\n| `--on <instance>` | class anycast | With `--detach` only: pin the launch to one manager instance id (the whole id, as `ps` prints it). Refused on a foreground spawn (no manager to pin), with `-f` (a manifest deploy launches through the manager class queue), and when empty |\n| `--file <cotal.yaml>`, `-f` | \u2014 | Deploy a manifest onto the running mesh |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing |\n| `--allow-stale <a,b>` | \u2014 | With `-f`: waive named stale agents (apply-only) |\n| `--runtime <name>` | manifest's | With `-f`: override the manifest's runtime |\n\n`--events` turns on the session's **event plane**: a stream of structured events describing what\nthe agent did, rather than the prose it wrote, on a channel of its own. The channel is named after\nthe agent's principal, `events.<owner>.<actor>`, never after its display name, because two live\nagents are allowed to share a display name and would then share a stream. The launch grants publish\nrights on exactly that one channel, foreground and detached alike, and a connector that does not\npublish an event plane refuses the flag rather than starting a session whose events have nowhere to\ngo.\n\nThe flag and the grant are separate on purpose. Holding publish rights on a channel is not a request\nto publish to it, so writing an event channel into an agent file's `allowPublish` does not turn the\nplane on: only the launch does.\n\nThe persona (`--config` > positional > `COTAL_DEFAULT_PERSONA` > `default`) is loaded from the\ntarget mesh's `.cotal/agents/`; the launch flags override the file. Foreground runs the agent\nattached to your terminal; `--detach` hands the launch to the running manager. Both modes get the\ndurable backstop on a mesh that runs the delivery daemon; `--live-only` skips it for a foreground\nspawn (messages posted while it is disconnected are then not replayed). A foreground exit retires\nthe agent's creds and broker footprint, like a manager despawn. A `--detach` spawn is an\n**action**: the manager accepts it and returns the allocated identity at once, then the launch\nfollows to a terminal outcome rather than blocking (see [the control surface](control-surface.md)).\nSee [Connect Claude Code](connect-claude.md) and [Agent files](agent-files.md); `-f` is a\n[manifest deploy](#manifest-deploys). (`cotal start` was merged into `cotal spawn --detach`.)\n\n## models\n\n```bash\ncotal models [--agent <connector>] [--refresh]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--agent <connector>` | all registered connectors | Connector whose catalog to list |\n| `--refresh` | off | Ask the connector to refresh its provider cache |\n\nAsks the running manager for each connector's model catalog (model ids plus their variants)\nfor connectors that expose one (OpenCode today; a connector without a catalog says so). Pick a\nresult with `cotal spawn --model <provider/model> --variant <v>`.\n\n## endpoints\n\n```bash\ncotal endpoints [--space <s>] [--server <url>] [--creds <path>]\n```\n\nLists the mesh presence roster: agents, the manager, and any other protocol endpoint, with each\nendpoint's role, kind, status, and current activity. Unlike `ps`, this is a read-only presence view;\nit is not limited to child processes owned by the manager.\n\n## describe, invoke\n\n```bash\ncotal describe <endpoint> [--space <s>]\ncotal invoke <endpoint> <command> [--args '<json>'] [--space <s>]\ncotal invoke <endpoint> <command> --name <agent> [--admin] [--space <s>]\n```\n\nThe generic v0.4 service surface. `describe` resolves a registered endpoint's command set off the\nwire - the reserved `describe` command answers the registered contract digests, the schemas are\nfetched from the space's content-addressed contract store, recompiled, and verified against those\ndigests - and prints each command with its capability class and targeting shape. `invoke` calls one\ncommand by name: `--args` is a JSON object validated against the fetched input schema *before*\npublish; a targeted command takes `--name <agent>` (resolved to the agent's current principal via\n`ps`) or `--self`. `--admin` uses the admin instrument credential, whose cross-agent reach rides\nthe operator-only `any` authorization mode. Neither command has compile-time knowledge of any\nendpoint's schemas - this is the same trust chain every built-in control command now uses. Needs an\nauth mesh: the manager registers its service on both static and per-user meshes (a signed-in user\nrides their bearer; cross-agent reach needs the `admin` scope). An open mesh has no service\nregistry.\n\n## ps, stop, attach\n\n```bash\ncotal ps [--on <instance>] [--space <s>]\ncotal stop --name <n> [--on <instance>] [--space <s>]\ncotal attach --name <n> [--on <instance>] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | \u2014 | Managed agent to stop / attach (required) |\n| `--on <instance>` | class anycast (`ps`: class scatter) | Pin to one manager instance id (multi-manager space); takes the whole id as `ps` prints it, not a prefix. An empty value (`--on \"\"`, an unset shell variable) is refused, never treated as absent |\n\nThese are operator clients over the running manager's control plane. `ps` prints two facts per\nmanaged agent, because they answer different questions: the process fact from the manager's own\nruntime handle (`running` with its uptime, or `exited` with how long it ran), and the mesh fact from\nthe roster (`idle` / `working` / `waiting` / `mesh offline`, or `not in roster` when the seat has no\npresence row at all: a seat that has not joined yet, or one that never did). A seat can be `running` and `mesh offline` at once: the process is alive and\nits presence has lapsed. On a user-auth mesh `ps` also renders each managed agent's last\ncredential-refresh outcome, fail-closed.\n\n**Mode split (chosen up front, never try-scatter-then-degrade):**\n\n- **Static / open mesh.** Bare `ps` is a **class scatter**: it freezes the live manager class from\n the records registry, merges every registered instance's agents grouped and attributed per\n instance, and a non-answering instance is shown as `registered, no answer within the deadline`\n (never silently omitted). That label is the whole claim: the instance is registered and did not\n answer. It does not say the host is down, because a dead host never deregisters itself and a\n live one can be slow; if it is gone, deregister it.\n `--on <instance>` pins the read to one exact instance id instead. A wrong pin fails loud\n rather than falling through: a well-formed id that no live manager carries is reported as\n `manager instance <id> did not answer` (nothing else is asked), and a credential without that\n instance's rail is reported as refused by the broker, not as an unresponsive manager. A manager\n that answers with a refusal is shown with its own cause; \"no manager reachable\" is said only when\n nothing answered at all. If the scatter's own registry read fails (the freeze or the reconcile),\n `ps` says the manager registry could not be read rather than pronouncing on the managers, which\n may all be up.\n\n**`stop` and `attach` route by seat locality.** A seat can only be stopped or attached by the\nmanager actually running it, and the class queue does not know which one that is. So on a\nstatic/open mesh both verbs first ask every registered instance which one hosts the named seat, then\naddress that instance directly. You do not need `--on` for this \u2014 it happens by default.\n\n`--on <instance>` remains the override, for when you already know where the seat lives or the\nlookup itself is degraded. It is also the **only** route on a **user-auth mesh**: a ledger-scoped\nbearer does not hold the registry-read rows the lookup needs, so there the verbs stay on the class\nqueue unless you pin them yourself.\n\nIf the seat is found on no reachable instance, the error says so \u2014 how many managers answered, and\nwhich ones did not \u2014 rather than reporting a bare `no agent <name>`. That distinction matters\nbecause a single manager cannot tell \"hosted elsewhere\" from \"does not exist\": it answers\n`not-found` for both.\n- **User-auth mesh.** `cotal ps` reports what **one** manager knows about your agents (an `ep.one`\n read against the manager's in-memory roster, owner-filtered). It does **not** report other\n manager instances, and it cannot tell you that one is down \u2014 an unreachable manager is absent\n from the list, not flagged. Completeness across a multi-manager user-auth space is not claimed.\n A manager that does not answer fails the command outright (exit non-zero), rather than printing\n an empty list that could be read as \"no agents\". Your ledger row needs the `admin` scope to\n reach `ps` at all; `spawn` alone is refused by the broker (the ep tier boundary).\n\n`attach` streams and drives an agent's terminal on the `pty` runtime; detach with the escape key\n(Ctrl-] by default; see [`COTAL_DETACH_KEY`](config.md)). It does so over a one-use, holder-bound\nmesh session ([SPEC](../SPEC.md) \xA713.6): the manager replies with a signed session grant (never a\n`127.0.0.1` URL), the CLI redeems it once over the broker, and the browser console (`cotal console`)\ndrives the same session. `stop` and `attach` need a running manager to talk to. On a static mesh\nthey are cross-agent admin operations. On a user-auth mesh, your own agents (any agent under your\nowner) need only the `spawn` scope; another owner's agent needs `admin` on your ledger row\n([identity & auth](identity-and-auth.md)). Launch detached agents with [`spawn --detach`](#spawn).\n\n`attach` streams over the manager's own HTTP/WS face rather than the mesh. That endpoint binds\n**loopback by default**, so nothing is exposed by accident; `cotal up --host <addr>` passes its bind\naddress down, which is what lets you attach to an agent whose manager runs on another machine. A\nbare `cotal supervise` and an embedded manager stay machine-local. Set it directly with\n`supervise --console-host <host>`.\n\nThat address is **recorded on the mesh** and carried forward, because it is a decision rather than\nsomething later commands can work out for themselves (a broker dial address is not a manager bind\naddress). Every later manager launch for the same mesh reuses it \u2014 a same-root `cotal up` repair,\nadopting a preserved or restored listener, a `spawn -f` manifest deploy \u2014 so a manager replacement\ndoes not quietly move a reachable attach face back to loopback. Passing `--host` again overrides it,\nso you can widen or narrow exposure whenever you like; a mesh that never asked stays loopback-only\nand records nothing.\n\nBecause that face carries terminal read and write for every managed agent, it is credentialed in two\ntiers. A mesh caller receives a **ticket** bound to the single agent the manager just authorized,\nsingle-use and short-lived, so one authorized attach can never be re-pointed at someone else's\nagent. The **console token** is the operator's own, reaches every agent, and is printed only to the\nmanager's output. The roster, the live feed, and the PTY stream all answer `401` without one; the\nstatic console shell is served openly, since it describes no agent.\n\n## input\n\n```bash\ncotal input --name <n> --text <text> [--no-enter] [--on <instance>] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | | Managed agent to type into (required) |\n| `--text <text>` | | The text to type, taken verbatim (required) |\n| `--no-enter` | off | Type the text and stop there, without pressing Enter |\n| `--on <instance>` | class anycast | Pin to one manager instance id, exactly as [`attach`](#ps-stop-attach) |\n\nTypes one line into a running agent's terminal, as if you had typed it there, and returns. This is\nthe half of [`attach`](#ps-stop-attach) that a program wants: `attach` is a live stream that holds a\nsession open and expects a terminal on your side, so a script, a cron job or a web UI cannot use it\nto send a single line. `input` is one authorized call.\n\nWhat it is for is **harness commands**. A line beginning with `/` is not chat and not a message: it\nis something the agent's own harness handles, and the only way in is the keyboard.\n\n```bash\ncotal input --name reviewer --text \"/compact\" # ask the harness to compact its context\ncotal input --name reviewer --text \"/model opus\" # switch its model\ncotal input --name reviewer --text \"hold on that PR\" # ordinary typing works too\n```\n\n**Quoting.** `--text` takes a value, so a payload starting with `/` survives as written. A payload\nstarting with a dash needs the `=` form, because the shell-style `--text --foo` is ambiguous and is\nrefused rather than guessed:\n\n```bash\ncotal input --name reviewer --text=--verbose # dash-leading text: use --text=<value>\n```\n\nEnter is pressed by default, since a command typed but never submitted has not been delivered.\n`--no-enter` types the text and leaves it sitting at the prompt, which is how you stage a line and\nsend it later.\n\nNothing comes back but a delivery receipt (`\u2713 sent 9 bytes to reviewer`, counting the trailing\ncarriage return). Whatever the agent does next shows up where its output already goes: the mesh, its\ntranscript, or an `attach`.\n\n**This one is operator-only, and more narrowly than `stop` or `attach`.** Those two are granted to\nanything holding `spawn`, so an agent can stop and attach to seats under its own owner. `input` is\nnot: it is granted only to operator credentials, which on a user-auth mesh means your ledger row\nneeds the `admin` scope, the same scope [`ps`](#ps-stop-attach) already needs there. The reason is\nthat a write into a terminal is control of whatever is running in it, and on a user-auth mesh the\nown-owner rule covers every seat under you, not only the ones you launched: a `spawn`-scoped agent\ncould otherwise type into a sibling it never started. Seat locality is still resolved for you.\n\nOnly the `pty` runtime can be typed into. The external terminal runtimes (`tmux`, `cmux`, `orca`,\n`herdr`) attach to a process they do not own, so they have no input stream for it and the command\nrefuses by name rather than dropping the keystroke.\n\n## personas\n\n```bash\ncotal personas list [-v] [--running]\ncotal personas show <name>\ncotal personas edit <name>\ncotal personas new <name> (--prompt <t> | --from <f>) [--role <r>] [--model <m>]\ncotal personas rm <name> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh's persona catalog |\n| `--role <r>` | \u2014 | `new`: the persona's role |\n| `--model <m>` | \u2014 | `new`: the persona's model |\n| `--prompt <t>` | \u2014 | `new`: the persona's prompt text |\n| `--from <f>` | \u2014 | `new`: seed the prompt from a file |\n| `--verbose`, `-v` | off | `list`: include role / model / description |\n| `--running` | off | `list`: mark personas live on the mesh |\n| `--force` | \u2014 | `rm`: required, delete without prompting |\n\nPersonas are the local agent files under `.cotal/agents/` that `cotal spawn` launches. See\n[Agent files](agent-files.md) for the file format.\n\n## supervise\n\n```bash\ncotal supervise [--runtime <name>] [--space <s>] [--server <url>] [--spawn <names>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space to supervise |\n| `--server <url>` | the local mesh | Broker URL |\n| `--runtime <name>` | `pty` | Agent runtime (`pty` built in; extension runtimes are explicit-only) |\n| `--console-port <n>` | \u2014 | Protocol-console port |\n| `--console-host <host>` | loopback | Bind host for the console + attach endpoint. Loopback keeps it machine-local; `cotal up` passes the address it bound the broker to, which is what lets `cotal attach` reach this manager from another machine |\n| `--roster <file>` | \u2014 | Declarative roster to boot at startup |\n| `--launch <spec>` | \u2014 | Resolved manifest launch spec (from `up -f` / `spawn -f`) |\n| `--spawn <names>` | \u2014 | Comma-separated personas to pre-spawn at startup |\n\nThe manager is the agent supervisor and control plane: it answers `spawn --detach`, `stop`, `ps`,\n`attach`, and the `cotal_*` manager tools. `cotal up --detach` starts one for you; run `supervise`\ndirectly to recover a dead manager or drive a custom runtime. Default runtime is `pty`; install an\noptional provider first (`cotal ext add @cotal-ai/orca`, `@cotal-ai/tmux`, `@cotal-ai/cmux`, or `@cotal-ai/herdr`) and\nselect it explicitly. A missing provider or app fails loudly; there is no fallback. See [Deploy](deploy.md).\n\n## reconcile-gate\n\n```bash\ncotal reconcile-gate [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the frozen gate lives in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint whose gate is frozen |\n| `--instance <id>` | this folder's persisted manager instance | Instance id |\n\n**When you need this.** A manager restart killed partway through \u2014 after it began deregistering,\nbefore the new incarnation finished \u2014 leaves the endpoint's issuance gate *frozen*, held by a\nprocess that no longer exists. The next manager start refuses to proceed, which is correct: the\nfreeze is what stops two incarnations serving at once. But nothing can lift it, so every restart\nfails the same way. `cotal doctor` shows the gate as frozen; the manager's own start logs name the\ngate it could not advance.\n\nThis command is the way out. It checks that the holder really is gone, prints what it found, and\nthen finishes the dead operation exactly as the interrupted restart would have: revoke the old\ncredentials, evict their holders with verification, and reopen the gate. Start the manager\nafterwards and its normal takeover runs end to end.\n\n**It refuses far more often than it acts, on purpose**, and always says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `holder-alive` | The freeze-holder still has a live connection \u2014 a manager *is* running | Stop that process first. Reconciling would evict a live manager's credentials |\n| `holder-unknown` | The connection sweep could not prove the holder absent | Not safe to proceed: an unprovable holder is treated as a live one. Re-run once the broker answers completely |\n| `liveness-unestablishable` | The delivery daemon could not be asked at all | Start it (`cotal up` runs it) and re-run. Silence is never read as death |\n| `not-frozen` / `no-gate` | The gate is open, or there is no gate at that coordinate | Nothing to repair \u2014 check `--endpoint` / `--instance` |\n| `wrong-op-kind` | Frozen under a takeover or retirement, not a registration | Out of scope for this command; it will not reinterpret another operation's intent |\n| `eviction-unverified` | The holder looked gone but eviction could not be verified | The gate is left frozen, unchanged. Investigate the broker before retrying |\n| `raced` | A newer manager moved the gate mid-repair | Re-run `cotal doctor` and look again |\n\nThere is no `--force`, and no path that discards gate state: the only way this reopens a gate is by\nproving the holder is gone and then completing the operation properly.\n\n## deregister-instance\n\n```bash\ncotal deregister-instance [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the instance is registered in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint the instance serves |\n| `--instance <id>` | this folder's persisted manager instance | Instance id, the whole id as `cotal ps` prints it |\n\n**When you need this.** The service registry records *registration*, not liveness, and nothing in\nthe model expires a row. A manager that stops cleanly removes its own registration. One whose host\ndied without writing anything cannot, so its record goes on claiming a live instance forever: every\nclass scatter in that space freezes the dead slot in, and `cotal ps`, `stop` and `attach` each pay\ntheir whole deadline waiting for a machine that is never coming back. A laptop that was reimaged, a\ncontainer that was deleted, a box that will not be back on the network: those registrations have no\nother exit.\n\nThis command is that exit. It asks the instance first, and it removes a record only when the broker\naffirms the instance's own rail is empty: nothing subscribed there. Then it deletes the\nregistration's two records keys, each pinned to the revision it read, and prints what it removed.\n\n**Silence alone never passes.** An unanswered describe is what a dead host, a wedged process and a\nslow one all look like, and a hung process still holds its subscriptions, so the broker sees\ninterest on its rail. That instance is refused and the observation is printed. A dead process holds\nno connection and therefore no subscription, so a real corpse is still removed.\n\n**It refuses rather than guesses**, and says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `instance-answered` | The instance answered a pinned describe. It is alive | Nothing to repair. If it is wedged rather than gone, stop the process first; its own clean stop removes the record |\n| `instance-not-affirmed-gone` | It did not answer, and the broker did not report its rail empty, which is what a held subscription looks like: slow or hung, not affirmed gone | Nothing was removed. Stop the process; its record goes on its own clean stop, or re-run this once it is down |\n| `liveness-unestablishable` | The probe itself failed, so nothing was learned | Fix the probe's path (credential, broker) and re-run. A probe that could not run is never read as death |\n| `not-registered` | No registration at that coordinate | Check `--instance` and `--endpoint`. This takes the whole id, never a prefix |\n| `superseded` | The record moved between the read and the delete | Something is writing to it. Nothing was removed; re-observe before retrying |\n\nThere is no `--force` and no sweep: silence is not death, and a rule that removed rows on silence\nwould eventually remove a live instance that was merely slow. An operator names one instance, the\nbroker's verdict on its rail is what authorizes the removal, and the guard's job is to show them\nthey named a dead one. Removal is not a one way door either. The same instance re-registers over\nthe tombstone on its next start, under the same identity.\n\n## runtimes\n\n```bash\ncotal runtimes\n```\n\nLists every agent runtime the manager can spawn through: the built-in `pty`, the official providers\n(`orca`, `tmux`, `cmux`, `herdr`), and any custom provider installed via `cotal ext add`. Each installed\nprovider is probed so you can see what is actually reachable on this machine before selecting it:\n\n```\npty built in\norca installed \xB7 reachable @cotal-ai/orca\ntmux available \xB7 cotal ext add @cotal-ai/tmux\ncmux available \xB7 cotal ext add @cotal-ai/cmux\nherdr available \xB7 cotal ext add @cotal-ai/herdr\n```\n\n`installed \xB7 reachable` / `unreachable` is the provider's own `available()` probe; `available` means\nit is a known runtime you can add with the shown command. Selecting an unknown or uninstalled runtime\nvia `up`/`spawn --runtime <name>` fails loud and, for a known one, points at the exact `cotal ext add`\npackage \u2014 there is no silent fallback to `pty`.\n\n## send\n\n```bash\ncotal send dm <agent> \"<text>\" [--space <s>] [--server <url>] [--creds <path>]\ncotal send msg <channel> \"<text>\"\ncotal send ask <role> \"<text>\"\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and (off-registry) which credential |\n\nOne-shot messaging: connect, send a single direct message (`dm`), channel post (`msg`), or role\nask/anycast (`ask`), then exit. For a running conversation, agents use the mesh tools instead\n([MCP tools](mcp-tools.md)).\n\n## channels\n\n```bash\ncotal channels list\ncotal channels set <name> [--replay | --no-replay] [--window <n>] [--desc <s>] [--instructions <s>]\ncotal channels default --replay | --no-replay\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--replay` / `--no-replay` | \u2014 | `set`/`default`: replay history to new joiners, or not |\n| `--window <n>` | \u2014 | `set`: replay window size |\n| `--desc <s>` | \u2014 | `set`: one-line channel description |\n| `--instructions <s>` | \u2014 | `set`: instructions shown to joiners |\n\nInspects and edits the channel registry: replay policy, description, and joiner instructions. ACL\nsemantics (who may read or post) are set at mint / provision time, not here; see\n[Channels and permissions](channels-and-permissions.md). On a user-auth mesh, `list` rides your\nown login as is; `set` and `default` edit the registry over a short-lived\nchannel-writer view, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n\n## history\n\n```bash\ncotal history clear --force [--dms] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--dms` | off | Also clear DM history |\n| `--force` | \u2014 | Required: clear without prompting |\n\nPurges retained channel history; `--dms` extends it to direct-message history. An alias of\n[`clean history`](#clean). On a user-auth mesh the purge rides a short-lived purger view over\nyour login, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n## console\n\n```bash\ncotal console [--plain] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to watch |\n| `--plain` | off | Line stream instead of the TUI |\n\nA live protocol view for a space: a lazygit-style TUI, or a plain line stream on `--plain`. On a\nuser-auth mesh it rides the read-only admin view over your login, which needs ledger scope\n`admin`. See [Watch a mesh](watch-a-mesh.md).\n\n## web\n\n```bash\ncotal web [--detach] [--port <n>] [--no-open] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to serve |\n| `--port <n>` | `7799` | HTTP port |\n| `--detach` | off | Run in the background; stop with `cotal down web` or bare `cotal down` |\n| `--no-open` | off | Don't open the browser |\n\nThe browser observability dashboard: presence, channels, and a live feed. It is **not** part of\n`cotal up`: it ships inside `cotal-ai` as the `@cotal-ai/web` extension, seeded automatically on first\nrun (like the built-in connectors) so it always matches your CLI version. It self-registers `cotal web`\ninto this surface and serves\n`http://cotal.localhost:7799` (loopback; `*.localhost` resolves in Chrome/Firefox/Edge; Safari may\nneed `http://127.0.0.1:7799`). On a user-auth mesh the dashboard rides the read-only admin view\nover your login, and a channel purge asks for its own channel-purger view per click; both need\nledger scope `admin`. Detached mode re-execs the current Cotal installation, writes diagnostics to\nthe mesh root's `.cotal/web.log`, and reports success only after the HTTP server answers. It requires\na recorded mesh root, but can be launched from any directory once `cotal up` has recorded the mesh.\nSee [Watch a mesh](watch-a-mesh.md).\n\n## mint\n\n```bash\ncotal mint <name> [--profile <agent|observer|admin>] [--out <path>] [--signer]\ncotal mint <name> --provision [--role <role>] [--space <s>] [--server <url>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--profile <agent\\|observer\\|admin>` | `agent` | Credential profile |\n| `--out <path>` | `.cotal/auth/creds/<name>.creds` | Output path |\n| `--signer` | off | Emit a stripped account-signing file instead |\n| `--force` | off | With `--signer`: overwrite an existing file |\n| `--allow-subscribe <a,b>` | profile default | Read-ACL override |\n| `--allow-publish <a,b>` | profile default | Post-ACL override |\n| `--role <role>` | the agent file's | Agent profile: the anycast task queue the identity pulls (`svc_<role>`) |\n| `--provision` | off | Agent profile: also pre-create the identity's bind-only DM/deliver durables (and its role's task queue) on the live mesh, so the credential can consume |\n| `--space <s>`, `--server <url>` | the resolved mesh | With `--provision`: which mesh to provision on |\n\nMints a NATS creds file for a space in **static** auth mode, scoped to a profile and (optionally)\nexplicit read/post ACLs. `--signer` emits an account-signing file for delegating minting to another\nhost. A per-user-auth space refuses `mint`: agents there join under a logged-in user\n([`login`](#login-logout) + [`actor grant`](#actor)), never via a handed-out creds file. See\n[Identity and auth](identity-and-auth.md).\n\nA plain mint is creds only: the identity can publish within its post ACL at once, but on an authed\nmesh its DM inbox and task queue are provisioner-pre-created and bind-only, so a **consuming**\nconnect fails until they exist. `--provision` performs that pre-create in the same command (a\nprovisioner cred is minted from the space's trust material, used, and dropped), so a long-running\nclient you start yourself can receive DMs and role anycasts like a spawned seat. The command prints\nthe identity's principal (its wire id) and lifecycle uid; a consuming client passes that uid as its\n`lifecycleUid`. Agent profile only; an open mesh needs none of this (peers self-create there). The\nmesh it provisions on must be the one this folder's auth is for - same space and same account key -\nso `--provision` can never quietly mint under another root's trust material.\n\n## login, logout\n\n```bash\ncotal login --idp <auth base URL> [--client-id <id>]\ncotal logout --idp <auth base URL>\n```\n\nSigns you in to a per-user-auth mesh's IdP (device code flow) and caches the session; run it\nonce per machine. It prints your IdP subject, the id the operator grants against. After a\nlogin, every command on that mesh works under your identity: each connect takes a fresh IdP\nproof, exchanges it locally for a short-lived bearer, and is authorized against the actor\nledger at connect time. `logout` revokes the IdP session and clears the cache. See\n[identity & auth](identity-and-auth.md).\n\n## actor\n\n```bash\ncotal actor grant <actor> --sub <IdP subject> [--scope a,b] [--allow-subscribe a,b] [--allow-publish a,b] [--role <r>] [--label <l>]\ncotal actor revoke <actor> (--sub <IdP subject> | --owner <u_\u2026>)\ncotal actor list\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | the folder's | Space whose ledger to manage |\n| `--sub <subject>` | \u2014 | The IdP subject (shown by `cotal login`) the actor belongs to |\n| `--owner <u_\u2026>` | \u2014 | The derived owner token (alternative to `--sub`) |\n| `--scope <a,b>` | `spawn,role:default` | Capability scope (`''` = none; `spawn` = may run agents, `role:<r>` = may delegate role r, `admin` = cross-agent control) |\n| `--allow-subscribe <a,b>` | `>` (all channels) | Channel read ACL; the user's envelope, their agents can never read beyond it |\n| `--allow-publish <a,b>` | `>` (all channels) | Channel post ACL; also the envelope for their agents' posting |\n| `--role <r>` | \u2014 | Role (scopes the task-queue consumer) |\n| `--label <l>` | \u2014 | Display label for `actor list` (never the IdP subject) |\n\nThe actor ledger is the single authorization source of a user-auth space: no row, no access.\nA bare `grant` is the **full** envelope (all channels, may spawn); the flags narrow it. A\nre-grant **replaces** the row, so to add a capability, re-grant with it added to the current\nscope (`cotal actor list` shows what a row holds). `revoke` denies the next exchange and the\nnext connect with no restart, and evicts the principal's live connections. Managed-agent rows\n(written by the spawn path) live in a disjoint row space this command never touches. See\n[identity & auth](identity-and-auth.md).\n\n## doctor\n\n```bash\ncotal doctor auth [--fix]\n```\n\nCredential-health diagnosis and repair for this folder's mesh: renders every managed\ncredential as healthy / near-expiry / expired and ends in `healthy` or the exact next\ncommand; `--fix` applies the repairs it can. The one surface every stale-credential error\npoints at.\n\n## join\n\n```bash\ncotal join --space <s> --name <n> [--role <r>] [--channel <c>]\ncotal join --link <url> | --token <t>\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and which credential |\n| `--name <n>` | \u2014 | Your presence name |\n| `--role <r>` | \u2014 | Your role |\n| `--channel <c>` | \u2014 | Channel to join |\n| `--kind <k>` | `agent` | Endpoint kind |\n| `--link <url>` | \u2014 | Join link (`cotal://\u2026`) |\n| `--token <t>` | \u2014 | Join token |\n| `--lifecycle-uid <uid>` | \u2014 | Required with `--creds`: the lifecycle UID minted alongside the credential (`COTAL_LIFECYCLE_UID` works too). A credential's durable grants name exact lifecycle-keyed resources, so `join` refuses to invent one |\n| `--tls` | off | Connect over TLS |\n\nAn interactive presence: join a space under your own name and role, without launching an agent\nharness. A `--link` or `--token` supplies the where and the auth in one value. See\n[Spaces](spaces.md) and [Identity and auth](identity-and-auth.md).\n\n## Manifest deploys\n\nA `cotal.yaml` manifest declares a whole mesh (channels, personas, roles, and ACLs) in one file.\nThree commands consume it, plus a read-only validator:\n\n```bash\ncotal up -f cotal.yaml # boot a fresh mesh from the manifest\ncotal spawn -f cotal.yaml # deploy the manifest additively onto a running mesh\ncotal down -f cotal.yaml # tear that deploy down (or --run <id> for one run)\ncotal topology view -f cotal.yaml # validate + view the access graph, change nothing\n```\n\n`up -f` and `spawn -f` differ in target: `up -f` brings up a new broker and applies the manifest;\n`spawn -f` requires an already-reachable mesh and applies additively (ownership-scoped). On a\nuser-auth mesh, `spawn -f` deploys over your own login (the deployer view, gated on ledger scope\n`spawn`): the manifest's agents land under your owner, a manifest claiming another owner is\nrefused, and seeding new channels additionally needs scope `admin`. Both take\n`--dry-run` to print the plan without mutating anything. `topology` validates the manifest and\nrenders its channel / role / ACL graph. See [Define a team](define-a-team.md) and the\n[manifest reference](manifest.md).\n\n## ext\n\n```bash\ncotal ext # same as `list`\ncotal ext add <npm-package>\ncotal ext remove <name>\ncotal ext list\ncotal ext root # print just the install prefix (scriptable)\ncotal ext seed [--repair|--reset|--force]\n```\n\nOperator-installed extensions: `add` installs an npm package into a cotal-owned prefix and records\nevery registry provider it contributes. Commands appear in help, completion, and dispatch; runtime\nproviders are lazy-loaded by commands such as `supervise`; local process providers participate in\n`status` and selective `down`. `remove` and `list` manage them. The `@cotal-ai/web` dashboard is the\ncanonical command/process example. Installed packages and their location are described in\n[config](config.md).\n\nBare `cotal ext` lists the inventory, headed by the install prefix. That prefix is a cotal-owned npm\nroot kept **separate** from npm's own global tree, so these packages never show up in `npm list -g` \u2014\n`cotal ext` (or the Extensions section of `cotal status`) is the canonical inventory. `cotal ext root`\nprints only the path, for scripts. The versions shown are the manifest pin recorded at add time.\n\nRemoving an extension that owns a running local process is refused with the mesh root and its\n`cotal down <component>` command; stop it first so uninstalling the package never strands a process\nwhose lifecycle provider is gone.\n\n### Built-in connectors are seeded extensions\n\nThe first-party agent connectors (`claude`, `opencode`, `codex`, `hermes`, `pi`) are not compiled into\nthe binary. They are seeded on first run through the **same** `ext add` path a third party uses, and\nappear in `cotal ext list` like any other extension. So you can remove one you do not want\n(`cotal ext remove @cotal-ai/connector-hermes`), and a deliberately-removed connector STAYS removed\nacross upgrades. `cotal ext add <your-package>` adds a third-party connector the same way. The web\ndashboard (`@cotal-ai/web`, providing `command:web`) is a fifth built-in seeded on the same path.\n\n`cotal ext seed` is the maintenance entry for that seeding (it runs automatically on the first real\ncommand of each boot, so you rarely call it):\n\n| Flag | Meaning |\n|---|---|\n| (none) | Reconcile: seed any never-seeded built-in, refresh a seeded one whose version the binary bumped, leave a removed one removed. A no-op once current. |\n| `--repair` | Recover after an interrupted seed or a lost authority (rebuilds the interrupted connector; restores the removed-vs-never-seeded record from its durable backup). |\n| `--reset` | Discard the record and re-seed all six built-ins (the five connectors plus the web dashboard). **Resurrects any you removed.** Rebuilds cleanly over corrupt seed state. |\n| `--force` | Re-seed the built-ins even when the version stamp is current or a downgrade. |\n\nThe default connector for a bare `cotal spawn` (no `--agent`) is `claude`; set `COTAL_DEFAULT_AGENT`\n(e.g. `opencode`) to change it. An `--agent` naming a removed connector fails loud with the exact\n`cotal ext add` to restore it. Set `COTAL_SKIP_CONNECTOR_SEED=1` to turn off the automatic first-run\nseed/refresh entirely (for a controlled or offline setup that manages connectors by hand); `cotal ext\nseed` still runs on request.\n\n## completion\n\n```bash\ncotal completion <bash|zsh|fish|powershell> # print a stub to eval / source\ncotal completion install [shell] # install it persistently\n```\n\nPrints or installs shell completion. Completion candidates come from each command's declared flags\nand, where useful, live mesh state (spaces, personas, managed agents) resolved offline.\n\n## feedback\n\n```bash\ncotal feedback \"<summary>\" [--type <t>] [--email <e>] [--details <text>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--type <t>` | \u2014 | `bug` \\| `idea` \\| `friction` \\| `praise` \\| `other` |\n| `--details <text>` | \u2014 | Longer free-form details |\n| `--severity <s>` | \u2014 | `low` \\| `medium` \\| `high` |\n| `--area <a>` | \u2014 | The part of Cotal this concerns |\n| `--email <e>` | git email | Contact email (required on the keyless public path) |\n| `--name <n>` | \u2014 | Your name (optional) |\n| `--url <url>` | keyed / public intake | Intake URL override |\n| `--key <k>` | `COTAL_FEEDBACK_KEY` | Feedback key |\n\nSends feedback to the Cotal developers. With a key (`--key` / `COTAL_FEEDBACK_KEY`) it routes to the\nkeyed beta intake; without one it goes to the public `cotal.ai` intake and requires a contact email\n(`--email` / `COTAL_FEEDBACK_EMAIL`, else your git email). Run a self-hosted intake with\n[`feedback-intake`](#server-daemons).\n\n## Server daemons\n\nTwo long-lived infra roles ship with the CLI. They are not part of everyday operation; the delivery\ndaemon comes up automatically with `cotal up --detach` in auth mode.\n\n```bash\ncotal deliver --space <s> [--server <url>] [--creds <file>]\ncotal auth-service --space <s> --server <url> [--port <n>]\ncotal feedback-intake --keys <keys.json> [--port <n>] [--creds <file>]\n```\n\n`auth-service` runs a user-auth space's identity plane (the NATS auth callout plus the\nloopback token exchange and JWKS); `cotal up --user-auth` starts and supervises it for you,\nso you run it directly only to recover one by hand.\n\n`deliver` runs the server-side Plane-3 delivery daemon: the durable backstop and membership/ACL\nauthority. It is auth-mode-only and single-instance (`--shard`/`--shards` accept only `N=1`);\n`--dev-mint` mints a scoped cred from the local signer for standalone dev. See the\n[delivery daemon](delivery-daemon.md). `feedback-intake` runs a self-hosted feedback server\n(requires `--keys` and a scoped `--creds`), announcing submissions into a space channel; flags\ninclude `--host`/`--port`, `--store`, `--space`/`--channel`, `--max-bytes`, and `--rate-limit`.\n\n## Plumbing\n\n`cotal __complete <words\u2026>` is the internal entry the shell-completion stubs call to emit candidates\nfor the current command line; you never run it directly. `cotal agent-bearer` is machine-facing\nplumbing on user-auth meshes: spawned agents exec it to print a fresh short-lived bearer from their\nspawn-time secret; you never run it directly either. (`cotal start` is a removed tombstone: it\nerrors and points you to `cotal spawn --detach`.)\n"
67274
+ "body": "# `cotal` CLI reference\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\n`cotal` is the operator command line for the reference implementation: bring a mesh up, mint\nidentities, launch agents, watch what they do, and tear it all down. It is a thin client over the\nwire contract: the normative subjects and schemas live in the [SPEC](../SPEC.md); this page is\nlookup material for the commands, not a walkthrough; if you are new, start with\n[Getting started](getting-started.md).\n\n## Running it\n\n```bash\nnpm install -g cotal-ai # puts `cotal` on your PATH (needs Node 22+)\ncotal --help # every command, grouped\ncotal --version # cotal-ai version + each installed extension's (also `cotal -v`)\ncotal <command> --help # one command's flags and usage\n```\n\n`npx cotal-ai <command>` runs it without a global install; in a dev clone, `pnpm cotal <command>`\nruns it through `tsx` with no build step. Bare `cotal` prints help. Every command generates its own\n`--help`, usage, and shell completion from its declared flags.\n\nCommands come from the surfaces the binary composes: the base mesh CLI, the manager\n(`supervise`), and the delivery daemon (`deliver`), plus any operator-installed extensions.\n`cotal ext add <npm-package>` installs any registry providers a package contributes: commands,\nruntimes, and local process lifecycle descriptors. The `web` dashboard and optional manager\nruntimes ship this way.\n\n## Commands\n\n| Area | Command | Purpose |\n|---|---|---|\n| Set up & lifecycle | [`setup`](#setup) | Guided, configure-only setup (installs, seeds personas; launches nothing) |\n| Set up & lifecycle | [`update`](#update) | Reconcile first-party extensions and check or opt into a coherent CLI upgrade |\n| Set up & lifecycle | [`up`](#up) | Start a local mesh (nats-server + JetStream), or boot a whole manifest with `-f` |\n| Set up & lifecycle | [`down`](#down) | Stop the whole stack, selected registered components, or a manifest deploy |\n| Set up & lifecycle | [`backup`](#backup-and-restore) | Create an offline full-space or registry-only artifact from a preserved cut |\n| Set up & lifecycle | [`clean`](#clean) | Configurable cleanup: purge history (live), or wipe the local store / identity (stopped) |\n| Set up & lifecycle | [`meshes`](#meshes-use-status) | List the running meshes on this machine |\n| Set up & lifecycle | [`use`](#meshes-use-status) | Set the default mesh a bare `cotal spawn` joins |\n| Set up & lifecycle | [`status`](#meshes-use-status) | Read-only diagnostics for setup, processes, and the selected mesh |\n| Agents & personas | [`spawn`](#spawn) | Launch an agent from a persona (foreground, or `--detach` via the manager) |\n| Agents & personas | [`models`](#models) | List connector model catalogs and variants from the manager |\n| Agents & personas | [`ps`](#ps-stop-attach) | List managed agents and their mesh status |\n| Agents & personas | [`stop`](#ps-stop-attach) | Ask the manager to stop a managed agent |\n| Agents & personas | [`attach`](#ps-stop-attach) | Stream and drive a managed agent's terminal (pty runtime) |\n| Agents & personas | [`input`](#input) | Type one line into a managed agent's terminal without attaching |\n| Agents & personas | [`personas`](#personas) | List, show, edit, create, or remove local personas |\n| Agents & personas | [`supervise`](#supervise) | Run a manager daemon (the agent supervisor / control plane) |\n| Agents & personas | [`runtimes`](#runtimes) | List the agent runtimes the manager can spawn through and whether each is reachable |\n| Agents & personas | [`reconcile-gate`](#reconcile-gate) | Unfreeze an issuance gate left frozen by a crashed manager restart, after verifying the holder is gone |\n| Messaging & watching | [`endpoints`](#endpoints) | List every endpoint in the live presence roster, including infrastructure |\n| Messaging & watching | [`describe` / `invoke`](#describe-invoke) | Resolve a v0.4 service's command surface off the wire; invoke one command by name |\n| Messaging & watching | [`send`](#send) | Send one message, then exit: DM a peer, post a channel, or ask a role |\n| Messaging & watching | [`channels`](#channels) | Inspect or set the channel registry |\n| Messaging & watching | [`history`](#history) | Clear retained message history |\n| Messaging & watching | [`console`](#console) | Live protocol view for a space (TUI, or `--plain` line stream) |\n| Messaging & watching | [`web`](#web) | Browser dashboard (installed as the `@cotal-ai/web` extension) |\n| Auth & meshes | [`mint`](#mint) | Mint a creds file for a space (static auth mode) |\n| Auth & meshes | [`login`](#login-logout) | Sign in to a per-user-auth mesh's IdP (once per machine) |\n| Auth & meshes | [`logout`](#login-logout) | Revoke the IdP session and clear the cached login |\n| Auth & meshes | [`actor`](#actor) | Manage a user-auth space's actor ledger (grant / revoke / list) |\n| Auth & meshes | [`doctor`](#doctor) | Credential-health diagnosis and repair (`doctor auth`) |\n| Auth & meshes | [`join`](#join) | Join a space as your own presence (interactive) |\n| Manifest | [`topology`](#manifest-deploys) | Validate and view a mesh manifest's access graph (read-only) |\n| Extensions & misc | [`ext`](#ext) | Install / remove operator CLI extensions |\n| Extensions & misc | [`completion`](#completion) | Print or install shell completion |\n| Extensions & misc | [`feedback`](#feedback) | Send feedback to the Cotal developers |\n| Extensions & misc | [`deliver`](#server-daemons) | Run the server-side Plane-3 delivery daemon |\n| Extensions & misc | [`feedback-intake`](#server-daemons) | Run a self-hosted feedback intake server |\n\nThe manifest modes of `up`, `spawn`, and `down` (`-f <cotal.yaml>`) plus `topology` are covered\ntogether under [Manifest deploys](#manifest-deploys).\n\n## setup\n\n```bash\ncotal setup [--full] [--demo] [--yes]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--full` | off | Redo the full guided flow (implies `--demo`) |\n| `--demo` | off | Also seed the guided expert team (`david`, `sven`, `me`) |\n| `--yes`, `-y` | off | Non-interactive accept-all (for agents / CI) |\n\nGuided setup is **configure-only**: it checks prerequisites, installs the Claude Code plugin, and\nseeds persona files, and it launches nothing (no mesh, no web, no manager). First run gets the\nnarrated flow; later runs print a status card. By default it seeds one `default` persona; the\n`david`/`sven`/`me` team is opt-in via `--demo`. See [Getting started](getting-started.md) and, for\nmaintainers, [setup internals](setup-internals.md).\n\n## update\n\n```bash\ncotal update [--self]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--self` | off | If a newer release exists, install that exact validated `cotal-ai` version globally and reconcile through the newly installed binary |\n\nWithout `--self`, `update` keeps the installed first-party surfaces coherent with the running\nbinary: it force-reconciles the four built-in connectors, then reinstalls other `@cotal-ai/*`\noperator extensions at the binary's exact version. Each extension runs in an isolated child, so one\nfailure cannot poison later replays. It then checks npm; a newer binary is an informational notice\nwith `cotal update --self` as the next command, not an automatic install.\n\nWith `--self`, the npm check happens first. When a newer release exists, Cotal installs the exact\nversion it validated, resolves and verifies that package in npm's global root, then launches that\nbinary to reconcile connectors and first-party extensions to the new generation. An npx or dev-clone\ninvocation therefore installs and continues through a separate global copy; it never claims the\nalready-running process changed. If the binary is current, `--self` performs the normal local\nreconcile without reinstalling it.\n\nThird-party extensions are listed with their installed version and recorded spec but are not\nauto-updated in v1. Floating third-party updates require `@cotal-ai/*` peer-range validation and are\na future follow-up. A failed connector/extension install, npm metadata check, or requested global\ninstall is reported and makes the command exit nonzero. Independent extension attempts continue so\nthe output includes every failure; an unavailable npm registry does not undo a completed local\nreconcile, but the command still exits nonzero because it could not establish that the install is\ncurrent.\n\n## up\n\n```bash\ncotal up [--detach] [--open] [--space <s>] [--server <url>] [--channels <path>] [--runtime <name>]\ncotal up --tls-cert <cert.pem> --tls-key <key.pem> # serve TLS (both, or neither)\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\ncotal up -f <cotal.yaml> [--dry-run] [--runtime <name>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--server <url>` | auto (free local port) | Listen URL override |\n| `--host <host>` | \u2014 | Bind host override. With no `--server`, the broker URL is derived from it, so `--host <addr>` alone is enough to make a mesh reachable at that address; a `--host`/`--server` pair naming different addresses is refused. A wildcard bind (`0.0.0.0`, `::`) keeps a dialable loopback URL. Recorded on the mesh and reused by every later manager launch, so a repair or resume keeps remote [`attach`](#ps-stop-attach) working |\n| `--space <s>` | the folder's name | Space name |\n| `--store-dir <dir>` | \u2014 | JetStream store directory |\n| `--channels <path>` | `.cotal/channels.json` if present | Channel-registry seed file (JSON). An explicit path that is missing is an error |\n| `--restore <dir>` | \u2014 | Restore a completed offline backup before exposing the normal listener |\n| `--restore-only registry` | artifact selection | Restore only the registry component |\n| `--accept-missing-source` | off | Explicit disaster consent when the inode-bound preserved source is absent |\n| `--open` | off (auth) | Unauthenticated dev mesh: no JWT, no ACLs |\n| `--user-auth` | off | Per-user auth: people `cotal login`; connects are authorized against the actor ledger |\n| `--idp <url>` | \u2014 | With `--user-auth`: the IdP auth base URL to pin on first enable |\n| `--detach` | off | Run in the background (stop with `cotal down`) |\n| `--tls-cert <path>` | \u2014 | PEM certificate to serve TLS with. Must be given together with `--tls-key`. The pair is validated **before** the broker starts \u2014 readability, private-key mode, that the two match, the validity window, and that the certificate covers the host clients will dial \u2014 because `nats-server` starts happily on an expired certificate and only the client then fails. The decision is recorded, so a later bare `cotal up` after a `cotal down` keeps serving TLS rather than silently reverting to cleartext |\n| `--tls-key <path>` | \u2014 | PEM private key for `--tls-cert`. Refused if group- or other-readable (tighten to `600`) |\n| `--file <cotal.yaml>`, `-f` | \u2014 | Launch a whole mesh from a manifest |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing |\n| `--runtime <name>` | `pty` (or the manifest's, with `-f`) | Agent runtime for the mesh manager (`pty` built in; others are installed extensions, explicit-only). Resolved + probed before the broker starts; an uninstalled/unreachable runtime fails loud. With `-f`, overrides the manifest's runtime |\n| `--rotate-sys` | off | Rotate the space's system account and re-mint its two `$SYS` creds. Needs a stopped mesh; refused with `--open` |\n\n`cotal up` boots a local nats-server with JetStream and, in auth mode (the default), JWT auth and\nper-agent ACLs; `--detach` records the mesh so `cotal spawn` from any directory can find it. With no\n`--server`, it auto-selects a free port if the default address is taken; an explicit `--server`\nstays fail-loud on collision. `--detach` also brings up the control plane (delivery daemon in auth\nmode, then the manager). The `-f` form is a [manifest deploy](#manifest-deploys); see\n[Run a mesh](run-a-mesh.md).\n\n`--user-auth --idp <url>` starts the space's auth service alongside the broker (the NATS\nauth callout plus the loopback token exchange); it is torn down with `cotal down`, and a\nre-run of `cotal up` heals a dead service on a running broker. `--user-auth` and `--open`\ncontradict each other and are refused loudly; a running broker cannot change auth mode\nwithout a `cotal down` first. See [identity & auth](identity-and-auth.md).\n\n`--rotate-sys` renews the two `$SYS` credentials (`membership-observer`, `connection-evictor`).\nThey carry a 30-day expiry and nothing re-signs them in place, because the system-account seed is\nnever persisted, so they are renewed by issuing a **new system account** under the same broker\noperator and minting fresh creds against it. A plain re-`up` does **not** do this: it reuses the\nexisting trust record, and its `$SYS` creds along with it.\n\nThe rotation is safe to run on a real space, with one operational cost. The data account, the account\nsigning key, every agent credential minted from it, and the JetStream store are all untouched; what\ndies is the retired system account, and with it any out-of-band copy of the old `$SYS` creds, on every\nbroker that loads the rotated config. The cost is that **earlier full backups stop being restorable**\n(see below), so this is not a no-consequence operation. It needs the broker to restart on the rewritten\nconfig, so it runs as part of a boot:\n\n```bash\ncotal down\ncotal up --rotate-sys --detach # agents reconnect; nothing is re-provisioned\ncotal doctor auth # both $SYS creds healthy again, 30 days out\n```\n\nA rotation is a stopped, fresh boot, and anything that is not one refuses it, all for the same reason\n(the on-disk material and the broker it runs on must never end up on different generations):\n\n- a live mesh, because the running broker would keep serving the retired account;\n- an open mesh, whether that comes from `--open` or from `broker.auth: false` in a manifest, which\n has no system account at all;\n- `--restore`, because reinstating a trust root and superseding it in one command leaves no way to\n say which authority the mesh came up on;\n- an unfinished restore or resume attempt on this root, including one `cotal up` would recover on\n its own, because those paths can adopt a live listener and return without booting a broker;\n- a root that hosts more than one space, because the system account lives in the shared broker\n record and a rotation would retire every tenant's, while the root holds one `$SYS` cred pair\n pinned to one data account.\n\nTwo things to know before you run it:\n\n- **The retirement is config-load-bound.** Old `$SYS` creds are refused by any broker that loads the\n rotated config. A stale `nats-server` still running the *previous* config in memory would keep\n honouring them, so stop every broker for this root first. `--rotate-sys` refuses if this root's\n mesh is recorded as running, if anything unidentified is answering at the address it was given, or\n if the root's pid file names a live (or unreadable) process. Those are Cotal's own ownership\n records, not a scan of the process table: a `nats-server` you started by hand against this root's\n `server.conf` on some other port writes none of them and will not be seen. Do not run one.\n- **It invalidates earlier full backups.** A full artifact binds to the trust chain it was taken\n against, and that commitment covers the operator JWT and the system account. Every full backup\n taken before a rotation refuses to restore afterwards, so take a fresh `cotal backup` once the\n rotated mesh is up. `cotal up --restore` names this case when the data account still matches.\n\nThe commit is not atomic (a trust-record write plus two credential writes), so an interrupted\nrotation leaves the record ahead of the creds. That split is detected rather than silent: every\n`cotal up` on an auth mesh, and every `cotal doctor auth`, compares each `$SYS` cred's issuer against\nthe persisted record and names the retired account. `up` warns rather than refusing, because these\ncreds power the membership graph and live eviction, both of which degrade fail-soft; the mesh is not\nworth taking down over them. Re-running the rotation heals it, at the cost of one generation.\n\nWhile those creds are expired the mesh keeps delivering messages, but the\n[membership feed](delivery-daemon.md) and live connection eviction stay down; `cotal doctor auth`\nand the manager's log both name the credential and this repair.\n\n## down\n\n```bash\ncotal down\ncotal down --preserve-state [--store-dir <dir>]\ncotal down manager [delivery auth web nats ...]\ncotal down web [--space <name>]\ncotal down -f <cotal.yaml> | --run <id> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--file <cotal.yaml>`, `-f` | \u2014 | Tear down this manifest's deploy |\n| `--run <id>` | \u2014 | Tear down one `spawn -f` run by id |\n| `--space <name>` | current mesh | With components: the mesh whose target-addressed components (e.g. `web`) to stop |\n| `--dry-run` | off | Print the manifest teardown or selected components, mutate nothing |\n| `--preserve-state` | off | Bare whole stack only: fence the manager, retain principals and durable state, stop and prove the stack down, then publish `ready` |\n| `--store-dir <dir>` | `.cotal/nats` | With `--preserve-state`: the actual store path (required for a custom store) |\n\nBare `cotal down` stops the whole local stack in dependency order. Positional component names stop\nonly those self-registered local processes; for example, `cotal down manager` leaves delivery and\nthe broker running, and `cotal down web` is available when the web extension is installed. A\ncomponent that starts target-resolved (the web dashboard) is stopped the same way: `cotal down web`\nresolves the mesh exactly like `cotal web` (registry current mesh first, `--space` to name one), so\nit works from any directory; the other components always stop under the folder you run it in. The\n`-f` / `--run` forms tear down a [manifest deploy](#manifest-deploys) without stopping the whole mesh\nand cannot be combined with component names. Stopping `nats` alone is refused while an unselected\nregistered daemon is still live; include those components or use bare `cotal down`.\n\nNormal `down` remains destructive at the logical identity/durable layer. `--preserve-state` is a\ndifferent maintenance transition: it suppresses leave/deprovision cleanup, persists the manager's\nsame-principal resume inventory, stops the entire stack without removing run/auth artifacts, and\npublishes a stable inode-bound cut only after every recorded process is proven stopped and the exact\nrecorded NATS endpoint is unreachable. A missing or stale broker pidfile never counts as stopped. The\nattempt is bound durably before the manager is fenced, the resume document and attempt-bound\n`cut-intent` are fsynced before manager commit, and the manager's commitment itself is journaled\n(`cut-committed`) before any process stops. A retry after a crash at any of those boundaries reuses\nthe exact recorded attempt and finishes the remaining stop and endpoint proofs idempotently, without\nneeding the (by then intentionally dead) manager. A partial cut never publishes `ready`. It cannot\nbe combined with component names, manifest teardown, or `--dry-run`.\n\n## clean\n\n```bash\ncotal clean <history|store|all> --force\ncotal clean restore-attempt --attempt <id> --force\ncotal clean restore-fallback --attempt <id> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | `history`: target mesh |\n| `--dms` | off | `history`: also clear DM history |\n| `--store-dir <dir>` | `.cotal/nats` | `store`/`all`: JetStream store directory |\n| `--force` | \u2014 | Required: destructive, no prompting |\n| `--attempt <id>` | \u2014 | `restore-attempt`: exact stale pre-commit attempt; `restore-fallback`: matching healthy committed restore |\n\nOne configurable cleanup verb; every target requires `--force`.\n\n- `history` purges the retained message backlog on the **running** broker (channels, plus DMs\n with `--dms`). The same operation as [`history clear`](#history), which stays as an alias.\n- `store` deletes the **stopped** mesh's JetStream store (`.cotal/nats`): streams, durable\n consumers, and messages. This is the reset for stale on-disk broker state, e.g. durables\n minted by an older, incompatible Cotal generation surviving a `down`/`up` cycle.\n- `all` is `store` plus the space identity (`.cotal/auth`), the local creds and markers tied to\n it, any crash residue a normal `down` would have swept (stale pidfiles, `run/`), and the mesh's\n registry entry; the next `cotal up` mints a fresh identity.\n\n`history` needs the mesh up; `store` and `all` refuse while any recorded mesh process is still\nalive or any same-root recorded broker endpoint remains reachable (run `cotal down` first). They\nalso refuse outright on a root that holds accounts for several spaces: the store and the broker\ntrust record are shared by every space on the broker, so both targets would take out all of them\nand no `--space` can narrow that. `down`, `backup` and `up --restore` refuse there for the same\nreason. `cotal status` lists the tenants on such a root. Personas\n(`.cotal/agents`) and logs are never touched. A custom\nstore location is not recorded anywhere, so `--store-dir` must repeat whatever the mesh was\nlaunched with. Custom cleanup targets must contain either the Cotal store-generation marker or a\nreal `jetstream/` store directory; filesystem roots, project roots, and Cotal auth/maintenance trees\nare always refused.\n\n`store` and `all` also refuse every maintenance journal state. After a healthy committed restore,\n`restore-fallback` is the only supported way to remove the recorded unchanged old-store inode; it\nnever deletes the active target, requires both the exact attempt id and `--force`, and retires the\ncompleted restore journal so a later `down --preserve-state` can start a new backup cycle.\n\n## backup and restore\n\n```bash\ncotal down --preserve-state [--store-dir <dir>]\ncotal backup create <dir> [--only full|registry] [--store-dir <dir>]\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\n```\n\nBackup is offline-only. It requires the stable `ready` record from `down --preserve-state`, an exact\nstore match, no live recorded process, and an unreachable exact endpoint from the recorded cut.\nThat endpoint is probed immediately before cloning, so a live broker with a missing or stale pidfile\nis still refused. It claims the cut, reflink/copies the stopped source to a\nprivate attempt clone, and opens only that clone on a random loopback bootstrap broker with an\nindependent parent/deadline watchdog. It validates the canonical stream and pull-consumer inventory,\nwrites native snapshots with consumers excluded, and stores conservative contiguous ACK-floor\ncheckpoints separately. The original store is never opened by the backup broker, and the stack is\nnot restarted implicitly. Artifact destinations must not overlap the preserved source or maintenance\nattempt tree. Restore artifacts and targets likewise cannot nest inside or contain each other, the\npreserved source, or the maintenance attempt tree.\n\n`full` is the default and indivisible: channel registry, CHAT/DM/TASK/INBOX/DLV, ACL, MEMBERS, and\nvalidated durable checkpoints. `registry` is the sole partial artifact. Presence, derived membership\nfeed, leases, native ephemeral/history consumers, credentials, keys, tokens, owner secrets, and actor\nledger files are excluded. Artifacts are exclusively created `0700`; snapshot/checkpoint files and\nthe manifest are `0600`; `manifest.json` is written last with exact sizes and SHA-256 values. The\ndirectory is trusted operator input: hashes detect corruption, not malicious rewriting.\n\nRestore validates and stages the exact allowlisted artifact bytes before moving or creating a store.\nIt requires the same space and existing trust state. The whole pre-commit window holds a journaled\nliveness claim (coordinator, watchdogs, brokers, absolute deadline): ordinary `up` and a repeated\n`up --restore` refuse while the claim is live, and a stale attempt is recovered only after the\ndeadline has elapsed and every recorded owner is proven dead \u2014 automatically by a retried\n`up --restore`, or explicitly with `cotal clean restore-attempt --attempt <id> --force`. Nothing\never rolls back a live attempt. A registry-only artifact restores as registry-only whether or not\n`--restore-only registry` is passed; omitted infrastructure is always created and the exact\npost-restore stream inventory is asserted before commit intent. Ordinary `up` from a preserved cut\nresumes only the exact recorded source store and runtime; a contradicting `--store-dir` or\n`--runtime` fails in preflight. Authenticated restores validate the complete\nspace trust bundle before staging, including nkeys, seed matches, JWTs, signers, and space binding;\nfull restores commit to the validated operator, system-account, data-account, and active-signer root\nchain in addition to the static/user authority fingerprint. Because the system account is part of that\ncommitment, a [`cotal up --rotate-sys`](#up) makes every full artifact taken before it unrestorable\nagainst this root: take a fresh full backup after each rotation. The composed commitment is revalidated\nimmediately before store mutation and never includes secret seeds. Restore never creates fresh auth.\nSame-path restores atomically retain the old\nsource at the journaled fallback path; alternate targets retain it in place; a missing canonical\nsource needs explicit `--accept-missing-source`. Quarantine and target restores use current canonical\nconfigs on isolated random-loopback brokers, never expose native snapshot consumers, and publish a\ncommit-intent immediately before the normal listener starts. Archive bytes never instantiate the real\ntarget: after quarantine validation, every stream is re-snapshotted from the validated quarantine\nstate into attempt-owned sanitized files, and the target is restored solely from those. Before that boundary, failure rolls back\nthe attempt-owned target; after it, ambiguity preserves both stores and records forward-repair\nrecourse. The cooperative maintenance lock excludes Cotal commands, not arbitrary raw NATS processes.\n\nBootstrap brokers in every auth mode \u2014 including open \u2014 mount the store under a local account with\nrandom operation-specific logins only, each carrying the exact per-phase subject permission matrix;\nnormal static credentials and user-auth sentinel/bearer connections are rejected, and no auth\nservice or callout starts. Open mode differs only in its account label, never in authority. Inventory, each stream snapshot,\nrestore initiation, exact upload id, validation, and each checkpoint recreation use separate exact\nauthorities. Every checkpoint carries the source stream's message/first/last sequence state and must\nmatch its snapshot record before mutation; core then derives and validates the only allowed start\npolicy. TASK is not a CLI exception: the same core checkpoint API recreates its canonical `DeliverAll`\nWorkQueue durable because acknowledged tasks are absent from retention and NATS forbids a\nstart-sequence policy there. Registry-only restore creates every omitted canonical stream and transient\nbucket on the isolated target before the normal listener is exposed. It deliberately does not resume\nretained agents or recreate their DM/DLV/TASK/ACL state; their identity material stays retained and\nstopped rather than being reprovisioned into a partial restore.\n\nAfter listener readiness, the manager starts attempt-bound, validates retained credentials/tokens\nwithout granting or reprovisioning, and resumes the exact persisted principals under cleanup\nsuppression. Registry-only restore uses the same flow with an empty agent set. `commitResume` is an\nidempotent validation barrier only: success must be `awaitingFinalize` with an attempt-bound 64-hex\ncommit token and does not release suppression. Under the workspace lock, the CLI first fsyncs that\nexact evidence as `manager-committed` (restore) or `resume-committed` (ordinary resume), then calls\ntoken-bound `finalizeResume`; only an `active` response for the exact token releases suppression. The\nCLI records the same token in finalization evidence before a restore becomes `active`, or before an\nordinary resume retires and consumes the marker. Re-entry from either committed state skips the prior\nidempotent activation/commit phases, retries finalization with the durable token, and finishes the\nworkspace transition. Failure before finalization preserves the committed state and cleanup\nsuppression; it is not rewritten through a degraded transition. Re-entry between any two earlier\nboundaries reuses the same attempt and may retry the idempotent phases without deleting retained state. A missing or\nchanged per-agent dependency is a named fail-closed result; the journal becomes degraded and remains\navailable for forward repair. A retry from `resume-intent`,\n`resume-active`, or `resume-degraded` reuses the same attempt and inventory after the prior listener is\nproven stopped. Every normal restore listener has an unguessable attempt-bound NATS server name. The\nCLI fsyncs its exact name/nonce, canonical endpoint, process owner, and generation-bound target identity\nimmediately after spawn. Re-entry accepts a surviving listener only when its INFO server name, live PID\nrecord, endpoint, and target identity all match that proof; degraded restore repair then moves through\nthe guarded workspace transition only after manager commit. If an uncommitted bound owner is provably\ndead, recovery retires that exact proof under the maintenance lock and binds a fresh listener for the\nsame attempt, endpoint, and target with a new nonce and server name. A live foreign/mismatched listener\nor ambiguous owner is preserved and refused, never adopted by reachability alone. A reconstructed\ncommit/degraded attempt without either the exact bound proof or a durable dead-listener replacement\nrecord fails closed even when the recorded port is free. A later ordinary startup may pass an `active`\nrestore only when its details prove manager commit and its exact recorded listener is dead.\n\n## meshes, use, status\n\n```bash\ncotal meshes\ncotal meshes add # guided, on a terminal\ncotal meshes add <space> --server <url> [--root <dir>] [--mode auth|open] [--force]\ncotal meshes rm <space> [<space> \u2026] [--force]\ncotal use <space>\ncotal status [--space <s>] [--server <url>]\n```\n\n`meshes` lists the meshes this machine knows; a `*` marks the `current` default a bare\n`cotal spawn` joins.\n\nRun on a terminal with the space or `--server` missing, **`meshes add` is guided**: it asks for the\none thing that cannot be derived (the broker URL), probes it, and tells you what answered - open or\nrequiring credentials. It then offers the spaces your `--root` already holds credentials for, states\nthe mode as a fact about that broker rather than asking, and shows the exact record before writing\nanything. A broker that does not answer, or a space name already registered, becomes a choice rather\nthan an error. Anything you pass on the command line is taken as given and not asked again. Without\na terminal - a script, an agent, CI - nothing prompts and the flag form's errors stand\n(`COTAL_NO_PROMPT=1` forces that too).\n\n`cotal up` and `cotal down` maintain their own records. `meshes add` registers a mesh they cannot\nspeak for: one running on another machine, a shared broker, a hosted space. `--root` is the folder\nwhose `.cotal/auth` holds that mesh's credentials and whose `.cotal/agents` holds its personas\n(default: the project you run it in) \u2014 the registry stores that path, never a secret. `--mode`\ndefaults to `auth` when the root holds the space's account record and to `open` otherwise; a\nuser-auth space cannot be registered by hand, because its IdP pins are trust that only\n`cotal up --user-auth` establishes. The broker is probed before anything is recorded, so a wrong\naddress, or credentials that mesh will not accept, fails here instead of at the first `spawn`;\n`--force` records without verifying (and replaces an existing record).\n\n`meshes rm` drops records \u2014 it never stops a mesh. For a mesh running on this machine `cotal down`\nis the right verb, and `rm` says so unless you pass `--force`. A record you added by hand is only\nremoved by something that names it \u2014 `meshes rm`, or an `add --force` replacement \u2014 or by a\n`cotal up` that actually starts the broker for that same space, server and root, which becomes that\nmesh and so takes the record over (a `cotal up` for that space anywhere else refuses instead).\nNothing that merely *infers* a record is stale touches it: an\nunreachable broker is listed `offline` and stays, and `cotal down` / `cotal clean all` leave it\nalone even when it shares a root with the project they are tearing down, because nothing on this\nmachine could write it back.\n\n`use <space>` sets that default; the selection applies from every directory,\nincluding inside another mesh's project. `status` is a read-only report: machine prerequisites\n(starting with the installed `cotal-ai` version), the installed extensions and their versions, this\nfolder's `.cotal/`, the recorded meshes, and a live snapshot of the selected mesh (roster, channels,\nmembership feed). `status` takes only `--space` / `--server` to pick the mesh to inspect; it starts\nnothing.\n\n## spawn\n\n```bash\ncotal spawn [<persona>] [--detach] [--name <n>] [--agent <a>] [--model <m>] [--variant <v>] [--prompt <text>] [--cwd <dir>]\ncotal spawn -f <cotal.yaml> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | resolved mesh | Target space |\n| `--server <url>` | registry entry | Broker URL override |\n| `--creds <path>` | \u2014 | Control-caller creds for an off-registry manager (`--detach` only) |\n| `--name <n>` | persona's `name:` | Presence-name override (does not choose the persona) |\n| `--config <persona-or-path>` | \u2014 | Persona catalog name or file path; wins over the positional |\n| `--agent <a>` | `COTAL_DEFAULT_AGENT`, else `claude` | Connector type (`claude`, `opencode`, `hermes`, \u2026) |\n| `--role <r>` | persona's `role:` | Role override |\n| `--model <m>` | persona's `model:` | Model override |\n| `--variant <v>` | persona's `variant:` | Model variant override (connector-defined; e.g. OpenCode reasoning tiers) |\n| `--cwd <dir>` | this cwd | Working directory to root the agent at |\n| `--prompt <text>` | \u2014 | Initial prompt auto-submitted at start |\n| `--resume <id>` | \u2014 | Fork an existing session id into the mesh (claude only) |\n| `--events` / `--no-events` | off | Publish the session's structured event plane to its own event channel |\n| `--share-tools <sel>` | none | Share named operator MCP servers with the agent |\n| `--subscribe <a,b>` | persona's | Channel read-set override |\n| `--allow-subscribe <a,b>` | = subscribe | Read-ACL override |\n| `--allow-publish <a,b>` | deny | Post-ACL override |\n| `--detach`, `-d` | off | Launch via the manager into a detached PTY (reattach with `cotal attach`) |\n| `--on <instance>` | class anycast | With `--detach` only: pin the launch to one manager instance id (the whole id, as `ps` prints it). Refused on a foreground spawn (no manager to pin), with `-f` (a manifest deploy launches through the manager class queue), and when empty |\n| `--file <cotal.yaml>`, `-f` | \u2014 | Deploy a manifest onto the running mesh |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing |\n| `--allow-stale <a,b>` | \u2014 | With `-f`: waive named stale agents (apply-only) |\n| `--runtime <name>` | manifest's | With `-f`: override the manifest's runtime |\n\n`--events` turns on the session's **event plane**: a stream of structured events describing what\nthe agent did, rather than the prose it wrote, on a channel of its own. The channel is named after\nthe agent's principal, `events.<owner>.<actor>`, never after its display name, because two live\nagents are allowed to share a display name and would then share a stream. The launch grants publish\nrights on exactly that one channel, foreground and detached alike, and a connector that does not\npublish an event plane refuses the flag rather than starting a session whose events have nowhere to\ngo.\n\nThe flag and the grant are separate on purpose. Holding publish rights on a channel is not a request\nto publish to it, so writing an event channel into an agent file's `allowPublish` does not turn the\nplane on: only the launch does.\n\nThe persona (`--config` > positional > `COTAL_DEFAULT_PERSONA` > `default`) is loaded from the\ntarget mesh's `.cotal/agents/`; the launch flags override the file. Foreground runs the agent\nattached to your terminal; `--detach` hands the launch to the running manager. Both modes get the\ndurable backstop on a mesh that runs the delivery daemon; `--live-only` skips it for a foreground\nspawn (messages posted while it is disconnected are then not replayed). A foreground exit retires\nthe agent's creds and broker footprint, like a manager despawn. A `--detach` spawn is an\n**action**: the manager accepts it and returns the allocated identity at once, then the launch\nfollows to a terminal outcome rather than blocking (see [the control surface](control-surface.md)).\nSee [Connect Claude Code](connect-claude.md) and [Agent files](agent-files.md); `-f` is a\n[manifest deploy](#manifest-deploys). (`cotal start` was merged into `cotal spawn --detach`.)\n\n## models\n\n```bash\ncotal models [--agent <connector>] [--refresh]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--agent <connector>` | all registered connectors | Connector whose catalog to list |\n| `--refresh` | off | Ask the connector to refresh its provider cache |\n\nAsks the running manager for each connector's model catalog (model ids plus their variants)\nfor connectors that expose one (OpenCode today; a connector without a catalog says so). Pick a\nresult with `cotal spawn --model <provider/model> --variant <v>`.\n\n## endpoints\n\n```bash\ncotal endpoints [--space <s>] [--server <url>] [--creds <path>]\n```\n\nLists the mesh presence roster: agents, the manager, and any other protocol endpoint, with each\nendpoint's role, kind, status, and current activity. Unlike `ps`, this is a read-only presence view;\nit is not limited to child processes owned by the manager.\n\n## describe, invoke\n\n```bash\ncotal describe <endpoint> [--space <s>]\ncotal invoke <endpoint> <command> [--args '<json>'] [--space <s>]\ncotal invoke <endpoint> <command> --name <agent> [--admin] [--space <s>]\n```\n\nThe generic v0.4 service surface. `describe` resolves a registered endpoint's command set off the\nwire - the reserved `describe` command answers the registered contract digests, the schemas are\nfetched from the space's content-addressed contract store, recompiled, and verified against those\ndigests - and prints each command with its capability class and targeting shape. `invoke` calls one\ncommand by name: `--args` is a JSON object validated against the fetched input schema *before*\npublish; a targeted command takes `--name <agent>` (resolved to the agent's current principal via\n`ps`) or `--self`. `--admin` uses the admin instrument credential, whose cross-agent reach rides\nthe operator-only `any` authorization mode. Neither command has compile-time knowledge of any\nendpoint's schemas - this is the same trust chain every built-in control command now uses. Needs an\nauth mesh: the manager registers its service on both static and per-user meshes (a signed-in user\nrides their bearer; cross-agent reach needs the `admin` scope). An open mesh has no service\nregistry.\n\n## ps, stop, attach\n\n```bash\ncotal ps [--on <instance>] [--space <s>]\ncotal stop --name <n> [--on <instance>] [--space <s>]\ncotal attach --name <n> [--on <instance>] [--no-reconnect] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | \u2014 | Managed agent to stop / attach (required) |\n| `--on <instance>` | class anycast (`ps`: class scatter) | Pin to one manager instance id (multi-manager space); takes the whole id as `ps` prints it, not a prefix. An empty value (`--on \"\"`, an unset shell variable) is refused, never treated as absent |\n| `--no-reconnect` (`attach`) | off | End the attach when its session ends, instead of re-establishing it. For scripts that want one run and one exit code |\n\nThese are operator clients over the running manager's control plane. `ps` prints two facts per\nmanaged agent, because they answer different questions: the process fact from the manager's own\nruntime handle (`running` with its uptime, or `exited` with how long it ran), and the mesh fact from\nthe roster (`idle` / `working` / `waiting` / `mesh offline`, or `not in roster` when the seat has no\npresence row at all: a seat that has not joined yet, or one that never did). A seat can be `running` and `mesh offline` at once: the process is alive and\nits presence has lapsed. On a user-auth mesh `ps` also renders each managed agent's last\ncredential-refresh outcome, fail-closed.\n\n**Mode split (chosen up front, never try-scatter-then-degrade):**\n\n- **Static / open mesh.** Bare `ps` is a **class scatter**: it freezes the live manager class from\n the records registry, merges every registered instance's agents grouped and attributed per\n instance, and a non-answering instance is shown as `registered, no answer within the deadline`\n (never silently omitted). That label is the whole claim: the instance is registered and did not\n answer. It does not say the host is down, because a dead host never deregisters itself and a\n live one can be slow; if it is gone, deregister it.\n `--on <instance>` pins the read to one exact instance id instead. A wrong pin fails loud\n rather than falling through: a well-formed id that no live manager carries is reported as\n `manager instance <id> did not answer` (nothing else is asked), and a credential without that\n instance's rail is reported as refused by the broker, not as an unresponsive manager. A manager\n that answers with a refusal is shown with its own cause; \"no manager reachable\" is said only when\n nothing answered at all. If the scatter's own registry read fails (the freeze or the reconcile),\n `ps` says the manager registry could not be read rather than pronouncing on the managers, which\n may all be up.\n\n**`stop` and `attach` route by seat locality.** A seat can only be stopped or attached by the\nmanager actually running it, and the class queue does not know which one that is. So on a\nstatic/open mesh both verbs first ask every registered instance which one hosts the named seat, then\naddress that instance directly. You do not need `--on` for this \u2014 it happens by default.\n\n`--on <instance>` remains the override, for when you already know where the seat lives or the\nlookup itself is degraded. It is also the **only** route on a **user-auth mesh**: a ledger-scoped\nbearer does not hold the registry-read rows the lookup needs, so there the verbs stay on the class\nqueue unless you pin them yourself.\n\nIf the seat is found on no reachable instance, the error says so \u2014 how many managers answered, and\nwhich ones did not \u2014 rather than reporting a bare `no agent <name>`. That distinction matters\nbecause a single manager cannot tell \"hosted elsewhere\" from \"does not exist\": it answers\n`not-found` for both.\n- **User-auth mesh.** `cotal ps` reports what **one** manager knows about your agents (an `ep.one`\n read against the manager's in-memory roster, owner-filtered). It does **not** report other\n manager instances, and it cannot tell you that one is down \u2014 an unreachable manager is absent\n from the list, not flagged. Completeness across a multi-manager user-auth space is not claimed.\n A manager that does not answer fails the command outright (exit non-zero), rather than printing\n an empty list that could be read as \"no agents\". Your ledger row needs the `admin` scope to\n reach `ps` at all; `spawn` alone is refused by the broker (the ep tier boundary).\n\n`attach` streams and drives an agent's terminal on the `pty` runtime; detach with the escape key\n(Ctrl-] by default; see [`COTAL_DETACH_KEY`](config.md)). It does so over a one-use, holder-bound\nmesh session ([SPEC](../SPEC.md) \xA713.6): the manager replies with a signed session grant (never a\n`127.0.0.1` URL), the CLI redeems it once over the broker, and the browser console (`cotal console`)\ndrives the same session. `stop` and `attach` need a running manager to talk to. On a static mesh\nthey are cross-agent admin operations. On a user-auth mesh, your own agents (any agent under your\nowner) need only the `spawn` scope; another owner's agent needs `admin` on your ledger row\n([identity & auth](identity-and-auth.md)). Launch detached agents with [`spawn --detach`](#spawn).\n\n**`attach` reconnects when the link dies.** A session lives on a network link, and a laptop that\nsleeps, a VPN that drops or a wifi handover kills it. When that happens `attach` prints\n`[cotal: connection lost, reconnecting]` on stderr and starts asking the manager for a new session:\na fresh grant, a fresh per-session credential, a fresh connection, so every attempt re-runs the same\nauthorization the first attach did. On success it prints `[cotal: reconnected]`, the manager repaints\nthe seat's current screen the way it does for any attach, and you carry on in the same terminal.\nRetries wait 1s, 2s, 5s, 10s, then 30s, for as long as the seat exists. The detach key is read\nduring those waits, so a reconnect never traps you; it is not read across the round trip that\nhands the old session back and opens the new one, so a press inside that window takes effect when\nthe round trip returns, within seconds.\n\nIt stops on its own when reconnecting cannot help, and says why: a manager that refuses the attach\nexits non-zero with the manager's own message, and a reconnect that finds the seat no longer there\n(despawned, or its agent exited while the link was down) exits cleanly with `seat <name> is gone`.\nA refusal that could still pass, such as a manager at its session ceiling, is relayed in the\nmanager's own words while the loop keeps trying, once per refusal rather than once per attempt.\nPressing the detach key, or the agent's process exiting while you are attached, ends the attach as\nit always did. `--no-reconnect` turns all of this off and restores the single-session behaviour,\nwhich is what a script wants.\n\nEach reconnect also hands the abandoned session back to the manager, over the first link that can\ncarry the message, so an attach that flaps does not eat the manager's session slots one outage at a\ntime. If that message never gets a link, the attach says so when it ends.\n\n`attach` streams over the manager's own HTTP/WS face rather than the mesh. That endpoint binds\n**loopback by default**, so nothing is exposed by accident; `cotal up --host <addr>` passes its bind\naddress down, which is what lets you attach to an agent whose manager runs on another machine. A\nbare `cotal supervise` and an embedded manager stay machine-local. Set it directly with\n`supervise --console-host <host>`.\n\nThat address is **recorded on the mesh** and carried forward, because it is a decision rather than\nsomething later commands can work out for themselves (a broker dial address is not a manager bind\naddress). Every later manager launch for the same mesh reuses it \u2014 a same-root `cotal up` repair,\nadopting a preserved or restored listener, a `spawn -f` manifest deploy \u2014 so a manager replacement\ndoes not quietly move a reachable attach face back to loopback. Passing `--host` again overrides it,\nso you can widen or narrow exposure whenever you like; a mesh that never asked stays loopback-only\nand records nothing.\n\nBecause that face carries terminal read and write for every managed agent, it is credentialed in two\ntiers. A mesh caller receives a **ticket** bound to the single agent the manager just authorized,\nsingle-use and short-lived, so one authorized attach can never be re-pointed at someone else's\nagent. The **console token** is the operator's own, reaches every agent, and is printed only to the\nmanager's output. The roster, the live feed, and the PTY stream all answer `401` without one; the\nstatic console shell is served openly, since it describes no agent.\n\n## input\n\n```bash\ncotal input --name <n> --text <text> [--no-enter] [--on <instance>] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | | Managed agent to type into (required) |\n| `--text <text>` | | The text to type, taken verbatim (required) |\n| `--no-enter` | off | Type the text and stop there, without pressing Enter |\n| `--on <instance>` | class anycast | Pin to one manager instance id, exactly as [`attach`](#ps-stop-attach) |\n\nTypes one line into a running agent's terminal, as if you had typed it there, and returns. This is\nthe half of [`attach`](#ps-stop-attach) that a program wants: `attach` is a live stream that holds a\nsession open and expects a terminal on your side, so a script, a cron job or a web UI cannot use it\nto send a single line. `input` is one authorized call.\n\nWhat it is for is **harness commands**. A line beginning with `/` is not chat and not a message: it\nis something the agent's own harness handles, and the only way in is the keyboard.\n\n```bash\ncotal input --name reviewer --text \"/compact\" # ask the harness to compact its context\ncotal input --name reviewer --text \"/model opus\" # switch its model\ncotal input --name reviewer --text \"hold on that PR\" # ordinary typing works too\n```\n\n**Quoting.** `--text` takes a value, so a payload starting with `/` survives as written. A payload\nstarting with a dash needs the `=` form, because the shell-style `--text --foo` is ambiguous and is\nrefused rather than guessed:\n\n```bash\ncotal input --name reviewer --text=--verbose # dash-leading text: use --text=<value>\n```\n\nEnter is pressed by default, since a command typed but never submitted has not been delivered.\n`--no-enter` types the text and leaves it sitting at the prompt, which is how you stage a line and\nsend it later.\n\nNothing comes back but a delivery receipt (`\u2713 sent 9 bytes to reviewer`, counting the trailing\ncarriage return). Whatever the agent does next shows up where its output already goes: the mesh, its\ntranscript, or an `attach`.\n\n**This one is operator-only, and more narrowly than `stop` or `attach`.** Those two are granted to\nanything holding `spawn`, so an agent can stop and attach to seats under its own owner. `input` is\nnot: it is granted only to operator credentials, which on a user-auth mesh means your ledger row\nneeds the `admin` scope, the same scope [`ps`](#ps-stop-attach) already needs there. The reason is\nthat a write into a terminal is control of whatever is running in it, and on a user-auth mesh the\nown-owner rule covers every seat under you, not only the ones you launched: a `spawn`-scoped agent\ncould otherwise type into a sibling it never started. Seat locality is still resolved for you.\n\nOnly the `pty` runtime can be typed into. The external terminal runtimes (`tmux`, `cmux`, `orca`,\n`herdr`) attach to a process they do not own, so they have no input stream for it and the command\nrefuses by name rather than dropping the keystroke.\n\n## personas\n\n```bash\ncotal personas list [-v] [--running]\ncotal personas show <name>\ncotal personas edit <name>\ncotal personas new <name> (--prompt <t> | --from <f>) [--role <r>] [--model <m>]\ncotal personas rm <name> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh's persona catalog |\n| `--role <r>` | \u2014 | `new`: the persona's role |\n| `--model <m>` | \u2014 | `new`: the persona's model |\n| `--prompt <t>` | \u2014 | `new`: the persona's prompt text |\n| `--from <f>` | \u2014 | `new`: seed the prompt from a file |\n| `--verbose`, `-v` | off | `list`: include role / model / description |\n| `--running` | off | `list`: mark personas live on the mesh |\n| `--force` | \u2014 | `rm`: required, delete without prompting |\n\nPersonas are the local agent files under `.cotal/agents/` that `cotal spawn` launches. See\n[Agent files](agent-files.md) for the file format.\n\n## supervise\n\n```bash\ncotal supervise [--runtime <name>] [--space <s>] [--server <url>] [--spawn <names>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space to supervise |\n| `--server <url>` | the local mesh | Broker URL |\n| `--runtime <name>` | `pty` | Agent runtime (`pty` built in; extension runtimes are explicit-only) |\n| `--console-port <n>` | \u2014 | Protocol-console port |\n| `--console-host <host>` | loopback | Bind host for the console + attach endpoint. Loopback keeps it machine-local; `cotal up` passes the address it bound the broker to, which is what lets `cotal attach` reach this manager from another machine |\n| `--roster <file>` | \u2014 | Declarative roster to boot at startup |\n| `--launch <spec>` | \u2014 | Resolved manifest launch spec (from `up -f` / `spawn -f`) |\n| `--spawn <names>` | \u2014 | Comma-separated personas to pre-spawn at startup |\n\nThe manager is the agent supervisor and control plane: it answers `spawn --detach`, `stop`, `ps`,\n`attach`, and the `cotal_*` manager tools. `cotal up --detach` starts one for you; run `supervise`\ndirectly to recover a dead manager or drive a custom runtime. Default runtime is `pty`; install an\noptional provider first (`cotal ext add @cotal-ai/orca`, `@cotal-ai/tmux`, `@cotal-ai/cmux`, or `@cotal-ai/herdr`) and\nselect it explicitly. A missing provider or app fails loudly; there is no fallback. See [Deploy](deploy.md).\n\n## reconcile-gate\n\n```bash\ncotal reconcile-gate [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the frozen gate lives in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint whose gate is frozen |\n| `--instance <id>` | this folder's persisted manager instance | Instance id |\n\n**When you need this.** A manager restart killed partway through \u2014 after it began deregistering,\nbefore the new incarnation finished \u2014 leaves the endpoint's issuance gate *frozen*, held by a\nprocess that no longer exists. The next manager start refuses to proceed, which is correct: the\nfreeze is what stops two incarnations serving at once. But nothing can lift it, so every restart\nfails the same way. `cotal doctor` shows the gate as frozen; the manager's own start logs name the\ngate it could not advance.\n\nThis command is the way out. It checks that the holder really is gone, prints what it found, and\nthen finishes the dead operation exactly as the interrupted restart would have: revoke the old\ncredentials, evict their holders with verification, and reopen the gate. Start the manager\nafterwards and its normal takeover runs end to end.\n\n**It refuses far more often than it acts, on purpose**, and always says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `holder-alive` | The freeze-holder still has a live connection \u2014 a manager *is* running | Stop that process first. Reconciling would evict a live manager's credentials |\n| `holder-unknown` | The connection sweep could not prove the holder absent | Not safe to proceed: an unprovable holder is treated as a live one. Re-run once the broker answers completely |\n| `liveness-unestablishable` | The delivery daemon could not be asked at all | Start it (`cotal up` runs it) and re-run. Silence is never read as death |\n| `not-frozen` / `no-gate` | The gate is open, or there is no gate at that coordinate | Nothing to repair \u2014 check `--endpoint` / `--instance` |\n| `wrong-op-kind` | Frozen under a takeover or retirement, not a registration | Out of scope for this command; it will not reinterpret another operation's intent |\n| `eviction-unverified` | The holder looked gone but eviction could not be verified | The gate is left frozen, unchanged. Investigate the broker before retrying |\n| `raced` | A newer manager moved the gate mid-repair | Re-run `cotal doctor` and look again |\n\nThere is no `--force`, and no path that discards gate state: the only way this reopens a gate is by\nproving the holder is gone and then completing the operation properly.\n\n## deregister-instance\n\n```bash\ncotal deregister-instance [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the instance is registered in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint the instance serves |\n| `--instance <id>` | this folder's persisted manager instance | Instance id, the whole id as `cotal ps` prints it |\n\n**When you need this.** The service registry records *registration*, not liveness, and nothing in\nthe model expires a row. A manager that stops cleanly removes its own registration. One whose host\ndied without writing anything cannot, so its record goes on claiming a live instance forever: every\nclass scatter in that space freezes the dead slot in, and `cotal ps`, `stop` and `attach` each pay\ntheir whole deadline waiting for a machine that is never coming back. A laptop that was reimaged, a\ncontainer that was deleted, a box that will not be back on the network: those registrations have no\nother exit.\n\nThis command is that exit. It asks the instance first, and it removes a record only when the broker\naffirms the instance's own rail is empty: nothing subscribed there. Then it deletes the\nregistration's two records keys, each pinned to the revision it read, and prints what it removed.\n\n**Silence alone never passes.** An unanswered describe is what a dead host, a wedged process and a\nslow one all look like, and a hung process still holds its subscriptions, so the broker sees\ninterest on its rail. That instance is refused and the observation is printed. A dead process holds\nno connection and therefore no subscription, so a real corpse is still removed.\n\n**It refuses rather than guesses**, and says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `instance-answered` | The instance answered a pinned describe. It is alive | Nothing to repair. If it is wedged rather than gone, stop the process first; its own clean stop removes the record |\n| `instance-not-affirmed-gone` | It did not answer, and the broker did not report its rail empty, which is what a held subscription looks like: slow or hung, not affirmed gone | Nothing was removed. Stop the process; its record goes on its own clean stop, or re-run this once it is down |\n| `liveness-unestablishable` | The probe itself failed, so nothing was learned | Fix the probe's path (credential, broker) and re-run. A probe that could not run is never read as death |\n| `not-registered` | No registration at that coordinate | Check `--instance` and `--endpoint`. This takes the whole id, never a prefix |\n| `superseded` | The record moved between the read and the delete | Something is writing to it. Nothing was removed; re-observe before retrying |\n\nThere is no `--force` and no sweep: silence is not death, and a rule that removed rows on silence\nwould eventually remove a live instance that was merely slow. An operator names one instance, the\nbroker's verdict on its rail is what authorizes the removal, and the guard's job is to show them\nthey named a dead one. Removal is not a one way door either. The same instance re-registers over\nthe tombstone on its next start, under the same identity.\n\n## runtimes\n\n```bash\ncotal runtimes\n```\n\nLists every agent runtime the manager can spawn through: the built-in `pty`, the official providers\n(`orca`, `tmux`, `cmux`, `herdr`), and any custom provider installed via `cotal ext add`. Each installed\nprovider is probed so you can see what is actually reachable on this machine before selecting it:\n\n```\npty built in\norca installed \xB7 reachable @cotal-ai/orca\ntmux available \xB7 cotal ext add @cotal-ai/tmux\ncmux available \xB7 cotal ext add @cotal-ai/cmux\nherdr available \xB7 cotal ext add @cotal-ai/herdr\n```\n\n`installed \xB7 reachable` / `unreachable` is the provider's own `available()` probe; `available` means\nit is a known runtime you can add with the shown command. Selecting an unknown or uninstalled runtime\nvia `up`/`spawn --runtime <name>` fails loud and, for a known one, points at the exact `cotal ext add`\npackage \u2014 there is no silent fallback to `pty`.\n\n## send\n\n```bash\ncotal send dm <agent> \"<text>\" [--space <s>] [--server <url>] [--creds <path>]\ncotal send msg <channel> \"<text>\"\ncotal send ask <role> \"<text>\"\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and (off-registry) which credential |\n\nOne-shot messaging: connect, send a single direct message (`dm`), channel post (`msg`), or role\nask/anycast (`ask`), then exit. For a running conversation, agents use the mesh tools instead\n([MCP tools](mcp-tools.md)).\n\n## channels\n\n```bash\ncotal channels list\ncotal channels set <name> [--replay | --no-replay] [--window <n>] [--desc <s>] [--instructions <s>]\ncotal channels default --replay | --no-replay\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--replay` / `--no-replay` | \u2014 | `set`/`default`: replay history to new joiners, or not |\n| `--window <n>` | \u2014 | `set`: replay window size |\n| `--desc <s>` | \u2014 | `set`: one-line channel description |\n| `--instructions <s>` | \u2014 | `set`: instructions shown to joiners |\n\nInspects and edits the channel registry: replay policy, description, and joiner instructions. ACL\nsemantics (who may read or post) are set at mint / provision time, not here; see\n[Channels and permissions](channels-and-permissions.md). On a user-auth mesh, `list` rides your\nown login as is; `set` and `default` edit the registry over a short-lived\nchannel-writer view, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n\n## history\n\n```bash\ncotal history clear --force [--dms] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--dms` | off | Also clear DM history |\n| `--force` | \u2014 | Required: clear without prompting |\n\nPurges retained channel history; `--dms` extends it to direct-message history. An alias of\n[`clean history`](#clean). On a user-auth mesh the purge rides a short-lived purger view over\nyour login, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n## console\n\n```bash\ncotal console [--plain] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to watch |\n| `--plain` | off | Line stream instead of the TUI |\n\nA live protocol view for a space: a lazygit-style TUI, or a plain line stream on `--plain`. On a\nuser-auth mesh it rides the read-only admin view over your login, which needs ledger scope\n`admin`. See [Watch a mesh](watch-a-mesh.md).\n\n## web\n\n```bash\ncotal web [--detach] [--port <n>] [--no-open] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to serve |\n| `--port <n>` | `7799` | HTTP port |\n| `--detach` | off | Run in the background; stop with `cotal down web` or bare `cotal down` |\n| `--no-open` | off | Don't open the browser |\n\nThe browser observability dashboard: presence, channels, and a live feed. It is **not** part of\n`cotal up`: it ships inside `cotal-ai` as the `@cotal-ai/web` extension, seeded automatically on first\nrun (like the built-in connectors) so it always matches your CLI version. It self-registers `cotal web`\ninto this surface and serves\n`http://cotal.localhost:7799` (loopback; `*.localhost` resolves in Chrome/Firefox/Edge; Safari may\nneed `http://127.0.0.1:7799`). On a user-auth mesh the dashboard rides the read-only admin view\nover your login, and a channel purge asks for its own channel-purger view per click; both need\nledger scope `admin`. Detached mode re-execs the current Cotal installation, writes diagnostics to\nthe mesh root's `.cotal/web.log`, and reports success only after the HTTP server answers. It requires\na recorded mesh root, but can be launched from any directory once `cotal up` has recorded the mesh.\nSee [Watch a mesh](watch-a-mesh.md).\n\n## mint\n\n```bash\ncotal mint <name> [--profile <agent|observer|admin>] [--out <path>] [--signer]\ncotal mint <name> --provision [--role <role>] [--space <s>] [--server <url>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--profile <agent\\|observer\\|admin>` | `agent` | Credential profile |\n| `--out <path>` | `.cotal/auth/creds/<name>.creds` | Output path |\n| `--signer` | off | Emit a stripped account-signing file instead |\n| `--force` | off | With `--signer`: overwrite an existing file |\n| `--allow-subscribe <a,b>` | the agent file's, else subscribe | Read-ACL override, **agent profile only**: `observer` and `admin` carry a fixed read set, and `mint` refuses this flag there rather than narrowing nothing |\n| `--allow-publish <a,b>` | the agent file's, else deny | Post-ACL override, **agent profile only** |\n| `--role <role>` | the agent file's | Agent profile: the anycast task queue the identity pulls (`svc_<role>`) |\n| `--provision` | off | Agent profile: also pre-create the identity's bind-only DM/deliver durables (and its role's task queue) on the live mesh, so the credential can consume |\n| `--space <s>`, `--server <url>` | the resolved mesh | With `--provision`: which mesh to provision on |\n\nMints a NATS creds file for a space in **static** auth mode, scoped to a profile and (optionally)\nexplicit read/post ACLs. `--signer` emits an account-signing file for delegating minting to another\nhost. A per-user-auth space refuses `mint`: agents there join under a logged-in user\n([`login`](#login-logout) + [`actor grant`](#actor)), never via a handed-out creds file. See\n[Identity and auth](identity-and-auth.md).\n\nA plain mint is creds only: the identity can publish within its post ACL at once, but on an authed\nmesh its DM inbox and task queue are provisioner-pre-created and bind-only, so a **consuming**\nconnect fails until they exist. `--provision` performs that pre-create in the same command (a\nprovisioner cred is minted from the space's trust material, used, and dropped), so a long-running\nclient you start yourself can receive DMs and role anycasts like a spawned seat. The command prints\nthe identity's principal (its wire id) and lifecycle uid; a consuming client passes that uid as its\n`lifecycleUid`. Agent profile only; an open mesh needs none of this (peers self-create there). The\nmesh it provisions on must be the one this folder's auth is for - same space and same account key -\nso `--provision` can never quietly mint under another root's trust material.\n\n## login, logout\n\n```bash\ncotal login --idp <auth base URL> [--client-id <id>]\ncotal logout --idp <auth base URL>\n```\n\nSigns you in to a per-user-auth mesh's IdP (device code flow) and caches the session; run it\nonce per machine. It prints your IdP subject, the id the operator grants against. After a\nlogin, every command on that mesh works under your identity: each connect takes a fresh IdP\nproof, exchanges it locally for a short-lived bearer, and is authorized against the actor\nledger at connect time. `logout` revokes the IdP session and clears the cache. See\n[identity & auth](identity-and-auth.md).\n\n## actor\n\n```bash\n# an upsert of the WHOLE row: a flag left off is the WIDE default below, not \"unchanged\"\ncotal actor grant <actor> --sub <IdP subject> [--scope a,b] [--allow-subscribe a,b] [--allow-publish a,b] [--role <r>] [--label <l>]\ncotal actor revoke <actor> (--sub <IdP subject> | --owner <u_\u2026>)\ncotal actor list\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | the folder's | Space whose ledger to manage |\n| `--sub <subject>` | \u2014 | The IdP subject (shown by `cotal login`) the actor belongs to |\n| `--owner <u_\u2026>` | \u2014 | The derived owner token (alternative to `--sub`) |\n| `--scope <a,b>` | `spawn,role:default` | Capability scope (`''` = none; `spawn` = may run agents, `role:<r>` = may delegate role r, `admin` = cross-agent control) |\n| `--allow-subscribe <a,b>` | `>` (all channels) | Channel read ACL; the user's envelope, their agents can never read beyond it |\n| `--allow-publish <a,b>` | `>` (all channels) | Channel post ACL; also the envelope for their agents' posting |\n| `--role <r>` | \u2014 | Role (scopes the task-queue consumer) |\n| `--label <l>` | \u2014 | Display label for `actor list` (never the IdP subject) |\n\nThe actor ledger is the single authorization source of a user-auth space: no row, no access.\nA bare `grant` is the **full** envelope (all channels, may spawn); the flags narrow it. A\nre-grant **replaces the whole row**, not the one field you name, so to add a capability spell\nevery field out: the new scope plus the row's current read set, post set, role and label\n(`cotal actor list` shows what a row holds). A field left off does not stay as it was, it\nreverts to the wide default in the table above, which is how a narrow reader becomes a reader\nof every channel. `revoke` denies the next exchange and the next connect with no restart, and\nevicts the principal's live connections. Managed-agent rows\n(written by the spawn path) live in a disjoint row space this command never touches. See\n[identity & auth](identity-and-auth.md).\n\n## doctor\n\n```bash\ncotal doctor auth [--fix]\n```\n\nCredential-health diagnosis and repair for this folder's mesh: renders every managed\ncredential as healthy / near-expiry / expired and ends in `healthy` or the exact next\ncommand; `--fix` applies the repairs it can. The one surface every stale-credential error\npoints at.\n\n## join\n\n```bash\ncotal join --space <s> --name <n> [--role <r>] [--channel <c>]\ncotal join --link <url> | --token <t>\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and which credential |\n| `--name <n>` | \u2014 | Your presence name |\n| `--role <r>` | \u2014 | Your role |\n| `--channel <c>` | \u2014 | Channel to join |\n| `--kind <k>` | `agent` | Endpoint kind |\n| `--link <url>` | \u2014 | Join link (`cotal://\u2026`) |\n| `--token <t>` | \u2014 | Join token |\n| `--lifecycle-uid <uid>` | \u2014 | Required with `--creds`: the lifecycle UID minted alongside the credential (`COTAL_LIFECYCLE_UID` works too). A credential's durable grants name exact lifecycle-keyed resources, so `join` refuses to invent one |\n| `--tls` | off | Connect over TLS |\n\nAn interactive presence: join a space under your own name and role, without launching an agent\nharness. A `--link` or `--token` supplies the where and the auth in one value. See\n[Spaces](spaces.md) and [Identity and auth](identity-and-auth.md).\n\n## Manifest deploys\n\nA `cotal.yaml` manifest declares a whole mesh (channels, personas, roles, and ACLs) in one file.\nThree commands consume it, plus a read-only validator:\n\n```bash\ncotal up -f cotal.yaml # boot a fresh mesh from the manifest\ncotal spawn -f cotal.yaml # deploy the manifest additively onto a running mesh\ncotal down -f cotal.yaml # tear that deploy down (or --run <id> for one run)\ncotal topology view -f cotal.yaml # validate + view the access graph, change nothing\n```\n\n`up -f` and `spawn -f` differ in target: `up -f` brings up a new broker and applies the manifest;\n`spawn -f` requires an already-reachable mesh and applies additively (ownership-scoped). On a\nuser-auth mesh, `spawn -f` deploys over your own login (the deployer view, gated on ledger scope\n`spawn`): the manifest's agents land under your owner, a manifest claiming another owner is\nrefused, and seeding new channels additionally needs scope `admin`. Both take\n`--dry-run` to print the plan without mutating anything. `topology` validates the manifest and\nrenders its channel / role / ACL graph. See [Define a team](define-a-team.md) and the\n[manifest reference](manifest.md).\n\n## ext\n\n```bash\ncotal ext # same as `list`\ncotal ext add <npm-package>\ncotal ext remove <name>\ncotal ext list\ncotal ext root # print just the install prefix (scriptable)\ncotal ext seed [--repair|--reset|--force]\n```\n\nOperator-installed extensions: `add` installs an npm package into a cotal-owned prefix and records\nevery registry provider it contributes. Commands appear in help, completion, and dispatch; runtime\nproviders are lazy-loaded by commands such as `supervise`; local process providers participate in\n`status` and selective `down`. `remove` and `list` manage them. The `@cotal-ai/web` dashboard is the\ncanonical command/process example. Installed packages and their location are described in\n[config](config.md).\n\nBare `cotal ext` lists the inventory, headed by the install prefix. That prefix is a cotal-owned npm\nroot kept **separate** from npm's own global tree, so these packages never show up in `npm list -g` \u2014\n`cotal ext` (or the Extensions section of `cotal status`) is the canonical inventory. `cotal ext root`\nprints only the path, for scripts. The versions shown are the manifest pin recorded at add time.\n\nRemoving an extension that owns a running local process is refused with the mesh root and its\n`cotal down <component>` command; stop it first so uninstalling the package never strands a process\nwhose lifecycle provider is gone.\n\n### Built-in connectors are seeded extensions\n\nThe first-party agent connectors (`claude`, `opencode`, `codex`, `hermes`, `pi`) are not compiled into\nthe binary. They are seeded on first run through the **same** `ext add` path a third party uses, and\nappear in `cotal ext list` like any other extension. So you can remove one you do not want\n(`cotal ext remove @cotal-ai/connector-hermes`), and a deliberately-removed connector STAYS removed\nacross upgrades. `cotal ext add <your-package>` adds a third-party connector the same way. The web\ndashboard (`@cotal-ai/web`, providing `command:web`) is a fifth built-in seeded on the same path.\n\n`cotal ext seed` is the maintenance entry for that seeding (it runs automatically on the first real\ncommand of each boot, so you rarely call it):\n\n| Flag | Meaning |\n|---|---|\n| (none) | Reconcile: seed any never-seeded built-in, refresh a seeded one whose version the binary bumped, leave a removed one removed. A no-op once current. |\n| `--repair` | Recover after an interrupted seed or a lost authority (rebuilds the interrupted connector; restores the removed-vs-never-seeded record from its durable backup). |\n| `--reset` | Discard the record and re-seed all six built-ins (the five connectors plus the web dashboard). **Resurrects any you removed.** Rebuilds cleanly over corrupt seed state. |\n| `--force` | Re-seed the built-ins even when the version stamp is current or a downgrade. |\n\nThe default connector for a bare `cotal spawn` (no `--agent`) is `claude`; set `COTAL_DEFAULT_AGENT`\n(e.g. `opencode`) to change it. An `--agent` naming a removed connector fails loud with the exact\n`cotal ext add` to restore it. Set `COTAL_SKIP_CONNECTOR_SEED=1` to turn off the automatic first-run\nseed/refresh entirely (for a controlled or offline setup that manages connectors by hand); `cotal ext\nseed` still runs on request.\n\n## completion\n\n```bash\ncotal completion <bash|zsh|fish|powershell> # print a stub to eval / source\ncotal completion install [shell] # install it persistently\n```\n\nPrints or installs shell completion. Completion candidates come from each command's declared flags\nand, where useful, live mesh state (spaces, personas, managed agents) resolved offline.\n\n## feedback\n\n```bash\ncotal feedback \"<summary>\" [--type <t>] [--email <e>] [--details <text>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--type <t>` | \u2014 | `bug` \\| `idea` \\| `friction` \\| `praise` \\| `other` |\n| `--details <text>` | \u2014 | Longer free-form details |\n| `--severity <s>` | \u2014 | `low` \\| `medium` \\| `high` |\n| `--area <a>` | \u2014 | The part of Cotal this concerns |\n| `--email <e>` | git email | Contact email (required on the keyless public path) |\n| `--name <n>` | \u2014 | Your name (optional) |\n| `--url <url>` | keyed / public intake | Intake URL override |\n| `--key <k>` | `COTAL_FEEDBACK_KEY` | Feedback key |\n\nSends feedback to the Cotal developers. With a key (`--key` / `COTAL_FEEDBACK_KEY`) it routes to the\nkeyed beta intake; without one it goes to the public `cotal.ai` intake and requires a contact email\n(`--email` / `COTAL_FEEDBACK_EMAIL`, else your git email). Run a self-hosted intake with\n[`feedback-intake`](#server-daemons).\n\n## Server daemons\n\nTwo long-lived infra roles ship with the CLI. They are not part of everyday operation; the delivery\ndaemon comes up automatically with `cotal up --detach` in auth mode.\n\n```bash\ncotal deliver --space <s> [--server <url>] [--creds <file>]\ncotal auth-service --space <s> --server <url> [--port <n>]\ncotal feedback-intake --keys <keys.json> [--port <n>] [--creds <file>]\n```\n\n`auth-service` runs a user-auth space's identity plane (the NATS auth callout plus the\nloopback token exchange and JWKS); `cotal up --user-auth` starts and supervises it for you,\nso you run it directly only to recover one by hand.\n\n`deliver` runs the server-side Plane-3 delivery daemon: the durable backstop and membership/ACL\nauthority. It is auth-mode-only and single-instance (`--shard`/`--shards` accept only `N=1`);\n`--dev-mint` mints a scoped cred from the local signer for standalone dev. See the\n[delivery daemon](delivery-daemon.md). `feedback-intake` runs a self-hosted feedback server\n(requires `--keys` and a scoped `--creds`), announcing submissions into a space channel; flags\ninclude `--host`/`--port`, `--store`, `--space`/`--channel`, `--max-bytes`, and `--rate-limit`.\n\n## Plumbing\n\n`cotal __complete <words\u2026>` is the internal entry the shell-completion stubs call to emit candidates\nfor the current command line; you never run it directly. `cotal agent-bearer` is machine-facing\nplumbing on user-auth meshes: spawned agents exec it to print a fresh short-lived bearer from their\nspawn-time secret; you never run it directly either. (`cotal start` is a removed tombstone: it\nerrors and points you to `cotal spawn --detach`.)\n"
65437
67275
  },
65438
67276
  {
65439
67277
  "slug": "config",
65440
67278
  "title": "Configuration & environment",
65441
67279
  "kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract.",
65442
67280
  "summary": "Three things configure a Cotal workstation: the config file (per-connector settings, notably which of your MCP servers get shared with spawned agents), a set of COTAL environment variables, and the\u2026",
65443
- "body": '# Configuration & environment\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\nThree things configure a Cotal workstation: the **config file** (per-connector settings, notably\nwhich of your MCP servers get shared with spawned agents), a set of **`COTAL_*` environment\nvariables**, and the **on-disk layout** under a project\'s `.cotal/` and your machine\'s `~/.cotal`.\nNone of these are part of the wire contract; they configure the reference implementation only.\n\n## The config file\n\nThe cotal config file carries per-connector launch settings. It is layered from two locations,\nmost-specific-wins:\n\n| Layer | Path | Scope |\n|---|---|---|\n| Base | `$XDG_CONFIG_HOME/cotal/config.json` (else `~/.config/cotal/config.json`; `%APPDATA%\\Cotal\\config.json` on Windows) | Operator-level, every space |\n| Override | `<project-root>/.cotal/config.json` | Space-local |\n\nThey merge per connector and per server name: a server in the space-local file replaces the\nsame-named server in the operator-level file; connectors or servers present in only one side are\nkept. A missing file is empty (valid); malformed JSON or a non-object top level is a loud error.\n\nToday it carries one thing: which of your personal MCP servers a connector should **share** with the\nagents it spawns. By default a spawned agent gets none: the Claude connector launches with\n`--strict-mcp-config`, dropping every ambient MCP server (they are heavy and useless to a meshed\nteammate). This file is the explicit opt-in.\n\n```json\n{\n "connectors": {\n "claude": {\n "mcpServers": {\n "github": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-github"],\n "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }\n }\n }\n }\n }\n}\n```\n\nEach server is written in the de-facto `.mcp.json` shape, so you can copy an entry straight out of\nyour own Claude / VS Code / Cursor config. Secrets ride as **`${VAR}` references** (also\n`${VAR:-default}`), resolved from your environment at launch and forwarded to the child **by name**\n(never as literals) so the file stays safe to keep in `~/.config` or a gitignored `.cotal/`. Only\n`command`, `args`, `env`, `url`, and `headers` are expanded; any other key passes through verbatim.\n\n**`--share-tools` interplay**. The per-spawn selection narrows what this config declares:\n\n| `--share-tools` | Result |\n|---|---|\n| (flag absent) | Every server declared for the connector |\n| `none` or empty | Nothing |\n| `a,b` | Only those named: each **must** be declared, or the spawn fails (no silent drop) |\n\nToday only the `claude` connector consumes shared MCP servers; OpenCode inherits config through its\nown merge layer and Hermes has no MCP. See [Connect Claude Code](connect-claude.md) for the full\nsharing model.\n\n## Environment variables\n\nThese are the operator-facing variables. Most of the connector-session ones (space, name, role, \u2026)\nare set **for you** by `cotal spawn` / the manager when they launch an agent; you set them by hand\nonly when you drive a connector session yourself (e.g. your own `claude` with the plugin) or a custom\nlauncher. Comma-separated lists are trimmed.\n\n| Variable | Consumed by | Meaning | Default |\n|---|---|---|---|\n| `COTAL_SPACE` | connector session | Space to join | `demo` (or the join link\'s) |\n| `COTAL_NAME` | connector session | Presence name / identity | required (or via `COTAL_AGENT_FILE` / `COTAL_LINK`) |\n| `COTAL_ROLE` | connector session | Role | agent file\'s `role:`, else none |\n| `COTAL_SERVERS` | connector session | Broker URL(s) | the default local broker (or the link\'s) |\n| `COTAL_CREDS` | connector session | Path to a NATS creds file (auth mode) | none (open mode) |\n| `COTAL_LINK` | connector session | `cotal://token@host/space` join link: supplies server, auth, space | none |\n| `COTAL_AGENT_FILE` | connector session | Path to a persona file: supplies name, role, kind, channels | none |\n| `COTAL_SUBSCRIBE` | connector session | Active channel read set | agent file / link, else `general` |\n| `COTAL_ALLOW_SUBSCRIBE` | connector session | Read ACL (channels the agent *may* read) | = `COTAL_SUBSCRIBE` |\n| `COTAL_ALLOW_PUBLISH` | connector session | Post ACL (channels the agent *may* post to) | deny (empty) |\n| `COTAL_MODEL` | connector session | Model label (display metadata) | agent file\'s `model:`, else none |\n| `COTAL_KIND` | connector session | Endpoint kind | `agent` |\n| `COTAL_TLS` | connector session | Connect over TLS (`1`) | off |\n| `COTAL_TOKEN` | connector session | Auth token (token / open modes) | none |\n| `COTAL_CAPABILITIES` | connector session | Control-plane capabilities (e.g. `spawn`) that gate manager tools | agent file\'s `capabilities:` |\n| `COTAL_QUIET` / `COTAL_MUTED` | connector session | Per-channel attention defaults (never-wake / drop-on-receive) | agent file\'s, else none |\n| `COTAL_CHANNEL` | Claude connector | Force channel wake-nudges on (`1`) / off; set to `1` by the Claude launcher | auto-detect |\n| `COTAL_TRANSCRIPT` | connector session | Mirror this session\'s transcript to `tr-<name>` (`1`) | off |\n| `COTAL_TRANSCRIPT_DEFAULT` | manager | Default transcript-mirror for managed spawns (`1`) | off |\n| `COTAL_EVENTS_DEFAULT` | manager | Default AG-UI event plane for managed spawns (`1`) | off |\n| `COTAL_DEFAULT_AGENT` | `cotal spawn` | Default connector type for a bare spawn | `claude` |\n| `COTAL_DEFAULT_PERSONA` | `cotal spawn` | Default persona for a bare spawn | `default` |\n| `COTAL_SKIP_CONNECTOR_SEED` | boot gate | Skip the automatic built-in-connector seed/refresh on a command (`1`); `cotal ext seed` still works | off |\n| `COTAL_DETACH_KEY` | `cotal attach` | Detach escape key (`ctrl-<char>` / `^<char>`) | `ctrl-]` |\n| `COTAL_FEEDBACK_KEY` | `feedback`, connector | Beta feedback key \u2192 keyed intake | none (public intake) |\n| `COTAL_FEEDBACK_EMAIL` | `feedback`, connector | Contact email for the keyless public intake | your git email |\n| `COTAL_FEEDBACK_URL` | `feedback`, connector | Intake URL override (self-hosted) | keyed / public intake |\n| `COTAL_SKIP_ASSIST` | `setup` | Disable the interactive Claude handoff on a failed step (`1`; for CI) | off |\n| `COTAL_COMPLETE_DEBUG` | `completion` | Print completion-resolution errors to stderr | off |\n| `COTAL_SERVE_HEADLESS` | OpenCode runtime | Run the OpenCode server without a foreground TUI (`1`) | off |\n| `COTAL_HOME` | workspace | Override the machine-home dir for the **mesh registry only** (`meshes/`, `current-mesh`, onboard marker). Does **not** redirect project-root paths (`findCotalRoot` / `.cotal/broker-policy.json`, NATS store, manager/delivery state, auth). Tests that run `cotal up` must also use a temp project root with its own `.cotal/` as `cwd` | `~/.cotal` |\n\n> `--console-port` is a `cotal supervise` flag, not an environment variable; there is no\n> `COTAL_CONSOLE_PORT`.\n\n### Set by the launcher, not by you\n\nThese are wired into a spawned child\'s environment by the connector / launcher and read back inside\nthe session. They are not operator knobs; listed so you recognize them in a process listing.\n\n| Variable | Purpose |\n|---|---|\n| `COTAL_ID` | Stable agent id chosen by the launcher (static meshes) |\n| `COTAL_LIFECYCLE_UID` | The incarnation\'s lifecycle UID, minted once per spawn; the session binds its lifecycle-keyed DM/delivery/history consumers by it (its credential pins the same names). Required for an authed launch (`COTAL_CREDS` or user-mode); config parsing fails loud without it. Open mode omits it (the endpoint self-mints per session) |\n| `COTAL_OWNER` / `COTAL_ACTOR` / `COTAL_SENTINEL_CREDS` / `COTAL_BEARER_CMD` | User-auth launch identity: the agent\'s principal, its sentinel creds path, and the exec-able bearer command; all four together, mutually exclusive with `COTAL_CREDS` |\n| `COTAL_CONTROL_SOCKET` / `COTAL_CONTROL_TOKEN` | The session\'s local control endpoint (path + token) the MCP server listens on and the lifecycle hooks connect to; token is env-only, never argv or logs |\n| `COTAL_BRIDGE_SOCKET` / `COTAL_TOOLS_FILE` / `COTAL_PARENT_PID` | Hermes sidecar plumbing (bridge socket, generated tool descriptors, launcher pid to watch) |\n| `OPENCODE_CONFIG_CONTENT` | Inline OpenCode config (the injected cotal plugin, highest merge layer) |\n| `OPENCODE_DB` / `OPENCODE_HOME` / `OPENCODE_PORT` / `OPENCODE_SERVER_URL` / `COTAL_OPENCODE_*` | OpenCode server plumbing (home, port, DB, server URL) |\n\nThe launcher forwards only a fixed OS allow-list (PATH, HOME, TERM, locale, XDG/Windows config dirs,\n\u2026) plus the named model-provider key and any `${VAR}` secrets a shared MCP server references, never\nyour whole environment, so unrelated secrets don\'t bleed into spawned agents. There are also a few\ninternal timing knobs (e.g. `COTAL_MEMBERSHIP_INTERVAL_MS`, `COTAL_DELIVERY_BROKER_GONE_MS`) that you\nshould not set in normal operation.\n\n## On-disk layout\n\n### Project: `.cotal/`\n\nA project\'s state lives in `.cotal/` at the mesh root (found by walking up from the cwd, like `.git`).\n**It is gitignored**; it holds secrets and machine-local process state.\n\n| Path | What it is |\n|---|---|\n| `auth/broker.json` | Broker trust material: the operator seed and the system account (secret; the system-account signing seed is stripped before writing). One per broker, shared by every space on it |\n| `auth/account.<key>.json` | One space\'s own NATS data account and signing seed (secret). One file per space, all signed by the broker above; `<key>` is a stable, case-safe hex encoding of the space name (never the raw name, so two case-differing spaces can\'t collide) |\n| `auth/space.<key>/` | One space\'s user-auth state (IdP pin, issuer keys, owner secret, callout account), present only when that space enables per-user auth. Keyed by the same case-safe hex encoding; pre-hex layouts (`auth/<space>/`) are renamed here on first touch |\n| `auth/creds/<name>.creds` | Per-agent minted NATS credentials |\n| `auth/server.conf` | Generated nats-server config for the broker. The core renderer accepts every space on the broker; `cotal up` currently orchestrates one space per root, so it renders that one space\'s account |\n| `broker-policy.json` | Durable broker **launch** policy (TLS-required cert/key path references, or plaintext). Survives `cotal down` so a bare re-`up` cannot silently drop TLS. Under the project root \u2014 **not** under `COTAL_HOME` |\n| `agents/<name>.md` | Persona / agent files ([Agent files](agent-files.md)) |\n| `manifests/<hash>.json` | Manifest-deploy ledger (records of `up -f` / `spawn -f` runs) |\n| `config.json` | Space-local connector config (the override layer above) |\n| `nats.pid` \xB7 `nats.log` | Background nats-server pid + log |\n| `manager.pid` \xB7 `manager.log` | Manager (supervisor) pid + log; `manager.delivery-aware` marks a delivery-aware build. The manager writes the pid itself, whatever started it, and removes it on a clean stop only while it still names that process. A reader treats the record as a running manager only if the pid is alive **and** the process is a supervisor: a recycled pid belonging to something else is reported as a stale record, never signalled |\n| `delivery.pid` \xB7 `delivery.log` \xB7 `delivery.creds` | Delivery daemon pid, log, and scoped cred (auth mode) |\n| `web.pid` \xB7 `web.log` | Web dashboard pid + log |\n| `membership.json` \xB7 `membership-*.creds` | Membership feed state + its scoped creds |\n| `setup.log` | Last `cotal setup` run |\n\n### Machine: `~/.cotal`\n\nCross-project machine state, so a `cotal spawn` from any directory can find a running mesh. Location:\n`~/.cotal` on POSIX, `%LOCALAPPDATA%\\Cotal` on Windows; overridable with `COTAL_HOME`.\n\n`COTAL_HOME` overrides **this tree only** (registry + current pointer + onboard marker). It is not a\nfull workstation sandbox. Broker launch policy, the JetStream store, pidfiles, and auth live under\nthe **project** `.cotal/` found by walking up from the cwd ([Project: `.cotal/`](#project-cotal)\nabove, including `broker-policy.json` on TLS meshes). A probe that sets `COTAL_HOME` alone and runs\n`cotal up --tls-cert \u2026` from a directory whose walked root is the operator home still writes those\nproject paths on the live machine.\n\n| Path | What it is |\n|---|---|\n| `meshes/space.<key>.json` | Registry of running meshes: one file per broker `cotal up` started (server URL, root path, mode, TLS-required client intent when recorded); `<key>` is the same case-safe hex encoding of the space name, and the record\'s own `space` field is authoritative |\n| `current-mesh` | Default space a bare `cotal spawn` joins (set by `cotal use`) |\n| `onboarded.json` | First-run marker (with `ONBOARD_VERSION`) that flips setup between first-run and status-card |\n| the Claude plugin marketplace | The installed `cotal-mesh` plugin assets |\n\n### Config dir: `$XDG_CONFIG_HOME/cotal`\n\nDistinct from `~/.cotal`. Location: `$XDG_CONFIG_HOME/cotal`, else `~/.config/cotal` on POSIX, or\n`%APPDATA%\\Cotal` on Windows.\n\n| Path | What it is |\n|---|---|\n| `config.json` | Operator-level connector config (the base layer above) |\n| `extensions/` | `cotal ext` install prefix: its own npm root (`node_modules`) plus an `extensions.json` provider/command-display cache. Built-in connectors install here too, seeded on first run |\n| `seed/` | Built-in-connector seeding state: the `ever-seeded` authority (+ durable backup), the init witness, the version stamp, the crash cursor, and `store/<version>/<name>` (the stable payloads `ext add --install-links` reifies each seeded connector from) |\n\nFor how `cotal setup` populates the machine state and the plugin, and how the built-in connectors are\nseeded as removable extensions, see [setup internals](setup-internals.md).\n'
67281
+ "body": '# Configuration & environment\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\nThree things configure a Cotal workstation: the **config file** (per-connector settings, notably\nwhich of your MCP servers get shared with spawned agents), a set of **`COTAL_*` environment\nvariables**, and the **on-disk layout** under a project\'s `.cotal/` and your machine\'s `~/.cotal`.\nNone of these are part of the wire contract; they configure the reference implementation only.\n\n## The config file\n\nThe cotal config file carries per-connector launch settings. It is layered from two locations,\nmost-specific-wins:\n\n| Layer | Path | Scope |\n|---|---|---|\n| Base | `$XDG_CONFIG_HOME/cotal/config.json` (else `~/.config/cotal/config.json`; `%APPDATA%\\Cotal\\config.json` on Windows) | Operator-level, every space |\n| Override | `<project-root>/.cotal/config.json` | Space-local |\n\nThey merge per connector and per server name: a server in the space-local file replaces the\nsame-named server in the operator-level file; connectors or servers present in only one side are\nkept. A missing file is empty (valid); malformed JSON or a non-object top level is a loud error.\n\nToday it carries one thing: which of your personal MCP servers a connector should **share** with the\nagents it spawns. By default a spawned agent gets none: the Claude connector launches with\n`--strict-mcp-config`, dropping every ambient MCP server (they are heavy and useless to a meshed\nteammate). This file is the explicit opt-in.\n\n```json\n{\n "connectors": {\n "claude": {\n "mcpServers": {\n "github": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-github"],\n "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }\n }\n }\n }\n }\n}\n```\n\nEach server is written in the de-facto `.mcp.json` shape, so you can copy an entry straight out of\nyour own Claude / VS Code / Cursor config. Secrets ride as **`${VAR}` references** (also\n`${VAR:-default}`), resolved from your environment at launch and forwarded to the child **by name**\n(never as literals) so the file stays safe to keep in `~/.config` or a gitignored `.cotal/`. Only\n`command`, `args`, `env`, `url`, and `headers` are expanded; any other key passes through verbatim.\n\n**`--share-tools` interplay**. The per-spawn selection narrows what this config declares:\n\n| `--share-tools` | Result |\n|---|---|\n| (flag absent) | Every server declared for the connector |\n| `none` or empty | Nothing |\n| `a,b` | Only those named: each **must** be declared, or the spawn fails (no silent drop) |\n\nToday only the `claude` connector consumes shared MCP servers; OpenCode inherits config through its\nown merge layer and Hermes has no MCP. See [Connect Claude Code](connect-claude.md) for the full\nsharing model.\n\n## Environment variables\n\nThese are the operator-facing variables. Most of the connector-session ones (space, name, role, \u2026)\nare set **for you** by `cotal spawn` / the manager when they launch an agent; you set them by hand\nonly when you drive a connector session yourself (e.g. your own `claude` with the plugin) or a custom\nlauncher. Comma-separated lists are trimmed.\n\n| Variable | Consumed by | Meaning | Default |\n|---|---|---|---|\n| `COTAL_SPACE` | connector session | Space to join | `demo` (or the join link\'s) |\n| `COTAL_NAME` | connector session | Presence name / identity | required (or via `COTAL_AGENT_FILE` / `COTAL_LINK`) |\n| `COTAL_ROLE` | connector session | Role | agent file\'s `role:`, else none |\n| `COTAL_SERVERS` | connector session | Broker URL(s) | the default local broker (or the link\'s) |\n| `COTAL_CREDS` | connector session | Path to a NATS creds file (auth mode) | none (open mode) |\n| `COTAL_LINK` | connector session | `cotal://token@host/space` join link: supplies server, auth, space | none |\n| `COTAL_AGENT_FILE` | connector session | Path to a persona file: supplies name, role, kind, channels | none |\n| `COTAL_SUBSCRIBE` | connector session | Active channel read set | agent file / link, else `general` |\n| `COTAL_ALLOW_SUBSCRIBE` | connector session | Read ACL (channels the agent *may* read) | = `COTAL_SUBSCRIBE` |\n| `COTAL_ALLOW_PUBLISH` | connector session | Post ACL (channels the agent *may* post to) | deny (empty) |\n| `COTAL_MODEL` | connector session | Model label (display metadata) | agent file\'s `model:`, else none |\n| `COTAL_KIND` | connector session | Endpoint kind | `agent` |\n| `COTAL_TLS` | connector session | Connect over TLS (`1`) | off |\n| `COTAL_TOKEN` | connector session | Auth token (token / open modes) | none |\n| `COTAL_CAPABILITIES` | connector session | Control-plane capabilities (e.g. `spawn`) that gate manager tools | agent file\'s `capabilities:` |\n| `COTAL_QUIET` / `COTAL_MUTED` | connector session | Per-channel attention defaults (never-wake / drop-on-receive) | agent file\'s, else none |\n| `COTAL_CHANNEL` | Claude connector | Force channel wake-nudges on (`1`) / off; set to `1` by the Claude launcher | auto-detect |\n| `COTAL_EVENTS` | connector session | Arm this session\'s event plane (`1`); set by the launcher for `--events` spawns | off |\n| `COTAL_EVENTS_DEFAULT` | manager | Default event plane for managed spawns (`1`) | off |\n| `COTAL_DEFAULT_AGENT` | `cotal spawn` | Default connector type for a bare spawn | `claude` |\n| `COTAL_DEFAULT_PERSONA` | `cotal spawn` | Default persona for a bare spawn | `default` |\n| `COTAL_SKIP_CONNECTOR_SEED` | boot gate | Skip the automatic built-in-connector seed/refresh on a command (`1`); `cotal ext seed` still works | off |\n| `COTAL_DETACH_KEY` | `cotal attach` | Detach escape key (`ctrl-<char>` / `^<char>`) | `ctrl-]` |\n| `COTAL_FEEDBACK_KEY` | `feedback`, connector | Beta feedback key \u2192 keyed intake | none (public intake) |\n| `COTAL_FEEDBACK_EMAIL` | `feedback`, connector | Contact email for the keyless public intake | your git email |\n| `COTAL_FEEDBACK_URL` | `feedback`, connector | Intake URL override (self-hosted) | keyed / public intake |\n| `COTAL_SKIP_ASSIST` | `setup` | Disable the interactive Claude handoff on a failed step (`1`; for CI) | off |\n| `COTAL_COMPLETE_DEBUG` | `completion` | Print completion-resolution errors to stderr | off |\n| `COTAL_SERVE_HEADLESS` | OpenCode runtime | Run the OpenCode server without a foreground TUI (`1`) | off |\n| `COTAL_HOME` | workspace | Override the machine-home dir for the **mesh registry only** (`meshes/`, `current-mesh`, onboard marker). Does **not** redirect project-root paths (`findCotalRoot` / `.cotal/broker-policy.json`, NATS store, manager/delivery state, auth). Tests that run `cotal up` must also use a temp project root with its own `.cotal/` as `cwd` | `~/.cotal` |\n\n> `--console-port` is a `cotal supervise` flag, not an environment variable; there is no\n> `COTAL_CONSOLE_PORT`.\n\n### Set by the launcher, not by you\n\nThese are wired into a spawned child\'s environment by the connector / launcher and read back inside\nthe session. They are not operator knobs; listed so you recognize them in a process listing.\n\n| Variable | Purpose |\n|---|---|\n| `COTAL_ID` | Stable agent id chosen by the launcher (static meshes) |\n| `COTAL_LIFECYCLE_UID` | The incarnation\'s lifecycle UID, minted once per spawn; the session binds its lifecycle-keyed DM/delivery/history consumers by it (its credential pins the same names). Required for an authed launch (`COTAL_CREDS` or user-mode); config parsing fails loud without it. Open mode omits it (the endpoint self-mints per session) |\n| `COTAL_OWNER` / `COTAL_ACTOR` / `COTAL_SENTINEL_CREDS` / `COTAL_BEARER_CMD` | User-auth launch identity: the agent\'s principal, its sentinel creds path, and the exec-able bearer command; all four together, mutually exclusive with `COTAL_CREDS` |\n| `COTAL_CONTROL_SOCKET` / `COTAL_CONTROL_TOKEN` | The session\'s local control endpoint (path + token) the MCP server listens on and the lifecycle hooks connect to; token is env-only, never argv or logs |\n| `COTAL_BRIDGE_SOCKET` / `COTAL_TOOLS_FILE` / `COTAL_PARENT_PID` | Hermes sidecar plumbing (bridge socket, generated tool descriptors, launcher pid to watch) |\n| `OPENCODE_CONFIG_CONTENT` | Inline OpenCode config (the injected cotal plugin, highest merge layer) |\n| `OPENCODE_DB` / `OPENCODE_HOME` / `OPENCODE_PORT` / `OPENCODE_SERVER_URL` / `COTAL_OPENCODE_*` | OpenCode server plumbing (home, port, DB, server URL) |\n\nThe launcher forwards only a fixed OS allow-list (PATH, HOME, TERM, locale, XDG/Windows config dirs,\n\u2026) plus the named model-provider key and any `${VAR}` secrets a shared MCP server references, never\nyour whole environment, so unrelated secrets don\'t bleed into spawned agents. There are also a few\ninternal timing knobs (e.g. `COTAL_MEMBERSHIP_INTERVAL_MS`, `COTAL_DELIVERY_BROKER_GONE_MS`) that you\nshould not set in normal operation.\n\n## On-disk layout\n\n### Project: `.cotal/`\n\nA project\'s state lives in `.cotal/` at the mesh root (found by walking up from the cwd, like `.git`).\n**It is gitignored**; it holds secrets and machine-local process state.\n\n| Path | What it is |\n|---|---|\n| `auth/broker.json` | Broker trust material: the operator seed and the system account (secret; the system-account signing seed is stripped before writing). One per broker, shared by every space on it |\n| `auth/account.<key>.json` | One space\'s own NATS data account and signing seed (secret). One file per space, all signed by the broker above; `<key>` is a stable, case-safe hex encoding of the space name (never the raw name, so two case-differing spaces can\'t collide) |\n| `auth/space.<key>/` | One space\'s user-auth state (IdP pin, issuer keys, owner secret, callout account), present only when that space enables per-user auth. Keyed by the same case-safe hex encoding; pre-hex layouts (`auth/<space>/`) are renamed here on first touch |\n| `auth/creds/<name>.creds` | Per-agent minted NATS credentials |\n| `auth/server.conf` | Generated nats-server config for the broker. The core renderer accepts every space on the broker; `cotal up` currently orchestrates one space per root, so it renders that one space\'s account |\n| `broker-policy.json` | Durable broker **launch** policy (TLS-required cert/key path references, or plaintext). Survives `cotal down` so a bare re-`up` cannot silently drop TLS. Under the project root \u2014 **not** under `COTAL_HOME` |\n| `agents/<name>.md` | Persona / agent files ([Agent files](agent-files.md)) |\n| `manifests/<hash>.json` | Manifest-deploy ledger (records of `up -f` / `spawn -f` runs) |\n| `config.json` | Space-local connector config (the override layer above) |\n| `nats.pid` \xB7 `nats.log` | Background nats-server pid + log |\n| `manager.pid` \xB7 `manager.log` | Manager (supervisor) pid + log; `manager.delivery-aware` marks a delivery-aware build. The manager writes the pid itself, whatever started it, and removes it on a clean stop only while it still names that process. A reader treats the record as a running manager only if the pid is alive **and** the process is a supervisor: a recycled pid belonging to something else is reported as a stale record, never signalled |\n| `delivery.pid` \xB7 `delivery.log` \xB7 `delivery.creds` | Delivery daemon pid, log, and scoped cred (auth mode) |\n| `web.pid` \xB7 `web.log` | Web dashboard pid + log |\n| `membership.json` \xB7 `membership-*.creds` | Membership feed state + its scoped creds |\n| `setup.log` | Last `cotal setup` run |\n\n### Machine: `~/.cotal`\n\nCross-project machine state, so a `cotal spawn` from any directory can find a running mesh. Location:\n`~/.cotal` on POSIX, `%LOCALAPPDATA%\\Cotal` on Windows; overridable with `COTAL_HOME`.\n\n`COTAL_HOME` overrides **this tree only** (registry + current pointer + onboard marker). It is not a\nfull workstation sandbox. Broker launch policy, the JetStream store, pidfiles, and auth live under\nthe **project** `.cotal/` found by walking up from the cwd ([Project: `.cotal/`](#project-cotal)\nabove, including `broker-policy.json` on TLS meshes). A probe that sets `COTAL_HOME` alone and runs\n`cotal up --tls-cert \u2026` from a directory whose walked root is the operator home still writes those\nproject paths on the live machine.\n\n| Path | What it is |\n|---|---|\n| `meshes/space.<key>.json` | Registry of running meshes: one file per broker `cotal up` started (server URL, root path, mode, TLS-required client intent when recorded); `<key>` is the same case-safe hex encoding of the space name, and the record\'s own `space` field is authoritative |\n| `current-mesh` | Default space a bare `cotal spawn` joins (set by `cotal use`) |\n| `onboarded.json` | First-run marker (with `ONBOARD_VERSION`) that flips setup between first-run and status-card |\n| the Claude plugin marketplace | The installed `cotal-mesh` plugin assets |\n\n### Config dir: `$XDG_CONFIG_HOME/cotal`\n\nDistinct from `~/.cotal`. Location: `$XDG_CONFIG_HOME/cotal`, else `~/.config/cotal` on POSIX, or\n`%APPDATA%\\Cotal` on Windows.\n\n| Path | What it is |\n|---|---|\n| `config.json` | Operator-level connector config (the base layer above) |\n| `extensions/` | `cotal ext` install prefix: its own npm root (`node_modules`) plus an `extensions.json` provider/command-display cache. Built-in connectors install here too, seeded on first run |\n| `seed/` | Built-in-connector seeding state: the `ever-seeded` authority (+ durable backup), the init witness, the version stamp, the crash cursor, and `store/<version>/<name>` (the stable payloads `ext add --install-links` reifies each seeded connector from) |\n\nFor how `cotal setup` populates the machine state and the plugin, and how the built-in connectors are\nseeded as removable extensions, see [setup internals](setup-internals.md).\n'
65444
67282
  },
65445
67283
  {
65446
67284
  "slug": "connect-claude",
65447
67285
  "title": "Connect Claude",
65448
67286
  "kind": "Guide (informative)",
65449
67287
  "summary": "The Claude Code connector turns a real claude session into a Cotal mesh peer.",
65450
- "body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Hermes](connect-hermes.md) (alpha), [pi](connect-pi.md) (alpha); the\n[Connectors](connectors.md) matrix compares them feature-by-feature.\n\n## Set up\n\n```bash\ncotal setup # one-time: installs the plugin, seeds one agent; launches nothing\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin (so the repo's Claude sessions get the `cotal_*`\ntools) and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide), and **independent of the mesh connector**: it carries no\n code and no core dependency, installs whenever Claude is on `PATH` (even with the connector removed),\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release, so an upgrade + `cotal setup` runs `claude plugin update` and\n the deployed install actually gets the new skill. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it:\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches what it can actually invoke. Clearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | `http` lifecycle hooks \u2192 POST to the connector (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --strict-mcp-config --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_SERVERS, COTAL_CHANNEL=1\n```\n\n- **MCP isolation.** A spawned agent runs with **only** the cotal MCP server:\n `--strict-mcp-config` ignores every other MCP source, crucially the operator's personal\n `~/.claude.json` servers (several spawns each booting a heavy helper would starve\n memory). Share your own servers deliberately (see below).\n- **Installed, not `--plugin-dir`.** The plugin is installed once (`claude plugin install\n cotal@cotal-mesh --scope local`) because its hooks bind only to an *installed* plugin.\n In a clone the marketplace is the repo's `.claude-plugin/marketplace.json`; `cotal setup`\n (npx, no clone) materializes the same marketplace under `~/.cotal/claude-plugin/` (each plugin dir is\n rebuilt from scratch and atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its assets ship inside the CLI package, not the connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** Connector code requires `COTAL_NAME` *or* `COTAL_LINK`. A plain\n `claude` with no `COTAL_*` env stays inert and never joins, so your own sessions in a\n repo do not appear as stray peers.\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt; the PTY runtime\n auto-clears it, so a supervised launch needs no keypress.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write \u2014 a runtime whose pipe has gone away fails it \u2014 and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending \u2014 for an idle session it is the only wake source,\n so dropping it means silence until someone types. If a nudge is lost anyway (a race in the host's\n channel startup), JetStream redelivery re-announces the unacked durable item through the same\n attention policy, so a durable message always wakes the session eventually. If the channel cannot\n run at all, delivery still waits for the next hook. Live-only traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention: how much traffic wakes you\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items before automatic\ntraffic. If the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `Notification` (permission / elicitation) | `waiting` (blocked on a human: activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy) |\n| `SessionEnd` | `offline` (graceful leave) |\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. The full Claude Code hook-event list lives\nwith the adapter:\n[`extensions/connector-claude-code`](../extensions/connector-claude-code/README.md).\n\n## Transcript mirror\n\nA managed session mirrors its own transcript onto a per-agent channel, **`tr-<name>`**, so\npeers and cheap observer agents can read what the agent *actually* did: assistant text in\nfull, tool calls as one-liners, results truncated, thinking omitted. Gated by\n`COTAL_TRANSCRIPT` (set for managed sessions; a personal session with the plugin never\nmirrors). A `tr-` channel is a regular channel (durable, listed by `cotal_channels`,\nreadable on demand) with a rolling window, so long sessions age out early entries. In\nauth mode the launcher provisions publish rights for it alongside the agent's channels.\n\n## Resume an existing session (fork, never hijack)\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id>` works, with two differences: the id resolves against the\n **manager host's** `~/.claude` (you practically need `--cwd`), and the manager waits for\n a real outcome; `\u2713 started` means the agent *joined the mesh*, `\u2717 exited on launch`\n carries Claude's last output, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Only the Claude connector supports it today; OpenCode and Hermes fail loud.\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nIsolation is the default, but a meshed teammate sometimes genuinely needs one of your own\ntools (say, web search). The opt-in is the cotal config file\n(`~/.config/cotal/config.json`, or a space-local `.cotal/config.json` layered on top):\neach entry the familiar `.mcp.json` shape, secrets written as `${VAR}` references, never\nliterals ([full format](config.md)).\n\nAt launch the connector forwards *only* the named vars the chosen servers declare and\npasses the merged config as an owner-only temp file; `--strict-mcp-config` stays on, so\nonly cotal + the explicitly shared servers load. Scope per spawn with\n`--share-tools tavily,figma` (or `--share-tools none`).\n\nTwo caveats: sharing a server grants its credential to the agent (the var lives in the\nClaude process's environment, so share only when you're fine with that teammate holding\nthe key), and memory adds up, because a heavy server boots once per spawn, multiplied\nacross a team.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
67288
+ "body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Hermes](connect-hermes.md) (alpha), [pi](connect-pi.md) (alpha); the\n[Connectors](connectors.md) matrix compares them feature-by-feature.\n\n## Set up\n\n```bash\ncotal setup # one-time: installs the plugin, seeds one agent; launches nothing\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin (so the repo's Claude sessions get the `cotal_*`\ntools) and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide), and **independent of the mesh connector**: it carries no\n code and no core dependency, installs whenever Claude is on `PATH` (even with the connector removed),\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release, so an upgrade + `cotal setup` runs `claude plugin update` and\n the deployed install actually gets the new skill. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it:\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches what it can actually invoke. Clearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | `http` lifecycle hooks \u2192 POST to the connector (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --strict-mcp-config --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_SERVERS, COTAL_CHANNEL=1\n```\n\n- **MCP isolation.** A spawned agent runs with **only** the cotal MCP server:\n `--strict-mcp-config` ignores every other MCP source, crucially the operator's personal\n `~/.claude.json` servers (several spawns each booting a heavy helper would starve\n memory). Share your own servers deliberately (see below).\n- **Installed, not `--plugin-dir`.** The plugin is installed once (`claude plugin install\n cotal@cotal-mesh --scope local`) because its hooks bind only to an *installed* plugin.\n In a clone the marketplace is the repo's `.claude-plugin/marketplace.json`; `cotal setup`\n (npx, no clone) materializes the same marketplace under `~/.cotal/claude-plugin/` (each plugin dir is\n rebuilt from scratch and atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its assets ship inside the CLI package, not the connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** Connector code requires `COTAL_NAME` *or* `COTAL_LINK`. A plain\n `claude` with no `COTAL_*` env stays inert and never joins, so your own sessions in a\n repo do not appear as stray peers.\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt; the PTY runtime\n auto-clears it, so a supervised launch needs no keypress.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. If a nudge is lost anyway (a race in the host's\n channel startup), JetStream redelivery re-announces the unacked durable item through the same\n attention policy, so a durable message always wakes the session eventually. If the channel cannot\n run at all, delivery still waits for the next hook. Live-only traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention: how much traffic wakes you\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items before automatic\ntraffic. If the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `Notification` (permission / elicitation) | `waiting` (blocked on a human: activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy) |\n| `SessionEnd` | `offline` (graceful leave) |\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. The full Claude Code hook-event list lives\nwith the adapter:\n[`extensions/connector-claude-code`](../extensions/connector-claude-code/README.md).\n\n## Event plane\n\nA session launched with `cotal spawn --events` publishes a **structured** account of what it\ndid: run boundaries per turn, assistant text, reasoning, and each tool call with its arguments,\nits end, and its result. Not prose about the work, the work itself, in a vocabulary a program can\nread. Arming is `COTAL_EVENTS`, which the launcher sets for `--events` spawns; a personal session\nwith the plugin installed publishes nothing.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non exactly that one channel. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel carries the session's tool inputs and outputs. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, and an omitted flag is not\n\"leave it alone\": it is the wide default, `>` read, `>` post, and `spawn,role:default` scope. A bare\n`cotal actor grant <reader>` therefore grants a reader of every channel in the space, which is the\nopposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`, or the endpoint joins `general` by default and a scoped credential is refused\nthere. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal, not its display name. On a user-auth mesh\nthe actor half **is** the agent's name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, arming needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated `--events` spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected.\n\n## Resume an existing session (fork, never hijack)\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id>` works, with two differences: the id resolves against the\n **manager host's** `~/.claude` (you practically need `--cwd`), and the manager waits for\n a real outcome; `\u2713 started` means the agent *joined the mesh*, `\u2717 exited on launch`\n carries Claude's last output, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Only the Claude connector supports it today; OpenCode and Hermes fail loud.\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nIsolation is the default, but a meshed teammate sometimes genuinely needs one of your own\ntools (say, web search). The opt-in is the cotal config file\n(`~/.config/cotal/config.json`, or a space-local `.cotal/config.json` layered on top):\neach entry the familiar `.mcp.json` shape, secrets written as `${VAR}` references, never\nliterals ([full format](config.md)).\n\nAt launch the connector forwards *only* the named vars the chosen servers declare and\npasses the merged config as an owner-only temp file; `--strict-mcp-config` stays on, so\nonly cotal + the explicitly shared servers load. Scope per spawn with\n`--share-tools tavily,figma` (or `--share-tools none`).\n\nTwo caveats: sharing a server grants its credential to the agent (the var lives in the\nClaude process's environment, so share only when you're fine with that teammate holding\nthe key), and memory adds up, because a heavy server boots once per spawn, multiplied\nacross a team.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
65451
67289
  },
65452
67290
  {
65453
67291
  "slug": "connect-codex",
65454
67292
  "title": "Connect Codex (beta)",
65455
67293
  "kind": "Guide (informative)",
65456
67294
  "summary": "OpenAI Codex joins a Cotal mesh as a lateral peer: the same cotal tool surface, the same message delivery and attention model as the other connectors, plus mid-turn steering (previously pi-only): a\u2026",
65457
- "body": "# Connect Codex (beta)\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\n[OpenAI Codex](https://developers.openai.com/codex/) joins a Cotal mesh as a lateral peer: the\nsame `cotal_*` tool surface, the same message delivery and attention model as the other\nconnectors, plus mid-turn steering (previously pi-only): a directed peer message arriving\nmid-turn is **steered into the running turn** instead of waiting for it to end.\n\n**Beta** means the everyday path (spawn into the real Codex TUI, coordinate, watch) works; the\nspawn options that are not wired **fail loud** rather than degrade: resuming a session\n(`--resume`) and tool-sharing (`connectors.codex.mcpServers`). See [Limits](#limits).\n\n## Install\n\nThe connector ships with the CLI as a seeded extension (`@cotal-ai/connector-codex`): no\nseparate install step and no Codex-side plugin. You only need an authenticated `codex` binary\non your PATH (a ChatGPT-plan login or an `OPENAI_API_KEY`). If an older install is missing it,\n`cotal ext seed --repair` (or `cotal ext add @cotal-ai/connector-codex`) brings it in.\n\n**Don't install the `cotal` plugin Codex offers you.** Searching Codex's plugin list for \"cotal\"\nturns up a plugin named `cotal`, from the `cotal-mesh` marketplace. That is the **Claude Code**\nadapter, which appears there only because Codex reads the same plugin-marketplace format; it is\nnot this connector and installing it does not connect Codex to a mesh. Codex needs nothing\ninstalled on its side: the connector drives it from the outside, over `codex app-server`.\n\n**Codex version.** The connector drives `codex app-server` over its experimental v2 surface.\nMinimum **codex-cli 0.145.0**; tested against 0.145.0 and 0.146.0. An older binary authenticates fine but has\nno `--listen`/`--ws-auth` listener, so the launch fails at startup rather than misbehaving quietly:\ncheck with `codex --version` and upgrade (`npm i -g @openai/codex`) if a launch reports that the\napp-server exited before it started listening. The surface is explicitly experimental upstream, so\na later Codex release may change it and need a connector update. That is a break to report, not a\nsupport range we can promise ahead of it.\n\n## Spawn it\n\nSame launch grammar as any agent (see [run-a-mesh.md](run-a-mesh.md)):\n\n```bash\ncotal spawn --agent codex # foreground in this terminal\ncotal spawn reviewer --agent codex -d # detached via the manager; watch with `cotal attach`\nCOTAL_DEFAULT_AGENT=codex cotal spawn # make codex the default harness\n```\n\nOr set `agent: codex` in a team [manifest](manifest.md). Persona, role, and model come from the\nagent file as for any connector ([agent-files.md](agent-files.md)).\n\n## Choose a model\n\n```bash\ncotal models --agent codex # ids + reasoning-effort variants, via app-server model/list\ncotal spawn --agent codex --model gpt-5.6-sol --variant high\n```\n\nThe **variant** is Codex's reasoning effort (`minimal` | `low` | `medium` | `high` | `xhigh`).\nLike the `codex` CLI itself, the connector does not validate model ids or efforts locally. An\nunknown value fails at request time, server-side.\n\nModel and variant are published on presence, which is where `cotal roster` and the web dashboard's\n`model \xB7 variant` badge read them from. The variant appears only when you asked for one (via\n`--variant` or `variant:` in the agent file): there is no way to read the effort back off a running\nthread, so an unset variant is shown as absent rather than guessed at.\n\n## How it binds\n\nCodex has no in-process plugin runtime and its MCP client cannot wake an idle session, so the\nconnector runs Codex's own client/server split: a small **host process** embeds the mesh\nendpoint and drives a `codex app-server` thread over JSON-RPC (the same protocol the Codex TUI\nruns on). The app-server runs as an authenticated loopback **listener** rather than a private\npipe, which is what lets Codex's own TUI attach to the very thread the mesh is driving.\n\n- **Wake and steer.** An inbound batch starts a real turn (`turn/start`). A DIRECTED message\n (DM, anycast, @mention) arriving mid-turn is injected into the live turn (`turn/steer`);\n ambient channel chatter waits for the turn boundary so it can't derail work in flight.\n- **Native tools, one endpoint.** The host serves the shared `cotal_*` tools itself, on a\n bearer-authenticated loopback MCP endpoint (the token is passed by env name, so it never appears\n in the process table; see [Limits](#limits) for what that token does and does not protect). The model calls them like any tool and they\n execute against the host's single mesh endpoint: no sidecar process, no second identity. The\n app-server is the MCP client, so the tools work the same on a turn a peer message started and\n on one **you** typed into the TUI.\n- **At-least-once delivery.** A turn's surfaced messages are acked (by exact id) only when the\n turn completes. A failed turn retries with backoff, and an interrupted turn leaves the batch to\n redeliver. If the Codex app-server itself dies, the host restarts it in place (same mesh\n identity, credential, and durable) and re-drives the un-acked batch into the new thread; a\n crash *loop* (more than 3 in 2 minutes) is fatal rather than an endless respawn. (The shared\n bounded-inbox overflow rule applies: under extreme bursts an evicted in-flight id cannot\n redeliver.)\n- **Isolated, never written.** Each agent gets a private `CODEX_HOME` (one hashed directory\n per space+name under `.cotal/codex/`, rooted at the manager's workspace): your `~/.codex`\n config.toml, hooks, and MCP servers never load into a managed agent, and Codex's per-project\n trust records never touch your real config. Your `auth.json` is symlinked in (re-linked each\n launch), so ChatGPT-plan token refreshes never fork. Without an `auth.json` (or an\n `OPENAI_API_KEY`) the launch fails loud at thread start. Keyring-stored credentials are not\n wired through the isolated home; use the file store or the env key for managed agents. That\n symlink is why managed Codex agents are **POSIX-only** today: on Windows without Developer\n Mode the link fails, and the launch fails loud rather than copying `auth.json` (a copy would\n fork the token and break plan refreshes).\n- **Autonomy defaults.** Spawned agents run `approval_policy=never`,\n `sandbox_mode=workspace-write`, and `sandbox_workspace_write={network_access=true}`.\n See [Autonomy and the sandbox](#autonomy-and-the-sandbox) for what each one means and how to\n change it.\n- **It really is Codex.** `cotal spawn --agent codex` drops you into the actual Codex TUI,\n attached to the thread the mesh drives (`codex resume --remote`). Mesh turns render as they\n happen, and anything you type is a real user turn on that same thread with the `cotal_*` tools\n still available. In the foreground that is your terminal; detached it is the manager's pty,\n which is exactly what `cotal attach` streams and drives. With no terminal at all (piped output,\n CI, a smoke) the host stays headless and prints an activity feed instead: the same peer either\n way, only the UI differs. `--transcript` mirrors the feed to `tr-<name>`.\n **Which mode you get** is decided by whether *stdout* is a terminal, and `COTAL_CODEX_TUI=1|0`\n overrides that check when it would guess wrong (a wrapper that redirects output, a CI run that\n wants deterministic text). It is read from the environment of **whichever process builds the\n launch**, so set it in the right place:\n - foreground `cotal spawn`: your own shell, per spawn;\n - detached (`-d`): the **manager's** environment, because the manager builds the launch. Set it\n where you start the manager (`COTAL_CODEX_TUI=0 cotal up`) and it applies to every codex agent\n that manager supervises. Exporting it in the shell that runs `cotal spawn -d` does nothing.\n\n A detached agent gets the manager's pty, which *is* a terminal, so the default there is the TUI,\n which is what `cotal attach` streams.\n Once the TUI paints, the terminal belongs to Codex, so the host's own diagnostics move to\n `host.log` inside the agent's private home\n (`<workspace>/.cotal/codex/<space>-<name>-<hash>/host.log`; the handoff line prints the exact\n path, and `ls -t .cotal/codex/*/host.log` finds it after the fact). Attached, a failure is also\n reported on the terminal; detached, that report goes to the pty, so the file is the durable copy.\n- **Presence from events.** working/idle/waiting are derived from the app-server event stream;\n the model id is reported from the started thread.\n\n`--opt k=v` launch options render as codex `-c k=v` config overrides on the app-server child\n(top-level keys, scalar values; write TOML inline-table text yourself for nested values). The\nconnector's own defaults and selectors ride the same rail and yield to yours, except\n`mcp_servers`, which is how the agent reaches the mesh: the whole namespace is refused loud (at\nspawn, not at launch) rather than silently overridden.\n\n## Autonomy and the sandbox\n\nA spawned Codex agent is woken by peer messages, which arrive when nobody is watching the\nterminal. The defaults follow from that, and all three are overridable per spawn with `--opt`.\n\n| Default | What it means |\n| --- | --- |\n| `approval_policy=\"never\"` | Never **ask** before running a command. Not \"refuse\": the agent runs its commands, it just does not stop to prompt. An interactive policy is refused loud rather than honored dishonestly, because a mesh-driven turn would block forever on a prompt nobody sees, and the alternative (auto-answering for you) nullifies the policy you asked for. |\n| `sandbox_mode=\"workspace-write\"` | Commands may read anywhere but write only inside the agent's workspace. This, not the prompt, is the part that is actually enforced; see below for the (real) exposure it leaves. |\n| `sandbox_workspace_write={network_access=true}` | Network **on** inside that sandbox. Codex's own default is off, which breaks installing a dependency, pushing a branch, or calling an API, with an error that reads like the task is impossible rather than the sandbox saying no. Applied only when the sandbox is actually `workspace-write`: tighten the mode and no network grant is emitted at all. |\n\nWhat the sandbox guarantees, stated literally: it **blocks out-of-workspace local filesystem\nwrites**. It does **not** block reads, exfiltration, or networked side effects.\n\nAll three of those are live with the defaults above, because a peer's message is a **remote input**\nthat can cause this agent to run commands. A confused or hostile peer can in principle get it to\nread a file elsewhere on your machine and send it; reach loopback or link-local services; or act\nthrough any credential it can read, which includes irreversible actions: a force-push, an API\ndelete, a deploy. Containing filesystem writes is therefore not the same as containing damage, and\nit should not be read that way. It is still worth keeping, because it is the one class this sandbox\ncan actually enforce.\n\nIf that exposure is wrong for a given agent, turn the network back off (below), tighten the mode,\nor run it under a separate OS user; the same point is repeated under [Limits](#limits) so it\nsurvives a skim. The spawn capability is the trust boundary for *who* may create an agent; the\nsandbox bounds one class of what it can then be talked into doing, not all of it.\n\nTune it per spawn:\n\n```bash\ncotal spawn --agent codex --opt sandbox_mode=read-only # tightest: no writes\ncotal spawn --agent codex --opt 'sandbox_workspace_write={network_access=false}' # contained, offline\ncotal spawn --agent codex --opt sandbox_mode=danger-full-access # no sandbox at all\n```\n\n`danger-full-access` is Codex's own name for it and means what it says: the agent may write\nanywhere your user account can. Codex documents that mode as intended only for environments that\nare already externally sandboxed (a container, a VM), not a workstation. On a laptop, prefer\ntightening the workspace over removing the sandbox.\n\n## Limits\n\n- **The sandbox blocks out-of-workspace filesystem writes, and only that.** It does not block\n reads, exfiltration, or networked side effects. With the default `workspace-write` + network on,\n a peer-driven turn can read anything your user account can (`~/.ssh`, `~/.aws`, `.env` files, the\n agent's own `auth.json`) and send it; reach loopback and link-local services; and act through any\n credential it can read, including irreversibly (a force-push, an API delete, a deploy). Only\n local writes outside the workspace are stopped, so this is not \"everything risky is reversible\"\n and not \"the only exposure is disclosure\". If that is wrong for a given agent, spawn it with\n `--opt 'sandbox_workspace_write={network_access=false}'` or `--opt sandbox_mode=read-only`, or\n run it as a separate OS user. See [Autonomy and the sandbox](#autonomy-and-the-sandbox).\n- **Not a boundary between agents on one machine.** The app-server listener and the tool\n endpoint are both loopback-bound and token-authenticated, which keeps out other OS users and\n anything off-box. It is not isolation between *managed agents*, which run as the same user and\n can therefore reach each other's tokens; a hostile agent on your workstation could drive\n another's Codex or speak as it on the mesh. Run mutually distrusted agents under separate OS\n users or separate machines.\n- **The TUI is local-only.** The app-server listener binds loopback and nothing else, so\n attaching Codex's UI to an agent on another machine needs your own SSH port-forward; there is\n no built-in remote attach. `cotal attach` (which streams the manager's pty) is the supported\n way to reach a detached agent.\n- **No session resume.** `cotal spawn --resume <id>` throws: a resumed codex thread comes up\n without its configured MCP servers, so the agent would be mute on the mesh.\n- **No tool-sharing.** `connectors.codex.mcpServers` is not implemented and throws if set.\n- **Experimental upstream surface.** `codex app-server` is labeled experimental by OpenAI (it\n is also what the Codex TUI itself runs on). The connector pins every protocol shape in one\n driver file and re-proves the contract with a gated live smoke (`COTAL_E2E_CODEX=1`).\n\n## See also\n\n- [Connectors](connectors.md): the feature matrix across all connectors\n- [Run a mesh](run-a-mesh.md) \xB7 [Define a team](define-a-team.md) \xB7 [Watch a mesh](watch-a-mesh.md)\n- [MCP tools](mcp-tools.md) \xB7 [Connect Claude Code](connect-claude.md) \xB7 [Connect OpenCode](connect-opencode.md)\n"
67295
+ "body": "# Connect Codex (beta)\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\n[OpenAI Codex](https://developers.openai.com/codex/) joins a Cotal mesh as a lateral peer: the\nsame `cotal_*` tool surface, the same message delivery and attention model as the other\nconnectors, plus mid-turn steering (previously pi-only): a directed peer message arriving\nmid-turn is **steered into the running turn** instead of waiting for it to end.\n\n**Beta** means the everyday path (spawn into the real Codex TUI, coordinate, watch) works; the\nspawn options that are not wired **fail loud** rather than degrade: resuming a session\n(`--resume`) and tool-sharing (`connectors.codex.mcpServers`). See [Limits](#limits).\n\n## Install\n\nThe connector ships with the CLI as a seeded extension (`@cotal-ai/connector-codex`): no\nseparate install step and no Codex-side plugin. You only need an authenticated `codex` binary\non your PATH (a ChatGPT-plan login or an `OPENAI_API_KEY`). If an older install is missing it,\n`cotal ext seed --repair` (or `cotal ext add @cotal-ai/connector-codex`) brings it in.\n\n**Don't install the `cotal` plugin Codex offers you.** Searching Codex's plugin list for \"cotal\"\nturns up a plugin named `cotal`, from the `cotal-mesh` marketplace. That is the **Claude Code**\nadapter, which appears there only because Codex reads the same plugin-marketplace format; it is\nnot this connector and installing it does not connect Codex to a mesh. Codex needs nothing\ninstalled on its side: the connector drives it from the outside, over `codex app-server`.\n\n**Codex version.** The connector drives `codex app-server` over its experimental v2 surface.\nMinimum **codex-cli 0.145.0**; tested against 0.145.0 and 0.146.0. An older binary authenticates fine but has\nno `--listen`/`--ws-auth` listener, so the launch fails at startup rather than misbehaving quietly:\ncheck with `codex --version` and upgrade (`npm i -g @openai/codex`) if a launch reports that the\napp-server exited before it started listening. The surface is explicitly experimental upstream, so\na later Codex release may change it and need a connector update. That is a break to report, not a\nsupport range we can promise ahead of it.\n\n## Spawn it\n\nSame launch grammar as any agent (see [run-a-mesh.md](run-a-mesh.md)):\n\n```bash\ncotal spawn --agent codex # foreground in this terminal\ncotal spawn reviewer --agent codex -d # detached via the manager; watch with `cotal attach`\nCOTAL_DEFAULT_AGENT=codex cotal spawn # make codex the default harness\n```\n\nOr set `agent: codex` in a team [manifest](manifest.md). Persona, role, and model come from the\nagent file as for any connector ([agent-files.md](agent-files.md)).\n\n## Choose a model\n\n```bash\ncotal models --agent codex # ids + reasoning-effort variants, via app-server model/list\ncotal spawn --agent codex --model gpt-5.6-sol --variant high\n```\n\nThe **variant** is Codex's reasoning effort (`minimal` | `low` | `medium` | `high` | `xhigh`).\nLike the `codex` CLI itself, the connector does not validate model ids or efforts locally. An\nunknown value fails at request time, server-side.\n\nModel and variant are published on presence, which is where `cotal roster` and the web dashboard's\n`model \xB7 variant` badge read them from. The variant appears only when you asked for one (via\n`--variant` or `variant:` in the agent file): there is no way to read the effort back off a running\nthread, so an unset variant is shown as absent rather than guessed at.\n\n## How it binds\n\nCodex has no in-process plugin runtime and its MCP client cannot wake an idle session, so the\nconnector runs Codex's own client/server split: a small **host process** embeds the mesh\nendpoint and drives a `codex app-server` thread over JSON-RPC (the same protocol the Codex TUI\nruns on). The app-server runs as an authenticated loopback **listener** rather than a private\npipe, which is what lets Codex's own TUI attach to the very thread the mesh is driving.\n\n- **Wake and steer.** An inbound batch starts a real turn (`turn/start`). A DIRECTED message\n (DM, anycast, @mention) arriving mid-turn is injected into the live turn (`turn/steer`);\n ambient channel chatter waits for the turn boundary so it can't derail work in flight.\n- **Native tools, one endpoint.** The host serves the shared `cotal_*` tools itself, on a\n bearer-authenticated loopback MCP endpoint (the token is passed by env name, so it never appears\n in the process table; see [Limits](#limits) for what that token does and does not protect). The model calls them like any tool and they\n execute against the host's single mesh endpoint: no sidecar process, no second identity. The\n app-server is the MCP client, so the tools work the same on a turn a peer message started and\n on one **you** typed into the TUI.\n- **At-least-once delivery.** A turn's surfaced messages are acked (by exact id) only when the\n turn completes. A failed turn retries with backoff, and an interrupted turn leaves the batch to\n redeliver. If the Codex app-server itself dies, the host restarts it in place (same mesh\n identity, credential, and durable) and re-drives the un-acked batch into the new thread; a\n crash *loop* (more than 3 in 2 minutes) is fatal rather than an endless respawn. (The shared\n bounded-inbox overflow rule applies: under extreme bursts an evicted in-flight id cannot\n redeliver.)\n- **Isolated, never written.** Each agent gets a private `CODEX_HOME` (one hashed directory\n per space+name under `.cotal/codex/`, rooted at the manager's workspace): your `~/.codex`\n config.toml, hooks, and MCP servers never load into a managed agent, and Codex's per-project\n trust records never touch your real config. Your `auth.json` is symlinked in (re-linked each\n launch), so ChatGPT-plan token refreshes never fork. Without an `auth.json` (or an\n `OPENAI_API_KEY`) the launch fails loud at thread start. Keyring-stored credentials are not\n wired through the isolated home; use the file store or the env key for managed agents. That\n symlink is why managed Codex agents are **POSIX-only** today: on Windows without Developer\n Mode the link fails, and the launch fails loud rather than copying `auth.json` (a copy would\n fork the token and break plan refreshes).\n- **Autonomy defaults.** Spawned agents run `approval_policy=never`,\n `sandbox_mode=workspace-write`, and `sandbox_workspace_write={network_access=true}`.\n See [Autonomy and the sandbox](#autonomy-and-the-sandbox) for what each one means and how to\n change it.\n- **It really is Codex.** `cotal spawn --agent codex` drops you into the actual Codex TUI,\n attached to the thread the mesh drives (`codex resume --remote`). Mesh turns render as they\n happen, and anything you type is a real user turn on that same thread with the `cotal_*` tools\n still available. In the foreground that is your terminal; detached it is the manager's pty,\n which is exactly what `cotal attach` streams and drives. With no terminal at all (piped output,\n CI, a smoke) the host stays headless and prints an activity feed instead: the same peer either\n way, only the UI differs.\n **Which mode you get** is decided by whether *stdout* is a terminal, and `COTAL_CODEX_TUI=1|0`\n overrides that check when it would guess wrong (a wrapper that redirects output, a CI run that\n wants deterministic text). It is read from the environment of **whichever process builds the\n launch**, so set it in the right place:\n - foreground `cotal spawn`: your own shell, per spawn;\n - detached (`-d`): the **manager's** environment, because the manager builds the launch. Set it\n where you start the manager (`COTAL_CODEX_TUI=0 cotal up`) and it applies to every codex agent\n that manager supervises. Exporting it in the shell that runs `cotal spawn -d` does nothing.\n\n A detached agent gets the manager's pty, which *is* a terminal, so the default there is the TUI,\n which is what `cotal attach` streams.\n Once the TUI paints, the terminal belongs to Codex, so the host's own diagnostics move to\n `host.log` inside the agent's private home\n (`<workspace>/.cotal/codex/<space>-<name>-<hash>/host.log`; the handoff line prints the exact\n path, and `ls -t .cotal/codex/*/host.log` finds it after the fact). Attached, a failure is also\n reported on the terminal; detached, that report goes to the pty, so the file is the durable copy.\n- **Presence from events.** working/idle/waiting are derived from the app-server event stream;\n the model id is reported from the started thread.\n\n`--opt k=v` launch options render as codex `-c k=v` config overrides on the app-server child\n(top-level keys, scalar values; write TOML inline-table text yourself for nested values). The\nconnector's own defaults and selectors ride the same rail and yield to yours, except\n`mcp_servers`, which is how the agent reaches the mesh: the whole namespace is refused loud (at\nspawn, not at launch) rather than silently overridden.\n\n## Autonomy and the sandbox\n\nA spawned Codex agent is woken by peer messages, which arrive when nobody is watching the\nterminal. The defaults follow from that, and all three are overridable per spawn with `--opt`.\n\n| Default | What it means |\n| --- | --- |\n| `approval_policy=\"never\"` | Never **ask** before running a command. Not \"refuse\": the agent runs its commands, it just does not stop to prompt. An interactive policy is refused loud rather than honored dishonestly, because a mesh-driven turn would block forever on a prompt nobody sees, and the alternative (auto-answering for you) nullifies the policy you asked for. |\n| `sandbox_mode=\"workspace-write\"` | Commands may read anywhere but write only inside the agent's workspace. This, not the prompt, is the part that is actually enforced; see below for the (real) exposure it leaves. |\n| `sandbox_workspace_write={network_access=true}` | Network **on** inside that sandbox. Codex's own default is off, which breaks installing a dependency, pushing a branch, or calling an API, with an error that reads like the task is impossible rather than the sandbox saying no. Applied only when the sandbox is actually `workspace-write`: tighten the mode and no network grant is emitted at all. |\n\nWhat the sandbox guarantees, stated literally: it **blocks out-of-workspace local filesystem\nwrites**. It does **not** block reads, exfiltration, or networked side effects.\n\nAll three of those are live with the defaults above, because a peer's message is a **remote input**\nthat can cause this agent to run commands. A confused or hostile peer can in principle get it to\nread a file elsewhere on your machine and send it; reach loopback or link-local services; or act\nthrough any credential it can read, which includes irreversible actions: a force-push, an API\ndelete, a deploy. Containing filesystem writes is therefore not the same as containing damage, and\nit should not be read that way. It is still worth keeping, because it is the one class this sandbox\ncan actually enforce.\n\nIf that exposure is wrong for a given agent, turn the network back off (below), tighten the mode,\nor run it under a separate OS user; the same point is repeated under [Limits](#limits) so it\nsurvives a skim. The spawn capability is the trust boundary for *who* may create an agent; the\nsandbox bounds one class of what it can then be talked into doing, not all of it.\n\nTune it per spawn:\n\n```bash\ncotal spawn --agent codex --opt sandbox_mode=read-only # tightest: no writes\ncotal spawn --agent codex --opt 'sandbox_workspace_write={network_access=false}' # contained, offline\ncotal spawn --agent codex --opt sandbox_mode=danger-full-access # no sandbox at all\n```\n\n`danger-full-access` is Codex's own name for it and means what it says: the agent may write\nanywhere your user account can. Codex documents that mode as intended only for environments that\nare already externally sandboxed (a container, a VM), not a workstation. On a laptop, prefer\ntightening the workspace over removing the sandbox.\n\n## Limits\n\n- **The sandbox blocks out-of-workspace filesystem writes, and only that.** It does not block\n reads, exfiltration, or networked side effects. With the default `workspace-write` + network on,\n a peer-driven turn can read anything your user account can (`~/.ssh`, `~/.aws`, `.env` files, the\n agent's own `auth.json`) and send it; reach loopback and link-local services; and act through any\n credential it can read, including irreversibly (a force-push, an API delete, a deploy). Only\n local writes outside the workspace are stopped, so this is not \"everything risky is reversible\"\n and not \"the only exposure is disclosure\". If that is wrong for a given agent, spawn it with\n `--opt 'sandbox_workspace_write={network_access=false}'` or `--opt sandbox_mode=read-only`, or\n run it as a separate OS user. See [Autonomy and the sandbox](#autonomy-and-the-sandbox).\n- **Not a boundary between agents on one machine.** The app-server listener and the tool\n endpoint are both loopback-bound and token-authenticated, which keeps out other OS users and\n anything off-box. It is not isolation between *managed agents*, which run as the same user and\n can therefore reach each other's tokens; a hostile agent on your workstation could drive\n another's Codex or speak as it on the mesh. Run mutually distrusted agents under separate OS\n users or separate machines.\n- **The TUI is local-only.** The app-server listener binds loopback and nothing else, so\n attaching Codex's UI to an agent on another machine needs your own SSH port-forward; there is\n no built-in remote attach. `cotal attach` (which streams the manager's pty) is the supported\n way to reach a detached agent.\n- **No session resume.** `cotal spawn --resume <id>` throws: a resumed codex thread comes up\n without its configured MCP servers, so the agent would be mute on the mesh.\n- **No tool-sharing.** `connectors.codex.mcpServers` is not implemented and throws if set.\n- **Experimental upstream surface.** `codex app-server` is labeled experimental by OpenAI (it\n is also what the Codex TUI itself runs on). The connector pins every protocol shape in one\n driver file and re-proves the contract with a gated live smoke (`COTAL_E2E_CODEX=1`).\n\n## See also\n\n- [Connectors](connectors.md): the feature matrix across all connectors\n- [Run a mesh](run-a-mesh.md) \xB7 [Define a team](define-a-team.md) \xB7 [Watch a mesh](watch-a-mesh.md)\n- [MCP tools](mcp-tools.md) \xB7 [Connect Claude Code](connect-claude.md) \xB7 [Connect OpenCode](connect-opencode.md)\n"
65458
67296
  },
65459
67297
  {
65460
67298
  "slug": "connect-hermes",
@@ -65587,7 +67425,7 @@ var DOCS_BUNDLE = {
65587
67425
  "title": "Security model",
65588
67426
  "kind": "Concept (informative threat model)",
65589
67427
  "summary": "Cotal v0 provides containment and sender authenticity for peers sharing one trusted NATS broker.",
65590
- "body": "# Security model\n\n> **Concept** (informative threat model) \xB7 **For:** operators and security reviewers \xB7 **Normative:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization). This page is the threat model SPEC \xA79 references; where the two disagree, the spec wins.\n\nCotal v0 provides containment and sender authenticity for peers sharing one trusted NATS\nbroker. It is not an end-to-end encrypted or untrusted-relay protocol. The enforcement\nmechanics (profiles, ACLs, consumer confinement) are defined in\n[SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization) and\n[Appendix B](../SPEC.md#appendix-b-profile-acls), explained informally in\n[identity & auth](identity-and-auth.md); this page covers **who the adversaries are and\nwhat is (not) defended**.\n\n## Trust boundary\n\n- One Cotal space maps to one NATS account.\n- The broker, operator, account signing key holder, and any `admin` credential are trusted.\n- On a per-user-auth mesh, ledger scope `admin` is the same trust grade as an `admin`\n credential: it unlocks the elevated views (the whole-space read tap, history and channel\n purges, channel-registry writes, cross-owner control), so grant it as operator authority,\n not as a convenience ([identity & auth](identity-and-auth.md)).\n- Agents are not trusted to self-report sender identity, channel permissions, or DM access.\n\n## Adversaries\n\nEach adversary, what it can attempt, and what stops it (or why it is out of scope).\n\n- **Compromised or malicious peer agent** (authenticated, in-space): the primary adversary.\n It cannot forge another agent's `from.id` (the subject sender, an `owner.actor` principal,\n is pinned to its connection by NATS permissions; not another owner, and not a sibling actor\n under its own owner), cannot publish to channels outside its declared allow-list, and cannot read\n another agent's DMs or another role's work queue ([SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)).\n It still can send well-formed hostile content to channels it is allowed on\n (see *Prompt-facing data*) and flood within its limits (see *availability* under *What v0\n does not protect*). These are **broker-enforced** guarantees and assume the peer has no host\n filesystem or process access to the account signer: the default single-host manager and container\n compositions do not isolate the signer from a same-uid agent, which could then mint `admin` and\n read any DM. Isolating it is a hosted-composition concern (see [Embedding Cotal](embedding.md) and\n [Deploy](deploy.md)).\n- **Buggy or lazy receiver:** sender authenticity depends on the receiver enforcing the\n `from.id`-equals-subject-sender check; a client that skips it accepts spoofed senders. The\n check is therefore normative: receivers MUST reject on mismatch\n ([SPEC \xA75](../SPEC.md#5-envelopes), [\xA712](../SPEC.md#12-conformance)).\n- **On-path network attacker** (between an agent and the broker): defeated only when the join\n link uses `cotals://` (TLS **required** \u2014 client refuses if the broker is not TLS). Plain\n `cotal://` does **not** require TLS: a NATS client may still auto-upgrade against an honest\n TLS broker, but a forged plaintext `INFO` can strip the upgrade and harvest credentials. Use\n plain `cotal://` only on trusted networks and in dev.\n- **Content author targeting a reading model:** any writer of channel `description` /\n `instructions`, presence `activity`, message bodies, or free-form metadata can attempt\n prompt injection against an agent that reads it. See *Prompt-facing data*.\n- **Untrusted broker, relay, operator, or admin:** out of scope by definition. The broker and\n any `admin` credential can read, drop, replay, or alter all plaintext traffic. v0 makes no\n claim against a hostile broker; signed envelopes and untrusted-relay bindings are reserved\n for a later version ([roadmap](roadmap.md)).\n\n## What v0 protects\n\nThe guarantees, at a glance, each enforced by the broker per\n[SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization):\n\n- **Sender authenticity**: the sender id is encoded in the subject and enforced by NATS\n permissions; receivers reject payloads whose `from.id` mismatches.\n- **Space containment**: account boundaries isolate one space's subjects, streams, and KV\n buckets from another.\n- **Channel publish scope**: posting only as self, only to declared `allowPublish`\n channels (default-deny).\n- **Channel read scope**, reads bounded to the `allowSubscribe` ACL: live joins are\n broker-refused outside it, and history reads ride server-pinned single-channel consumers.\n - **Known metadata leak (not content):** agents hold `STREAM.INFO` on the chat stream, so\n a `subjects_filter` query can enumerate retained chat *subjects* (channel names, sender\n ids, per-subject counts) including channels outside `allowSubscribe`. This is metadata,\n never message content, and channel *names* are already public via the registry. Hiding\n even the existence/volume of other channels requires the per-channel-stream model and is\n deferred strict-containment work ([roadmap](roadmap.md)).\n- **DM / task peer confidentiality**: per-identity inbox prefixes plus\n provisioner-created bind-only consumers, so an agent cannot read someone else's inbox or\n steal another role's work; durable-channel backstop reads are re-authorized by a trusted\n reader ([delivery daemon](delivery-daemon.md)).\n- **Transport secrecy (optional)**: `cotals://` enforces TLS for the hop to the broker.\n It protects that hop, not the broker itself.\n\n## What v0 does not protect\n\n- **Untrusted broker or relay:** the broker can read, drop, replay, or alter plaintext\n traffic. Signed envelopes are reserved for a later version.\n- **End-to-end secrecy:** DMs are plaintext to the broker and to `admin`. Cotal v0\n deliberately does not add end-to-end encryption, trading secrecy for a single trusted broker.\n- **Non-repudiation:** sender authenticity is broker-enforced, not portable proof. (A2A signs\n every message for this; here it is reserved as signed envelopes.)\n- **Availability:** an authenticated peer can flood any channel or inbox it may write to. v0\n relies on coarse NATS account limits (connections, subscriptions, payload and storage caps)\n and adds no per-agent application-level rate limiting.\n- **Replay by a peer:** a peer may re-send its own prior messages; v0 defines no protocol-level\n nonce or idempotency key. It cannot replay as another agent (subject binding still holds).\n- **Static agent credential revocation:** on a static-auth mesh, a *manager-spawned* agent cred\n is now bounded (24h TTL, renewed by the manager for live agents only) and lifecycle-registered:\n despawn drives the full \xA713.1 retirement \u2014 its ledger rows are revoked and the manager's\n control surface refuses the retired incarnation's credential outright. What remains: within\n the TTL window a *copied* cred keeps its inline data-plane grants (static has no auth callout,\n so nothing re-checks at reconnect), and an out-of-band `cotal mint` cred is still long-lived\n until key rotation. A per-user-auth mesh closes both: short-lived bearers, ledger revocation\n that bites at the next connect, and live-connection eviction\n ([identity & auth](identity-and-auth.md)). A copied signing *seed* still stays valid until\n rotation on either kind of mesh.\n- **Manager compromise:** the operator side is split into narrow, single-purpose profiles (there\n is **no allow-all cred**); the long-lived **supervisor** serves control and touches\n presence/its lease but cannot read a DM, create a consumer, or delete a stream; the destructive\n verbs (`STREAM.DELETE`/`PURGE`, cross-agent stop, per-agent provisioning) ride ephemeral\n per-command creds (teardown / control-caller-admin / deployer / provisioner). What stays hot on\n a static-auth mesh is the account **signing key** on the mint/manager box (a compromise there\n can still mint fresh creds); on a per-user-auth mesh it is held by the auth service (the callout\n stage) and by any running manager, which self-mints its supervisor cred and renewals from it\n ([identity & auth](identity-and-auth.md)).\n- **`spawn` is host-launch authority:** launch options are a raw passthrough (no allow/deny\n list), so a persona holding `capabilities: [spawn]` can drive the connector's full launch\n surface on the manager host (Claude `--mcp-config`, `--add-dir`, permission flags; OpenCode\n agent-config keys). The boundary is *who* may spawn (the authenticated caller, gated by the\n capability), not *which* flags they pass. Grant `spawn` as host-launch authority, not a narrow\n \"add a teammate\" permission ([run a mesh](run-a-mesh.md#spawning-agents)).\n\n## Prompt-facing data\n\nChannel `description` and `instructions`, presence `activity`, message bodies, and free-form\nmetadata may reach models. Writers that can set channel registry text are privileged, and\nregistry text is length-bounded, but clients MUST still render all of it as attributed,\nadvisory data, never as trusted system instruction. This is the indirect-prompt-injection\nsurface common to agent protocols (MCP tool descriptions, A2A agent cards): Cotal's position is\nthat the reading client, not the wire, is the trust boundary for model-facing text.\n\n## Reporting\n\nReport a suspected vulnerability privately to the maintainers rather than in a public issue.\n"
67428
+ "body": "# Security model\n\n> **Concept** (informative threat model) \xB7 **For:** operators and security reviewers \xB7 **Normative:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization). This page is the threat model SPEC \xA79 references; where the two disagree, the spec wins.\n\nCotal v0 provides containment and sender authenticity for peers sharing one trusted NATS\nbroker. It is not an end-to-end encrypted or untrusted-relay protocol. The enforcement\nmechanics (profiles, ACLs, consumer confinement) are defined in\n[SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization) and\n[Appendix B](../SPEC.md#appendix-b-profile-acls), explained informally in\n[identity & auth](identity-and-auth.md); this page covers **who the adversaries are and\nwhat is (not) defended**.\n\n## Trust boundary\n\n- One Cotal space maps to one NATS account.\n- The broker, operator, account signing key holder, and any `admin` credential are trusted.\n- On a per-user-auth mesh, ledger scope `admin` is the same trust grade as an `admin`\n credential: it unlocks the elevated views (the whole-space read tap, history and channel\n purges, channel-registry writes, cross-owner control), so grant it as operator authority,\n not as a convenience ([identity & auth](identity-and-auth.md)).\n- Agents are not trusted to self-report sender identity, channel permissions, or DM access.\n\n## Adversaries\n\nEach adversary, what it can attempt, and what stops it (or why it is out of scope).\n\n- **Compromised or malicious peer agent** (authenticated, in-space): the primary adversary.\n It cannot forge another agent's `from.id` (the subject sender, an `owner.actor` principal,\n is pinned to its connection by NATS permissions; not another owner, and not a sibling actor\n under its own owner), cannot publish to channels outside its declared allow-list, and cannot read\n another agent's DMs or another role's work queue ([SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)).\n It still can send well-formed hostile content to channels it is allowed on\n (see *Prompt-facing data*) and flood within its limits (see *availability* under *What v0\n does not protect*). These are **broker-enforced** guarantees and assume the peer has no host\n filesystem or process access to the account signer: the default single-host manager and container\n compositions do not isolate the signer from a same-uid agent, which could then mint `admin` and\n read any DM. Isolating it is a hosted-composition concern (see [Embedding Cotal](embedding.md) and\n [Deploy](deploy.md)).\n- **Buggy or lazy receiver:** sender authenticity depends on the receiver enforcing the\n `from.id`-equals-subject-sender check; a client that skips it accepts spoofed senders. The\n check is therefore normative: receivers MUST reject on mismatch\n ([SPEC \xA75](../SPEC.md#5-envelopes), [\xA712](../SPEC.md#12-conformance)).\n- **On-path network attacker** (between an agent and the broker): defeated only when the join\n link uses `cotals://` (TLS **required** \u2014 client refuses if the broker is not TLS). Plain\n `cotal://` does **not** require TLS: a NATS client may still auto-upgrade against an honest\n TLS broker, but a forged plaintext `INFO` can strip the upgrade and harvest credentials. Use\n plain `cotal://` only on trusted networks and in dev.\n- **Content author targeting a reading model:** any writer of channel `description` /\n `instructions`, presence `activity`, message bodies, or free-form metadata can attempt\n prompt injection against an agent that reads it. See *Prompt-facing data*.\n- **Untrusted broker, relay, operator, or admin:** out of scope by definition. The broker and\n any `admin` credential can read, drop, replay, or alter all plaintext traffic. v0 makes no\n claim against a hostile broker; signed envelopes and untrusted-relay bindings are reserved\n for a later version ([roadmap](roadmap.md)).\n\n## What v0 protects\n\nThe guarantees, at a glance, each enforced by the broker per\n[SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization):\n\n- **Sender authenticity**: the sender id is encoded in the subject and enforced by NATS\n permissions; receivers reject payloads whose `from.id` mismatches.\n- **Space containment**: account boundaries isolate one space's subjects, streams, and KV\n buckets from another.\n- **Channel publish scope**: posting only as self, only to declared `allowPublish`\n channels (default-deny).\n- **Channel read scope**, reads bounded to the `allowSubscribe` ACL: live joins are\n broker-refused outside it, and history reads ride server-pinned single-channel consumers.\n - **Known metadata leak (not content):** agents hold `STREAM.INFO` on the chat stream, so\n a `subjects_filter` query can enumerate retained chat *subjects* (channel names, sender\n ids, per-subject counts) including channels outside `allowSubscribe`. This is metadata,\n never message content, and channel *names* are already public via the registry. Hiding\n even the existence/volume of other channels requires the per-channel-stream model and is\n deferred strict-containment work ([roadmap](roadmap.md)).\n- **DM / task peer confidentiality**: per-identity inbox prefixes plus\n provisioner-created bind-only consumers, so an agent cannot read someone else's inbox or\n steal another role's work; durable-channel backstop reads are re-authorized by a trusted\n reader ([delivery daemon](delivery-daemon.md)).\n- **Transport secrecy (optional)**: `cotals://` enforces TLS for the hop to the broker.\n It protects that hop, not the broker itself.\n\n## What v0 does not protect\n\n- **Untrusted broker or relay:** the broker can read, drop, replay, or alter plaintext\n traffic. Signed envelopes are reserved for a later version.\n- **End-to-end secrecy:** DMs are plaintext to the broker and to `admin`. Cotal v0\n deliberately does not add end-to-end encryption, trading secrecy for a single trusted broker.\n- **Non-repudiation:** sender authenticity is broker-enforced, not portable proof. (A2A signs\n every message for this; here it is reserved as signed envelopes.)\n- **Availability:** an authenticated peer can flood any channel or inbox it may write to. v0\n relies on coarse NATS account limits (connections, subscriptions, payload and storage caps)\n and adds no per-agent application-level rate limiting.\n- **Replay by a peer:** a peer may re-send its own prior messages; v0 defines no protocol-level\n nonce or idempotency key. It cannot replay as another agent (subject binding still holds).\n- **Static agent credential revocation:** on a static-auth mesh, a *manager-spawned* agent cred\n is now bounded (24h TTL, renewed by the manager for live agents only) and lifecycle-registered:\n despawn drives the full \xA713.1 retirement \u2014 its ledger rows are revoked and the manager's\n control surface refuses the retired incarnation's credential outright. What remains: within\n the TTL window a *copied* cred keeps its inline data-plane grants (static has no auth callout,\n so nothing re-checks at reconnect), and an out-of-band `cotal mint` cred is still long-lived\n until key rotation. A per-user-auth mesh closes both: short-lived bearers, ledger revocation\n that bites at the next connect, and live-connection eviction\n ([identity & auth](identity-and-auth.md)). A copied signing *seed* still stays valid until\n rotation on either kind of mesh.\n- **Manager compromise:** the operator side is split into narrow, single-purpose profiles (there\n is **no allow-all cred**); the long-lived **supervisor** serves control and touches\n presence/its lease but cannot read a DM, create a consumer, or delete a stream; the destructive\n verbs (`STREAM.DELETE`/`PURGE`, cross-agent stop, per-agent provisioning) ride ephemeral\n per-command creds (teardown / control-caller-admin / deployer / provisioner). What stays hot on\n a static-auth mesh is the account **signing key** on the mint/manager box (a compromise there\n can still mint fresh creds); on a per-user-auth mesh it is held by the auth service (the callout\n stage) and by any running manager, which self-mints its supervisor cred and renewals from it\n ([identity & auth](identity-and-auth.md)).\n- **A static mesh's spawn credential is the ACL tier:** a caller that may spawn may also name the\n child's channel ACL, and on a static-auth mesh nothing attenuates that against the caller's own\n grant, because there is no ledger to attenuate against. This is the same class as the entry above\n and is not specific to any channel: the read set a spawn-capable static caller may hand its child\n covers ordinary channels, and `events.*` alongside them. A per-user-auth mesh does attenuate it:\n every delegation must sit inside the spawner's own grant, checked by NATS-pattern containment\n along the whole chain, at the grant write and again at every bearer exchange\n ([identity & auth](identity-and-auth.md)). Grant `spawn` on a static mesh as ACL authority, not\n as a narrow \"add a teammate\" permission.\n- **`spawn` is host-launch authority:** launch options are a raw passthrough (no allow/deny\n list), so a persona holding `capabilities: [spawn]` can drive the connector's full launch\n surface on the manager host (Claude `--mcp-config`, `--add-dir`, permission flags; OpenCode\n agent-config keys). The boundary is *who* may spawn (the authenticated caller, gated by the\n capability), not *which* flags they pass. Grant `spawn` as host-launch authority, not a narrow\n \"add a teammate\" permission ([run a mesh](run-a-mesh.md#spawning-agents)).\n\n## Prompt-facing data\n\nChannel `description` and `instructions`, presence `activity`, message bodies, and free-form\nmetadata may reach models. Writers that can set channel registry text are privileged, and\nregistry text is length-bounded, but clients MUST still render all of it as attributed,\nadvisory data, never as trusted system instruction. This is the indirect-prompt-injection\nsurface common to agent protocols (MCP tool descriptions, A2A agent cards): Cotal's position is\nthat the reading client, not the wire, is the trust boundary for model-facing text.\n\n## Reporting\n\nReport a suspected vulnerability privately to the maintainers rather than in a public issue.\n"
65591
67429
  },
65592
67430
  {
65593
67431
  "slug": "setup-internals",
@@ -65729,10 +67567,10 @@ function sectionsOf(slug, pageTitle, body) {
65729
67567
  for (const line of body.split("\n")) {
65730
67568
  if (line.trimStart().startsWith("```"))
65731
67569
  inFence = !inFence;
65732
- const h = inFence ? null : line.match(/^#{2,6}\s+(.+?)\s*$/);
65733
- if (h) {
67570
+ const h2 = inFence ? null : line.match(/^#{2,6}\s+(.+?)\s*$/);
67571
+ if (h2) {
65734
67572
  flush();
65735
- heading = `${pageTitle} \u203A ${h[1].trim()}`;
67573
+ heading = `${pageTitle} \u203A ${h2[1].trim()}`;
65736
67574
  }
65737
67575
  buf.push(line);
65738
67576
  }
@@ -65808,10 +67646,10 @@ function renderSearch(query, hits) {
65808
67646
  if (!hits.length) {
65809
67647
  return `No matches for "${query}" in the Cotal v${DOCS_BUNDLE.version} docs. Call cotal_docs() for the page index, or search an exact identifier (a subject, a cotal_* tool, a field name).`;
65810
67648
  }
65811
- const blocks = hits.map((h) => `## ${h.heading}
65812
- ${capSection(h.text)}
67649
+ const blocks = hits.map((h2) => `## ${h2.heading}
67650
+ ${capSection(h2.text)}
65813
67651
 
65814
- \u2192 read the full page: cotal_docs(page: "${h.slug}")`);
67652
+ \u2192 read the full page: cotal_docs(page: "${h2.slug}")`);
65815
67653
  return [
65816
67654
  `# Cotal v${DOCS_BUNDLE.version} docs \u2014 top matches for "${query}"`,
65817
67655
  "The most relevant sections are below. Read the full page before writing code or wire frames.",
@@ -65953,12 +67791,12 @@ function fmtFrom(i) {
65953
67791
  return i.fromRole ? `${i.fromName}/${i.fromRole}` : i.fromName;
65954
67792
  }
65955
67793
  function fmtItem(i) {
65956
- const h = i.historical ? "(history) " : "";
67794
+ const h2 = i.historical ? "(history) " : "";
65957
67795
  if (i.kind === "dm")
65958
- return `[DM from ${fmtFrom(i)}] ${h}${i.text}`;
67796
+ return `[DM from ${fmtFrom(i)}] ${h2}${i.text}`;
65959
67797
  if (i.kind === "anycast")
65960
- return `[@${i.service} from ${fmtFrom(i)}] ${h}${i.text}`;
65961
- return `[#${i.channel}${i.mentionsMe ? " @you" : ""} ${fmtFrom(i)}] ${h}${i.text}`;
67798
+ return `[@${i.service} from ${fmtFrom(i)}] ${h2}${i.text}`;
67799
+ return `[#${i.channel}${i.mentionsMe ? " @you" : ""} ${fmtFrom(i)}] ${h2}${i.text}`;
65962
67800
  }
65963
67801
  function renderChannelInfo(channel, info) {
65964
67802
  const lines = [
@@ -66433,8 +68271,8 @@ function registerCotalTools(server, agent, config2, source) {
66433
68271
 
66434
68272
  // ../connector-core/dist/control.js
66435
68273
  var import_node_net2 = require("node:net");
66436
- var import_node_fs3 = require("node:fs");
66437
- var import_node_crypto8 = require("node:crypto");
68274
+ var import_node_fs7 = require("node:fs");
68275
+ var import_node_crypto13 = require("node:crypto");
66438
68276
  var HANDOFF_DEADLINE_MS = 5e3;
66439
68277
  var LEGACY_WRITE_DEADLINE_MS = 5e3;
66440
68278
  var MAX_FRAME_BYTES = 1 << 20;
@@ -66442,18 +68280,18 @@ var AUTH_DEADLINE_MS = 5e3;
66442
68280
  function tokenMatches(presented, digest) {
66443
68281
  if (typeof presented !== "string")
66444
68282
  return false;
66445
- return (0, import_node_crypto8.timingSafeEqual)((0, import_node_crypto8.createHash)("sha256").update(presented).digest(), digest);
68283
+ return (0, import_node_crypto13.timingSafeEqual)((0, import_node_crypto13.createHash)("sha256").update(presented).digest(), digest);
66446
68284
  }
66447
68285
  function who(i) {
66448
68286
  return i.fromRole ? `${i.fromName}/${i.fromRole}` : i.fromName;
66449
68287
  }
66450
68288
  function fmtItem2(i) {
66451
- const h = i.historical ? " (history)" : "";
68289
+ const h2 = i.historical ? " (history)" : "";
66452
68290
  if (i.kind === "dm")
66453
- return `\u2022 DM from ${who(i)}${h}: ${i.text}`;
68291
+ return `\u2022 DM from ${who(i)}${h2}: ${i.text}`;
66454
68292
  if (i.kind === "anycast")
66455
- return `\u2022 @${i.service} (from ${who(i)})${h}: ${i.text}`;
66456
- return `\u2022 #${i.channel} ${who(i)}${h}: ${i.text}`;
68293
+ return `\u2022 @${i.service} (from ${who(i)})${h2}: ${i.text}`;
68294
+ return `\u2022 #${i.channel} ${who(i)}${h2}: ${i.text}`;
66457
68295
  }
66458
68296
  function formatInjection(items) {
66459
68297
  if (!items.length)
@@ -66521,10 +68359,10 @@ function writeReply(sock, reply, awaitHandoff) {
66521
68359
  }
66522
68360
  function startControlServer(agent, endpoint, handle, opts = {}) {
66523
68361
  const { path } = endpoint;
66524
- const digest = (0, import_node_crypto8.createHash)("sha256").update(endpoint.token).digest();
66525
- if (process.platform !== "win32" && (0, import_node_fs3.existsSync)(path)) {
68362
+ const digest = (0, import_node_crypto13.createHash)("sha256").update(endpoint.token).digest();
68363
+ if (process.platform !== "win32" && (0, import_node_fs7.existsSync)(path)) {
66526
68364
  try {
66527
- (0, import_node_fs3.unlinkSync)(path);
68365
+ (0, import_node_fs7.unlinkSync)(path);
66528
68366
  } catch {
66529
68367
  }
66530
68368
  }
@@ -66589,19 +68427,23 @@ function startControlServer(agent, endpoint, handle, opts = {}) {
66589
68427
  return server;
66590
68428
  }
66591
68429
 
68430
+ // src/mcp.ts
68431
+ var import_node_crypto14 = require("node:crypto");
68432
+ var import_node_path6 = require("node:path");
68433
+
66592
68434
  // src/hooks.ts
66593
68435
  function toolDetail(name, input) {
66594
68436
  if (typeof name !== "string" || !name) return void 0;
66595
68437
  const i = input ?? {};
66596
- const salient2 = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern ?? i.description;
66597
- let detail = typeof salient2 === "string" ? salient2 : Object.keys(i).length ? JSON.stringify(i) : "";
68438
+ const salient = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern ?? i.description;
68439
+ let detail = typeof salient === "string" ? salient : Object.keys(i).length ? JSON.stringify(i) : "";
66598
68440
  if (detail.length > 300) detail = `${detail.slice(0, 299)}\u2026`;
66599
68441
  return { name, detail };
66600
68442
  }
66601
68443
  var REPEAT_NOTE = "(A previous delivery of one or more of these was not confirmed, so they may be a repeat.)";
66602
68444
  var REPEAT_LABEL_CAP = 512;
66603
68445
  function createClaudeHandle(deps = {}) {
66604
- const mirror2 = () => deps.mirror?.();
68446
+ const events2 = () => deps.events?.();
66605
68447
  let pendingTool;
66606
68448
  const inFlight = /* @__PURE__ */ new WeakMap();
66607
68449
  const unconfirmed = /* @__PURE__ */ new Set();
@@ -66632,7 +68474,7 @@ ${body}` : body;
66632
68474
  try {
66633
68475
  switch (event) {
66634
68476
  case "SessionStart": {
66635
- mirror2()?.adopt(ev.transcript_path);
68477
+ events2()?.adopt(ev.transcript_path);
66636
68478
  if (typeof ev.model === "string") await agent.setModel(ev.model).catch(() => {
66637
68479
  });
66638
68480
  await safeStatus(agent, "idle");
@@ -66645,12 +68487,12 @@ ${body}` : body;
66645
68487
  }
66646
68488
  case "UserPromptSubmit":
66647
68489
  pendingTool = void 0;
66648
- mirror2()?.flush(ev.transcript_path);
68490
+ events2()?.flush(ev.transcript_path);
66649
68491
  await safeStatus(agent, "working");
66650
68492
  return withContext(surfaceAutomatic(agent, ev));
66651
68493
  case "PreToolUse":
66652
68494
  pendingTool = toolDetail(ev.tool_name, ev.tool_input);
66653
- mirror2()?.flush(ev.transcript_path);
68495
+ events2()?.flush(ev.transcript_path);
66654
68496
  return {};
66655
68497
  case "Notification": {
66656
68498
  const msg = typeof ev.message === "string" ? ev.message : void 0;
@@ -66661,12 +68503,13 @@ ${body}` : body;
66661
68503
  case "Stop":
66662
68504
  case "StopFailure":
66663
68505
  pendingTool = void 0;
66664
- mirror2()?.flush(ev.transcript_path);
68506
+ events2()?.flush(ev.transcript_path);
68507
+ events2()?.closeRun(Date.now());
66665
68508
  await safeStatus(agent, "idle");
66666
68509
  if (agent.pendingWake() > 0) agent.requestWake();
66667
68510
  return {};
66668
68511
  case "SessionEnd":
66669
- mirror2()?.flush(ev.transcript_path);
68512
+ events2()?.flush(ev.transcript_path);
66670
68513
  await safeStatus(agent, "offline");
66671
68514
  return {};
66672
68515
  default:
@@ -66759,168 +68602,211 @@ function createWakePolicy(agent, notify, log = () => {
66759
68602
  };
66760
68603
  }
66761
68604
 
66762
- // src/transcript.ts
66763
- var import_node_fs4 = require("node:fs");
66764
- var MAX_PREVIEW = 700;
66765
- var MAX_CHUNK = 6e3;
66766
- function truncate(s, max) {
66767
- return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
66768
- }
66769
- function salient(input) {
66770
- const i = input ?? {};
66771
- const v = i.command ?? i.file_path ?? i.path ?? i.url ?? i.pattern ?? i.description;
66772
- const s = typeof v === "string" ? v : Object.keys(i).length ? JSON.stringify(i) : "";
66773
- return s ? `: ${truncate(s, 300)}` : "";
66774
- }
66775
- function resultText(content) {
66776
- if (typeof content === "string") return content;
66777
- if (Array.isArray(content))
66778
- return content.map((b) => typeof b?.text === "string" ? b.text : "").filter(Boolean).join("\n");
66779
- return "";
66780
- }
66781
- function condense(line) {
66782
- let e;
66783
- try {
66784
- e = JSON.parse(line);
66785
- } catch {
66786
- return [];
66787
- }
66788
- if (!e || e.isMeta) return [];
66789
- const content = e.message?.content;
66790
- if (e.type === "assistant" && Array.isArray(content)) {
66791
- const out = [];
66792
- for (const b of content) {
66793
- if (b.type === "text" && b.text?.trim()) out.push(b.text.trim());
66794
- else if (b.type === "tool_use" && b.name) out.push(`\u2692 ${b.name}${salient(b.input)}`);
66795
- }
66796
- return out;
68605
+ // src/agui-map.ts
68606
+ var ORIGIN_RULE = {
68607
+ human: "human",
68608
+ channel: "channel",
68609
+ // a peer/mesh delivery IS a turn, the one change from the original table
68610
+ "task-notification": null,
68611
+ // known, and deliberately not a turn
68612
+ // MEASURED ON THIS MACHINE'S CORPUS, 237 session files and 531,882 records: 4 occurrences, every
68613
+ // one a standing goal the harness re-injects to continue work, all carrying
68614
+ // `promptSource: "system"`. It opens a run because a run is work that began, and this input is
68615
+ // what began it; an observer asking what the agent did and what triggered it is owed the answer
68616
+ // "the harness continued itself", which is neither a person nor a peer. So it is attributed as
68617
+ // itself rather than folded into either.
68618
+ //
68619
+ // WITHOUT THIS KEY THE MAPPER THROWS IN PRODUCTION on a value that is already in the corpus, and
68620
+ // the throw is the correct behaviour for an unmeasured provenance. Adding it is what a
68621
+ // measurement buys; guessing at it is what the throw exists to prevent.
68622
+ "auto-continuation": "auto-continuation"
68623
+ };
68624
+ var ABSENT_ORIGIN_RULE = {
68625
+ sdk: "sdk",
68626
+ // MEASURED, 6 occurrences across the same 237-file corpus, and READ rather than counted: every
68627
+ // one is a person typing, each a file path followed by a question about it. They carry no
68628
+ // `origin` because the harness does not stamp one on this shape, not because nobody authored
68629
+ // them.
68630
+ //
68631
+ // This is the case the previous table would have THROWN on, which is why it had to be read and
68632
+ // not assumed. A throw here is a session that stops mapping mid-stream; attributing these to
68633
+ // anything other than the person who typed them would be the confident wrong attribution the
68634
+ // enumeration exists to prevent. Both failures were available, so the record was opened.
68635
+ typed: "human",
68636
+ // **NOT FROM MY CORPUS — FROM §3.1's, WHICH I CANNOT RE-READ.** My 88-session sweep finds this
68637
+ // value on an absent-origin `user` record ZERO times in 129,910 records. §3.1's table records it
68638
+ // 81 times, as "local-command caveats, heartbeats, resumed-session summaries", on a capture with
68639
+ // 4728 `user` entries and 3068 `channel` deliveries — and **no session on this machine matches
68640
+ // that shape**; the closest has 18 human and 0 channel. So the two measurements are over different
68641
+ // corpora and one of them is gone.
68642
+ //
68643
+ // It is entered as `null` — known, and NOT a turn — because §3.1 already classified it and a
68644
+ // measurement I cannot repeat is still a measurement. Leaving it out would make the throw below
68645
+ // fire in production on a class the plan documents, which is the one thing a fail-loud branch must
68646
+ // not do: **a fail-loud branch is only safe if you know what is on the other side of it.**
68647
+ system: null
68648
+ };
68649
+ var runOpeningAttribution = (entry) => {
68650
+ const kind = entry.origin?.kind;
68651
+ if (kind === void 0) {
68652
+ const ps = entry.promptSource;
68653
+ if (ps === void 0) return null;
68654
+ if (!(ps in ABSENT_ORIGIN_RULE))
68655
+ throw new Error(
68656
+ `agui-map: origin-less entry ${entry.uuid ?? "<no uuid>"} carries promptSource ${JSON.stringify(ps)}, which this mapper has never measured. Refusing to decide whether it begins a run. Add it to ABSENT_ORIGIN_RULE deliberately, with a measurement.`
68657
+ );
68658
+ return ABSENT_ORIGIN_RULE[ps];
66797
68659
  }
66798
- if (e.type === "user") {
66799
- if (typeof content === "string")
66800
- return content.trim() ? [`\xBB ${truncate(content.trim(), MAX_PREVIEW)}`] : [];
66801
- if (Array.isArray(content)) {
66802
- const out = [];
66803
- for (const b of content) {
66804
- if (b.type === "tool_result") {
66805
- const t = resultText(b.content).trim();
66806
- out.push(`\u2192 ${b.is_error ? "ERROR: " : ""}${t ? truncate(t, MAX_PREVIEW) : "(no output)"}`);
66807
- } else if (b.type === "text" && b.text?.trim()) {
66808
- out.push(`\xBB ${truncate(b.text.trim(), MAX_PREVIEW)}`);
66809
- }
68660
+ if (!(kind in ORIGIN_RULE))
68661
+ throw new Error(
68662
+ `agui-map: unrecognised origin.kind ${JSON.stringify(kind)} on entry ${entry.uuid ?? "<no uuid>"} \u2014 refusing to decide whether it begins a run, or to attribute one to a provenance this mapper has never seen. Add it to ORIGIN_RULE deliberately, with a measurement.`
68663
+ );
68664
+ return ORIGIN_RULE[kind];
68665
+ };
68666
+ function stampOf(entry, now) {
68667
+ const parsed = entry.timestamp ? Date.parse(entry.timestamp) : Number.NaN;
68668
+ return Number.isFinite(parsed) ? { ts: parsed, arrival: false } : { ts: now(), arrival: true };
68669
+ }
68670
+ function resultContent(raw) {
68671
+ return typeof raw === "string" ? raw : JSON.stringify(raw ?? null);
68672
+ }
68673
+ function createClaudeMapper(opts) {
68674
+ const now = opts.now ?? (() => Date.now());
68675
+ let open5 = null;
68676
+ let runsOpened = 0;
68677
+ let promptShaped = 0;
68678
+ let refusedUnattributable = 0;
68679
+ const closeOpenRun = (timestamp, stopReason) => {
68680
+ if (open5 === null) return null;
68681
+ const runId = open5;
68682
+ open5 = null;
68683
+ return {
68684
+ runId,
68685
+ events: [
68686
+ runFinished({
68687
+ threadId: opts.threadId,
68688
+ runId,
68689
+ timestamp,
68690
+ ...stopReason ? { cotal: { stopReason } } : {}
68691
+ })
68692
+ ]
68693
+ };
68694
+ };
68695
+ const map2 = (entry) => {
68696
+ const { ts, arrival } = stampOf(entry, now);
68697
+ const arrivalMeta = arrival ? { tsSource: "arrival" } : void 0;
68698
+ const uuid3 = entry.uuid ?? "";
68699
+ const events2 = [];
68700
+ if (entry.type === "user") {
68701
+ const content = entry.message?.content;
68702
+ const toolResults = Array.isArray(content) ? content.filter((b) => b.type === "tool_result" && b.tool_use_id) : [];
68703
+ if (toolResults.length > 0) {
68704
+ content.forEach((b, i) => {
68705
+ if (b.type !== "tool_result" || !b.tool_use_id) return;
68706
+ events2.push(
68707
+ toolCallResult({
68708
+ messageId: `${uuid3}#${i}`,
68709
+ toolCallId: b.tool_use_id,
68710
+ content: resultContent(b.content),
68711
+ timestamp: ts,
68712
+ ...b.is_error || arrivalMeta ? { cotal: { ...b.is_error ? { isError: true } : {}, ...arrivalMeta } } : {}
68713
+ })
68714
+ );
68715
+ });
68716
+ return events2.length > 0 && open5 !== null ? { runId: open5, events: events2 } : null;
68717
+ }
68718
+ const promptText = typeof content === "string" ? content : Array.isArray(content) ? content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n") : null;
68719
+ if (promptText === null) return null;
68720
+ if (entry.isCompactSummary === true || entry.isVisibleInTranscriptOnly === true) return null;
68721
+ promptShaped += 1;
68722
+ const turnSource = runOpeningAttribution(entry);
68723
+ if (turnSource === null) {
68724
+ refusedUnattributable += 1;
68725
+ return null;
66810
68726
  }
66811
- return out;
66812
- }
66813
- }
66814
- return [];
66815
- }
66816
- var TranscriptMirror = class {
66817
- constructor(agent, channel) {
66818
- this.agent = agent;
66819
- this.channel = channel;
66820
- }
66821
- agent;
66822
- channel;
66823
- path;
66824
- offset = 0;
66825
- /** A batch that failed mid-publish: chunks + how many already landed. Retried (from the
66826
- * first unsent chunk — never re-sending a delivered one) before any new read. */
66827
- pending;
66828
- /** ALL path/offset mutation and publishing runs on this serialized chain — hook events
66829
- * land concurrently on the control socket. */
66830
- chain = Promise.resolve();
66831
- /** Adopt the transcript at its CURRENT end — mirror only what happens from now on, so a
66832
- * resumed session (or a mirror that first sees the path mid-session) never rebroadcasts. */
66833
- adopt(path) {
66834
- this.enqueue(() => {
66835
- this.adoptNow(path);
66836
- return Promise.resolve();
66837
- });
66838
- }
66839
- /** Queue a flush of new transcript entries to the channel. Never throws, never blocks the
66840
- * hook reply — the read+publish runs on the serialized chain. */
66841
- flush(path) {
66842
- this.enqueue(() => {
66843
- if (!this.path) this.adoptNow(path);
66844
- return this.doFlush();
66845
- });
66846
- }
66847
- enqueue(step) {
66848
- this.chain = this.chain.then(step).catch((e) => {
66849
- process.stderr.write(`[cotal-connector] transcript mirror: ${e.message}
66850
- `);
66851
- });
66852
- }
66853
- adoptNow(path) {
66854
- if (typeof path !== "string" || !path || this.path === path) return;
66855
- this.path = path;
66856
- this.pending = void 0;
66857
- try {
66858
- this.offset = (0, import_node_fs4.statSync)(path).size;
66859
- } catch {
66860
- this.offset = 0;
66861
- }
66862
- }
66863
- async doFlush() {
66864
- if (!this.path || !this.agent.connected) return;
66865
- if (!this.pending) {
66866
- const { lines, nextOffset } = this.readComplete();
66867
- if (nextOffset === this.offset) return;
66868
- this.pending = { chunks: chunkLines(lines.flatMap(condense), MAX_CHUNK), sent: 0, nextOffset };
66869
- }
66870
- const p = this.pending;
66871
- while (p.sent < p.chunks.length) {
66872
- await this.agent.send(p.chunks[p.sent], this.channel);
66873
- p.sent++;
66874
- }
66875
- this.offset = p.nextOffset;
66876
- this.pending = void 0;
66877
- }
66878
- /** New complete lines since the offset (a trailing partial line stays for the next flush). */
66879
- readComplete() {
66880
- const none = () => ({ lines: [], nextOffset: this.offset });
66881
- let fd;
66882
- try {
66883
- fd = (0, import_node_fs4.openSync)(this.path, "r");
66884
- } catch {
66885
- return none();
66886
- }
66887
- try {
66888
- const size = (0, import_node_fs4.fstatSync)(fd).size;
66889
- if (size < this.offset) this.offset = 0;
66890
- if (size === this.offset) return none();
66891
- const buf = Buffer.alloc(size - this.offset);
66892
- (0, import_node_fs4.readSync)(fd, buf, 0, buf.length, this.offset);
66893
- const text = buf.toString("utf8");
66894
- const lastNl = text.lastIndexOf("\n");
66895
- if (lastNl < 0) return none();
68727
+ const prior = closeOpenRun(ts);
68728
+ const runId2 = opts.mintRunId();
68729
+ open5 = runId2;
68730
+ runsOpened += 1;
68731
+ const messageId = `${uuid3}#0`;
68732
+ const selfAuthored = turnSource !== "channel";
68733
+ const carriesBody = selfAuthored && promptText.length > 0;
66896
68734
  return {
66897
- lines: text.slice(0, lastNl).split("\n").filter(Boolean),
66898
- nextOffset: this.offset + Buffer.byteLength(text.slice(0, lastNl + 1), "utf8")
68735
+ runId: runId2,
68736
+ events: [
68737
+ ...prior?.events ?? [],
68738
+ runStarted({
68739
+ threadId: opts.threadId,
68740
+ runId: runId2,
68741
+ timestamp: ts,
68742
+ cotal: { runIdSource: "connector", turnSource, ...arrivalMeta }
68743
+ }),
68744
+ ...carriesBody ? [
68745
+ textMessageStart({ messageId, timestamp: ts, role: "user", ...arrivalMeta ? { cotal: arrivalMeta } : {} }),
68746
+ textMessageContent({ messageId, delta: promptText, timestamp: ts }),
68747
+ textMessageEnd({ messageId, timestamp: ts })
68748
+ ] : []
68749
+ ]
66899
68750
  };
66900
- } finally {
66901
- (0, import_node_fs4.closeSync)(fd);
66902
68751
  }
66903
- }
66904
- };
66905
- function chunkLines(lines, max) {
66906
- const chunks = [];
66907
- let cur = "";
66908
- for (const line of lines) {
66909
- if (cur && cur.length + 1 + line.length > max) {
66910
- chunks.push(cur);
66911
- cur = line;
66912
- } else {
66913
- cur = cur ? `${cur}
66914
- ${line}` : line;
66915
- }
66916
- }
66917
- if (cur) chunks.push(cur);
66918
- return chunks;
68752
+ if (entry.type !== "assistant" || !Array.isArray(entry.message?.content)) return null;
68753
+ const runId = open5;
68754
+ entry.message.content.forEach((b, i) => {
68755
+ const messageId = `${uuid3}#${i}`;
68756
+ const meta3 = {
68757
+ ...entry.message?.id ? { providerMessageId: entry.message.id } : {},
68758
+ ...entry.message?.stop_reason ? { stopReason: entry.message.stop_reason } : {},
68759
+ ...arrivalMeta
68760
+ };
68761
+ const cotal = Object.keys(meta3).length > 0 ? { cotal: meta3 } : {};
68762
+ if (b.type === "text" && typeof b.text === "string") {
68763
+ events2.push(
68764
+ textMessageStart({ messageId, timestamp: ts, role: "assistant", ...cotal }),
68765
+ textMessageContent({ messageId, delta: b.text, timestamp: ts }),
68766
+ textMessageEnd({ messageId, timestamp: ts })
68767
+ );
68768
+ return;
68769
+ }
68770
+ if (b.type === "thinking" && opts.reasoning && typeof b.thinking === "string") {
68771
+ events2.push(
68772
+ reasoningMessageStart({ messageId, timestamp: ts, ...cotal }),
68773
+ reasoningMessageContent({ messageId, delta: b.thinking, timestamp: ts }),
68774
+ reasoningMessageEnd({ messageId, timestamp: ts })
68775
+ );
68776
+ return;
68777
+ }
68778
+ if (b.type === "tool_use" && b.id) {
68779
+ events2.push(
68780
+ toolCallStart({
68781
+ toolCallId: b.id,
68782
+ toolCallName: b.name ?? "",
68783
+ timestamp: ts,
68784
+ parentMessageId: messageId,
68785
+ ...cotal
68786
+ }),
68787
+ toolCallArgs({ toolCallId: b.id, delta: JSON.stringify(b.input ?? null), timestamp: ts }),
68788
+ toolCallEnd({ toolCallId: b.id, timestamp: ts })
68789
+ );
68790
+ }
68791
+ });
68792
+ if (events2.length === 0) return null;
68793
+ return runId === null ? null : { runId, events: events2 };
68794
+ };
68795
+ const diagnose = () => {
68796
+ if (runsOpened > 0) return null;
68797
+ if (promptShaped === 0)
68798
+ return `agui-map: no run opened \u2014 this session contains no prompt-shaped record at all (no string-content, non-compaction \`user\` entry). Nothing was refused; there was nothing to refuse.`;
68799
+ return `agui-map: NO RUN OPENED, and it was a refusal, not an empty session \u2014 ${refusedUnattributable} of ${promptShaped} prompt-shaped record(s) carry neither an \`origin.kind\` this mapper enumerates nor \`promptSource: "sdk"\`, so none of them could be attributed and none opened a run. Every event downstream of a run is therefore absent BY DECISION. If these are real prompts, the harness has a provenance shape that has not been measured: measure it and add it to ORIGIN_RULE or ABSENT_ORIGIN_RULE deliberately.`;
68800
+ };
68801
+ const forgetOpenRun = (runId) => {
68802
+ if (open5 === runId) open5 = null;
68803
+ };
68804
+ return { map: map2, closeOpenRun, openRun: () => open5, forgetOpenRun, diagnose };
66919
68805
  }
66920
68806
 
66921
68807
  // src/mcp.ts
66922
- var mirror;
66923
- var claude = createClaudeHandle({ mirror: () => mirror });
68808
+ var events;
68809
+ var claude = createClaudeHandle({ events: () => events });
66924
68810
  async function main() {
66925
68811
  if (!hasIdentity()) {
66926
68812
  process.stderr.write("[cotal-connector] no COTAL_NAME \u2014 not a managed session; staying off the mesh\n");
@@ -66930,8 +68816,37 @@ async function main() {
66930
68816
  config2.connector = "claude";
66931
68817
  const agent = new MeshAgent(config2);
66932
68818
  agent.start();
66933
- if (/^(1|true|yes|on)$/i.test(process.env.COTAL_TRANSCRIPT ?? ""))
66934
- mirror = new TranscriptMirror(agent, transcriptChannel(config2.name));
68819
+ if (/^(1|true|yes|on)$/i.test(process.env.COTAL_EVENTS ?? "")) {
68820
+ let mapper;
68821
+ events = new AguiEmitterHolder(
68822
+ async (transcriptPath) => {
68823
+ const workspaceRoot = resolveEventsStateRoot(process.env);
68824
+ const threadId = (0, import_node_path6.basename)(transcriptPath, ".jsonl");
68825
+ const principal = principalKey(agent.ep.principal.owner, agent.ep.principal.actor).key;
68826
+ const { walPath, subjectPath } = await ensureEventWalDir({ workspaceRoot, space: config2.space, principal, threadId });
68827
+ const subjectFrontier = await FileSubjectFrontier.open(subjectPath, { space: config2.space, principal });
68828
+ const wal = await EventWal.open(walPath, { space: config2.space, threadId, principal, subjectMayExist: false });
68829
+ mapper = createClaudeMapper({ threadId, mintRunId: () => (0, import_node_crypto14.randomUUID)() });
68830
+ return AguiEmitter.start({
68831
+ endpoint: agent.ep,
68832
+ wal,
68833
+ subjectFrontier,
68834
+ source: new JsonlFileSource(transcriptPath),
68835
+ map: mapper.map
68836
+ });
68837
+ },
68838
+ // Required, and not defaulted to a swallow: this runs behind a hook that must not throw, so
68839
+ // a failure reaches a human only if it is written somewhere. The holder is terminal on
68840
+ // error, it does not retry, so this line is the whole record of why events stopped.
68841
+ (e) => process.stderr.write(`[cotal-connector] AG-UI emitter stopped: ${e.message}
68842
+ `),
68843
+ // The turn terminal closes a run the record stream never described, so the mapper still
68844
+ // believes that run is open. Without this it would attribute the next records to a run the
68845
+ // published stream has already finished and the emitter would refuse the batch. Keyed on the
68846
+ // id, so a newer run opened in between is left alone.
68847
+ (runId) => mapper?.forgetOpenRun(runId)
68848
+ );
68849
+ }
66935
68850
  const controlPath = process.env.COTAL_CONTROL_SOCKET;
66936
68851
  const controlToken = process.env.COTAL_CONTROL_TOKEN;
66937
68852
  if (!controlPath || !controlToken) {