@cotal-ai/connector-claude-code 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mcp.cjs +76 -6
  2. package/package.json +3 -3
package/dist/mcp.cjs CHANGED
@@ -45774,6 +45774,10 @@ function deliveryBucket(space) {
45774
45774
  function leaseKey(shardIndex) {
45775
45775
  return `lease.${shardIndex}`;
45776
45776
  }
45777
+ function managerBucket(space) {
45778
+ return `cotal_manager_${token(space)}`;
45779
+ }
45780
+ var MANAGER_LEASE_KEY = "lease";
45777
45781
  function chatStream(space) {
45778
45782
  return `CHAT_${token(space)}`;
45779
45783
  }
@@ -47531,6 +47535,7 @@ var import_kv = __toESM(require_mod6(), 1);
47531
47535
  var MAX_MSGS_PER_SUBJECT = 1e3;
47532
47536
  var PLANE3_DEDUP_WINDOW_MS = 2 * 60 * 60 * 1e3;
47533
47537
  var DINBOX_MAX_ACK_PENDING = 1e3;
47538
+ var MANAGER_LEASE_TTL_MS = 1e4;
47534
47539
  var MEMBERSHIP_MAX_BYTES = 64 * 1024 * 1024;
47535
47540
  async function createSpaceStreams(jsm, space) {
47536
47541
  const p = spacePrefix(space);
@@ -47986,6 +47991,7 @@ var CotalEndpoint = class extends import_node_events.EventEmitter {
47986
47991
  membersKv;
47987
47992
  aclKv;
47988
47993
  deliveryKv;
47994
+ managerLeaseKv;
47989
47995
  membershipKv;
47990
47996
  /** The live `ctl.delivery` serve subscription (delivery daemon) — re-created on every (re)connect by
47991
47997
  * {@link armDeliveryControl}; tracked so the stale one is dropped on reconnect. */
@@ -48915,6 +48921,68 @@ var CotalEndpoint = class extends import_node_events.EventEmitter {
48915
48921
  return void 0;
48916
48922
  }
48917
48923
  }
48924
+ /** Ensure + bind the manager singleton-lease bucket. NOTE: `Kvm.open` binds LAZILY — it does NOT
48925
+ * verify the stream exists or throw when it's missing, so it can't "create if absent" (a fresh bucket
48926
+ * then fails 'stream not found' on the first write — unlike the delivery bucket, which is pre-created
48927
+ * at `cotal up`). `create` is the ensure-exists call: it makes the bucket (bucket-level TTL) or, when
48928
+ * another manager already did, throws — and we bind the now-existing one. Either way the per-KEY CAS
48929
+ * create stays the only single-flight gate, so a lost bucket-create race never reads as "lease held".
48930
+ * The manager is allow-all, so it may create. */
48931
+ async managerLeaseRegistry() {
48932
+ if (!this.nc)
48933
+ throw new Error("endpoint not started");
48934
+ if (this.managerLeaseKv)
48935
+ return this.managerLeaseKv;
48936
+ const kvm = new import_kv7.Kvm(this.nc);
48937
+ try {
48938
+ this.managerLeaseKv = await kvm.create(managerBucket(this.space), { ttl: MANAGER_LEASE_TTL_MS });
48939
+ } catch {
48940
+ this.managerLeaseKv = await kvm.open(managerBucket(this.space));
48941
+ }
48942
+ return this.managerLeaseKv;
48943
+ }
48944
+ encodeManagerLease(info) {
48945
+ return new TextEncoder().encode(JSON.stringify(info));
48946
+ }
48947
+ /** Acquire the singleton manager lease via ATOMIC CAS create. THROWS if a live lease exists (a loud
48948
+ * refusal-to-start, never a retry) so two managers never split control. A crashed holder's lease
48949
+ * auto-expires (bucket TTL). Returns the lease revision (for renew). */
48950
+ async acquireManagerLease(info) {
48951
+ return (await this.managerLeaseRegistry()).create(MANAGER_LEASE_KEY, this.encodeManagerLease({ ...info, since: Date.now() }));
48952
+ }
48953
+ /** Renew the held lease (CAS update against `revision`) before the bucket TTL expires it. Throws if the
48954
+ * revision moved (lost the lease). Returns the new revision. */
48955
+ async renewManagerLease(info, revision) {
48956
+ return (await this.managerLeaseRegistry()).update(MANAGER_LEASE_KEY, this.encodeManagerLease({ ...info, since: Date.now() }), revision);
48957
+ }
48958
+ /** Release the held lease on clean shutdown so a replacement manager acquires immediately. CAS-guarded
48959
+ * by `revision`: if we already LOST the lease (renew gap / another manager took over) the stored
48960
+ * revision has moved, the conditional delete no-ops, and we never delete the replacement's live lease. */
48961
+ async releaseManagerLease(revision) {
48962
+ try {
48963
+ const kv = await this.managerLeaseRegistry();
48964
+ if (revision === void 0)
48965
+ await kv.delete(MANAGER_LEASE_KEY);
48966
+ else
48967
+ await kv.delete(MANAGER_LEASE_KEY, { previousSeq: revision });
48968
+ } catch {
48969
+ }
48970
+ }
48971
+ /** Read the live manager lease, or undefined if none (bucket absent / key deleted/expired). Open-only —
48972
+ * never creates the bucket, so a probe that finds no manager leaves none behind. */
48973
+ async readManagerLease() {
48974
+ if (!this.nc)
48975
+ return void 0;
48976
+ try {
48977
+ const kv = await new import_kv7.Kvm(this.nc).open(managerBucket(this.space));
48978
+ const e = await kv.get(MANAGER_LEASE_KEY);
48979
+ if (!e || e.operation === "DEL" || e.operation === "PURGE")
48980
+ return void 0;
48981
+ return e.json();
48982
+ } catch {
48983
+ return void 0;
48984
+ }
48985
+ }
48918
48986
  /** Privileged: one owner's NON-TOMBSTONED durable memberships as `{channel, generation, activated}` —
48919
48987
  * the server-side delivery daemon serves this to a connecting agent (the `listMemberships` op on
48920
48988
  * `ctl.delivery`). The agent seeds its leave mirror from the ACTIVATED ones (the confirmed backstops),
@@ -50558,14 +50626,15 @@ var MeshAgent = class extends import_node_events2.EventEmitter {
50558
50626
  /** Ask the manager to spawn a new teammate into this space (its `start` op).
50559
50627
  * How it lands — a detached PTY, a tmux window, a cmux tab — is the manager's
50560
50628
  * runtime; from here it just joins the mesh as a lateral peer. `opts.agent` picks
50561
- * the harness (default the manager's `cotal`/Claude) and `opts.model` overrides the
50562
- * persona file's `model:` the same knobs the operator's `cotal start` carries, so
50629
+ * the harness (default the manager's `cotal`/Claude), `opts.model` overrides the
50630
+ * persona file's `model:`, and `opts.cwd` roots the new peer at a different folder/repo
50631
+ * than the manager's workspace — the same knobs the operator's `cotal start` carries, so
50563
50632
  * the agent and operator spawn doors share one control-op contract. */
50564
50633
  async spawn(name, role, opts) {
50565
50634
  this.assertConnected();
50566
50635
  return this.ep.requestControl(CONTROL_PRIVILEGED, {
50567
50636
  op: "start",
50568
- args: { name, role, agent: opts?.agent, model: opts?.model }
50637
+ args: { name, role, agent: opts?.agent, model: opts?.model, cwd: opts?.cwd }
50569
50638
  });
50570
50639
  }
50571
50640
  /** Ask the manager to tear a teammate down (its `stop` op). Graceful by default —
@@ -51154,11 +51223,12 @@ ${info}${caught}`);
51154
51223
  name: external_exports.string().describe("Which persona to spawn \u2014 the persona FILENAME in .cotal/agents (e.g. `review-critic`), without the .md. The new peer joins under the persona's own `name:` (auto-numbered, e.g. socrates-2, if that's taken). Fails if no such persona file exists \u2014 spawn an existing persona, don't invent a name."),
51155
51224
  role: external_exports.string().optional().describe("Optional role for the new peer (e.g. worker, reviewer); overrides the persona file's role."),
51156
51225
  agent: external_exports.string().optional().describe("Optional harness the new peer runs on \u2014 the agent/connector type (claude, opencode, hermes), NOT the persona to spawn (that's `name`). Defaults to the manager's default (Claude)."),
51157
- model: external_exports.string().optional().describe("Optional model override (e.g. opus, sonnet) \u2014 wins over the persona file's model:.")
51226
+ model: external_exports.string().optional().describe("Optional model override (e.g. opus, sonnet) \u2014 wins over the persona file's model:."),
51227
+ cwd: external_exports.string().optional().describe("Optional working directory to root the new peer at (e.g. a different repo). A relative path resolves against the manager's workspace; omitted \u2192 it shares the manager's workspace.")
51158
51228
  },
51159
- async run(agent, _config, { name, role, agent: agentType, model }) {
51229
+ async run(agent, _config, { name, role, agent: agentType, model, cwd }) {
51160
51230
  try {
51161
- const reply = await agent.spawn(name, role, { agent: agentType, model });
51231
+ const reply = await agent.spawn(name, role, { agent: agentType, model, cwd });
51162
51232
  if (!reply.ok)
51163
51233
  return err(`Couldn't spawn ${name}: ${reply.error ?? "manager refused"}`);
51164
51234
  const d = reply.data;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/connector-claude-code",
3
3
  "description": "Cotal connector for Claude Code: an installed plugin that joins a session to the mesh.",
4
- "version": "0.8.0",
4
+ "version": "0.8.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@modelcontextprotocol/sdk": "^1.29.0",
22
- "@cotal-ai/connector-core": "0.8.0"
22
+ "@cotal-ai/connector-core": "0.8.1"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@cotal-ai/core": ">=0.1.0"
@@ -27,7 +27,7 @@
27
27
  "devDependencies": {
28
28
  "esbuild": "^0.28.0",
29
29
  "tsx": "^4.22.4",
30
- "@cotal-ai/core": "0.8.0"
30
+ "@cotal-ai/core": "0.8.1"
31
31
  },
32
32
  "files": [
33
33
  "dist",