@cotal-ai/connector-hermes 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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/connector-hermes",
3
3
  "description": "Cotal connector for the Hermes (Nous Research) agent.",
4
- "version": "0.8.0",
4
+ "version": "0.8.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -23,14 +23,14 @@
23
23
  "dependencies": {
24
24
  "tsx": "^4.22.4",
25
25
  "zod": "^4.4.3",
26
- "@cotal-ai/connector-core": "0.8.0"
26
+ "@cotal-ai/connector-core": "0.8.1"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@cotal-ai/core": ">=0.1.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "esbuild": "^0.28.0",
33
- "@cotal-ai/core": "0.8.0"
33
+ "@cotal-ai/core": "0.8.1"
34
34
  },
35
35
  "files": [
36
36
  "bin",
@@ -14858,6 +14858,10 @@ function deliveryBucket(space) {
14858
14858
  function leaseKey(shardIndex) {
14859
14859
  return `lease.${shardIndex}`;
14860
14860
  }
14861
+ function managerBucket(space) {
14862
+ return `cotal_manager_${token(space)}`;
14863
+ }
14864
+ var MANAGER_LEASE_KEY = "lease";
14861
14865
  function chatStream(space) {
14862
14866
  return `CHAT_${token(space)}`;
14863
14867
  }
@@ -16615,6 +16619,7 @@ var import_kv = __toESM(require_mod6(), 1);
16615
16619
  var MAX_MSGS_PER_SUBJECT = 1e3;
16616
16620
  var PLANE3_DEDUP_WINDOW_MS = 2 * 60 * 60 * 1e3;
16617
16621
  var DINBOX_MAX_ACK_PENDING = 1e3;
16622
+ var MANAGER_LEASE_TTL_MS = 1e4;
16618
16623
  var MEMBERSHIP_MAX_BYTES = 64 * 1024 * 1024;
16619
16624
  async function createSpaceStreams(jsm, space) {
16620
16625
  const p = spacePrefix(space);
@@ -17070,6 +17075,7 @@ var CotalEndpoint = class extends import_node_events.EventEmitter {
17070
17075
  membersKv;
17071
17076
  aclKv;
17072
17077
  deliveryKv;
17078
+ managerLeaseKv;
17073
17079
  membershipKv;
17074
17080
  /** The live `ctl.delivery` serve subscription (delivery daemon) — re-created on every (re)connect by
17075
17081
  * {@link armDeliveryControl}; tracked so the stale one is dropped on reconnect. */
@@ -17999,6 +18005,68 @@ var CotalEndpoint = class extends import_node_events.EventEmitter {
17999
18005
  return void 0;
18000
18006
  }
18001
18007
  }
18008
+ /** Ensure + bind the manager singleton-lease bucket. NOTE: `Kvm.open` binds LAZILY — it does NOT
18009
+ * verify the stream exists or throw when it's missing, so it can't "create if absent" (a fresh bucket
18010
+ * then fails 'stream not found' on the first write — unlike the delivery bucket, which is pre-created
18011
+ * at `cotal up`). `create` is the ensure-exists call: it makes the bucket (bucket-level TTL) or, when
18012
+ * another manager already did, throws — and we bind the now-existing one. Either way the per-KEY CAS
18013
+ * create stays the only single-flight gate, so a lost bucket-create race never reads as "lease held".
18014
+ * The manager is allow-all, so it may create. */
18015
+ async managerLeaseRegistry() {
18016
+ if (!this.nc)
18017
+ throw new Error("endpoint not started");
18018
+ if (this.managerLeaseKv)
18019
+ return this.managerLeaseKv;
18020
+ const kvm = new import_kv7.Kvm(this.nc);
18021
+ try {
18022
+ this.managerLeaseKv = await kvm.create(managerBucket(this.space), { ttl: MANAGER_LEASE_TTL_MS });
18023
+ } catch {
18024
+ this.managerLeaseKv = await kvm.open(managerBucket(this.space));
18025
+ }
18026
+ return this.managerLeaseKv;
18027
+ }
18028
+ encodeManagerLease(info) {
18029
+ return new TextEncoder().encode(JSON.stringify(info));
18030
+ }
18031
+ /** Acquire the singleton manager lease via ATOMIC CAS create. THROWS if a live lease exists (a loud
18032
+ * refusal-to-start, never a retry) so two managers never split control. A crashed holder's lease
18033
+ * auto-expires (bucket TTL). Returns the lease revision (for renew). */
18034
+ async acquireManagerLease(info) {
18035
+ return (await this.managerLeaseRegistry()).create(MANAGER_LEASE_KEY, this.encodeManagerLease({ ...info, since: Date.now() }));
18036
+ }
18037
+ /** Renew the held lease (CAS update against `revision`) before the bucket TTL expires it. Throws if the
18038
+ * revision moved (lost the lease). Returns the new revision. */
18039
+ async renewManagerLease(info, revision) {
18040
+ return (await this.managerLeaseRegistry()).update(MANAGER_LEASE_KEY, this.encodeManagerLease({ ...info, since: Date.now() }), revision);
18041
+ }
18042
+ /** Release the held lease on clean shutdown so a replacement manager acquires immediately. CAS-guarded
18043
+ * by `revision`: if we already LOST the lease (renew gap / another manager took over) the stored
18044
+ * revision has moved, the conditional delete no-ops, and we never delete the replacement's live lease. */
18045
+ async releaseManagerLease(revision) {
18046
+ try {
18047
+ const kv = await this.managerLeaseRegistry();
18048
+ if (revision === void 0)
18049
+ await kv.delete(MANAGER_LEASE_KEY);
18050
+ else
18051
+ await kv.delete(MANAGER_LEASE_KEY, { previousSeq: revision });
18052
+ } catch {
18053
+ }
18054
+ }
18055
+ /** Read the live manager lease, or undefined if none (bucket absent / key deleted/expired). Open-only —
18056
+ * never creates the bucket, so a probe that finds no manager leaves none behind. */
18057
+ async readManagerLease() {
18058
+ if (!this.nc)
18059
+ return void 0;
18060
+ try {
18061
+ const kv = await new import_kv7.Kvm(this.nc).open(managerBucket(this.space));
18062
+ const e = await kv.get(MANAGER_LEASE_KEY);
18063
+ if (!e || e.operation === "DEL" || e.operation === "PURGE")
18064
+ return void 0;
18065
+ return e.json();
18066
+ } catch {
18067
+ return void 0;
18068
+ }
18069
+ }
18002
18070
  /** Privileged: one owner's NON-TOMBSTONED durable memberships as `{channel, generation, activated}` —
18003
18071
  * the server-side delivery daemon serves this to a connecting agent (the `listMemberships` op on
18004
18072
  * `ctl.delivery`). The agent seeds its leave mirror from the ACTIVATED ones (the confirmed backstops),
@@ -19635,14 +19703,15 @@ var MeshAgent = class extends import_node_events2.EventEmitter {
19635
19703
  /** Ask the manager to spawn a new teammate into this space (its `start` op).
19636
19704
  * How it lands — a detached PTY, a tmux window, a cmux tab — is the manager's
19637
19705
  * runtime; from here it just joins the mesh as a lateral peer. `opts.agent` picks
19638
- * the harness (default the manager's `cotal`/Claude) and `opts.model` overrides the
19639
- * persona file's `model:` the same knobs the operator's `cotal start` carries, so
19706
+ * the harness (default the manager's `cotal`/Claude), `opts.model` overrides the
19707
+ * persona file's `model:`, and `opts.cwd` roots the new peer at a different folder/repo
19708
+ * than the manager's workspace — the same knobs the operator's `cotal start` carries, so
19640
19709
  * the agent and operator spawn doors share one control-op contract. */
19641
19710
  async spawn(name, role, opts) {
19642
19711
  this.assertConnected();
19643
19712
  return this.ep.requestControl(CONTROL_PRIVILEGED, {
19644
19713
  op: "start",
19645
- args: { name, role, agent: opts?.agent, model: opts?.model }
19714
+ args: { name, role, agent: opts?.agent, model: opts?.model, cwd: opts?.cwd }
19646
19715
  });
19647
19716
  }
19648
19717
  /** Ask the manager to tear a teammate down (its `stop` op). Graceful by default —
@@ -34718,11 +34787,12 @@ ${info}${caught}`);
34718
34787
  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."),
34719
34788
  role: external_exports.string().optional().describe("Optional role for the new peer (e.g. worker, reviewer); overrides the persona file's role."),
34720
34789
  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)."),
34721
- model: external_exports.string().optional().describe("Optional model override (e.g. opus, sonnet) \u2014 wins over the persona file's model:.")
34790
+ model: external_exports.string().optional().describe("Optional model override (e.g. opus, sonnet) \u2014 wins over the persona file's model:."),
34791
+ 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.")
34722
34792
  },
34723
- async run(agent, _config, { name, role, agent: agentType, model }) {
34793
+ async run(agent, _config, { name, role, agent: agentType, model, cwd }) {
34724
34794
  try {
34725
- const reply = await agent.spawn(name, role, { agent: agentType, model });
34795
+ const reply = await agent.spawn(name, role, { agent: agentType, model, cwd });
34726
34796
  if (!reply.ok)
34727
34797
  return err(`Couldn't spawn ${name}: ${reply.error ?? "manager refused"}`);
34728
34798
  const d = reply.data;