@chipschat/hermes 1.0.7 → 1.0.8

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/main.js CHANGED
@@ -8,11 +8,11 @@ var __export = (target, all) => {
8
8
  };
9
9
 
10
10
  // src/main.ts
11
- import { randomBytes as randomBytes2 } from "node:crypto";
12
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
11
+ import { randomBytes as randomBytes3 } from "node:crypto";
12
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "node:fs";
13
13
  import { createServer as createServer2 } from "node:net";
14
14
  import { homedir as homedir8 } from "node:os";
15
- import { dirname as dirname6, join as join12, resolve as resolve6 } from "node:path";
15
+ import { dirname as dirname6, join as join13, resolve as resolve6 } from "node:path";
16
16
 
17
17
  // node_modules/zod/v4/classic/external.js
18
18
  var external_exports = {};
@@ -25555,7 +25555,7 @@ function fileStore(path) {
25555
25555
 
25556
25556
  // src/bridge.ts
25557
25557
  import { mkdirSync as mkdirSync5 } from "node:fs";
25558
- import { join as join8 } from "node:path";
25558
+ import { join as join10 } from "node:path";
25559
25559
 
25560
25560
  // src/context.ts
25561
25561
  import { spawn } from "node:child_process";
@@ -25578,7 +25578,7 @@ import { basename as basename3, dirname as dirname4, join as join6 } from "node:
25578
25578
 
25579
25579
  // src/home.ts
25580
25580
  import { execFileSync as execFileSync3 } from "node:child_process";
25581
- import { existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
25581
+ import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
25582
25582
  import { homedir as homedir4 } from "node:os";
25583
25583
  import { join as join5, resolve as resolve2 } from "node:path";
25584
25584
  function hermesRoot(env = process.env) {
@@ -25605,6 +25605,14 @@ function envValue(home2, name2) {
25605
25605
  }
25606
25606
  return found;
25607
25607
  }
25608
+ function appendEnv(home2, name2, value) {
25609
+ const path = join5(home2, ".env");
25610
+ const text = readFileSync5(path, "utf8");
25611
+ const mode = statSync(path).mode & 511;
25612
+ appendFileSync(path, `${text === "" || text.endsWith("\n") ? "" : "\n"}${name2}=${value}
25613
+ `);
25614
+ chmodSync3(path, mode);
25615
+ }
25608
25616
  function configText(home2) {
25609
25617
  const path = join5(home2, "config.yaml");
25610
25618
  try {
@@ -25839,13 +25847,9 @@ function fileOf(source2, name2) {
25839
25847
  }
25840
25848
  var byOrder = (a, b) => ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind) || a.profile.localeCompare(b.profile);
25841
25849
  var Picture = class {
25842
- /** Each file by its path, and the agents that read it. */
25843
- readers = /* @__PURE__ */ new Map();
25844
- /** The shared files, each with its id and the name the set shows. */
25845
- shared = /* @__PURE__ */ new Map();
25846
- /** The agents and the files each reads, in page order. */
25847
- agents = /* @__PURE__ */ new Map();
25848
- constructor(readers, first, env = process.env) {
25850
+ constructor(readers, first, env = process.env, ownProfiles = false) {
25851
+ this.first = first;
25852
+ this.ownProfiles = ownProfiles;
25849
25853
  const byProfile = /* @__PURE__ */ new Map();
25850
25854
  const sourcesFor = (profile) => {
25851
25855
  let sources = byProfile.get(profile.home);
@@ -25867,7 +25871,7 @@ var Picture = class {
25867
25871
  }
25868
25872
  const taken = /* @__PURE__ */ new Set();
25869
25873
  const ids2 = /* @__PURE__ */ new Set();
25870
- const sharing = [...this.readers.values()].filter(({ source: source2, agents }) => source2.kind === "project" || source2.profile === first?.name || agents.length > 1).sort((a, b) => (a.source.profile === first?.name ? 0 : 1) - (b.source.profile === first?.name ? 0 : 1) || byOrder(a.source, b.source));
25874
+ const sharing = [...this.readers.values()].filter(({ source: source2, agents }) => source2.kind === "project" || this.forNewAgents(source2) || agents.length > 1).sort((a, b) => (a.source.profile === first?.name ? 0 : 1) - (b.source.profile === first?.name ? 0 : 1) || byOrder(a.source, b.source));
25871
25875
  for (const { source: source2 } of sharing) {
25872
25876
  const name2 = taken.has(source2.file) ? `${source2.file} (${source2.profile})` : source2.file;
25873
25877
  taken.add(name2);
@@ -25877,8 +25881,24 @@ var Picture = class {
25877
25881
  this.shared.set(source2.path, { id: id2, name: name2 });
25878
25882
  }
25879
25883
  }
25884
+ first;
25885
+ ownProfiles;
25886
+ /** Each file by its path, and the agents that read it. */
25887
+ readers = /* @__PURE__ */ new Map();
25888
+ /** The shared files, each with its id and the name the set shows. */
25889
+ shared = /* @__PURE__ */ new Map();
25890
+ /** The agents and the files each reads, in page order. */
25891
+ agents = /* @__PURE__ */ new Map();
25892
+ /**
25893
+ * Whether each new agent starts with this file or a copy of it: the first profile's files, or
25894
+ * where each new agent gets a profile of its own, the first profile's SOUL.md and the project file.
25895
+ */
25896
+ forNewAgents(source2) {
25897
+ if (source2.profile !== this.first?.name) return false;
25898
+ return !this.ownProfiles || source2.kind === "soul" || source2.kind === "project";
25899
+ }
25880
25900
  /** The harness's own set, as `PUT /v1/harnesses/:id/context` takes it. */
25881
- harnessCopy(first) {
25901
+ harnessCopy() {
25882
25902
  const shown2 = [];
25883
25903
  const notShown = [];
25884
25904
  for (const [path, { id: id2, name: name2 }] of this.shared) {
@@ -25894,8 +25914,7 @@ var Picture = class {
25894
25914
  ...file3,
25895
25915
  id: id2,
25896
25916
  used_by: agents.length,
25897
- // A new agent runs on the first profile: it reads that profile's files and its project file.
25898
- ...source2.profile === first?.name ? { for_new_agents: true } : {}
25917
+ ...this.forNewAgents(source2) ? { for_new_agents: true } : {}
25899
25918
  });
25900
25919
  }
25901
25920
  return {
@@ -26056,9 +26075,9 @@ var Contexts = class {
26056
26075
  /** Every copy read again, and sent where it moved. */
26057
26076
  refresh() {
26058
26077
  return this.inLine(async () => {
26059
- const { readers, first } = this.roster();
26060
- const picture = new Picture(readers, first, this.env);
26061
- await this.port.harness(picture.harnessCopy(first)).catch((error62) => {
26078
+ const { readers, first, ownProfiles } = this.roster();
26079
+ const picture = new Picture(readers, first, this.env, ownProfiles);
26080
+ await this.port.harness(picture.harnessCopy()).catch((error62) => {
26062
26081
  this.log(`the harness's context not sent: ${messageOf(error62)}`);
26063
26082
  });
26064
26083
  for (const { agentId: agentId2 } of readers) {
@@ -26076,12 +26095,12 @@ var Contexts = class {
26076
26095
  */
26077
26096
  async edit(frame) {
26078
26097
  const answer = await this.inLine(async () => {
26079
- const { readers, first } = this.roster();
26098
+ const { readers, first, ownProfiles } = this.roster();
26080
26099
  const copyNow = () => {
26081
- const picture = new Picture(readers, first, this.env);
26082
- return frame.agent_id === void 0 ? picture.harnessCopy(first) : picture.agentCopy(frame.agent_id);
26100
+ const picture = new Picture(readers, first, this.env, ownProfiles);
26101
+ return frame.agent_id === void 0 ? picture.harnessCopy() : picture.agentCopy(frame.agent_id);
26083
26102
  };
26084
- const found = new Picture(readers, first, this.env).find(frame.agent_id, frame.name);
26103
+ const found = new Picture(readers, first, this.env, ownProfiles).find(frame.agent_id, frame.name);
26085
26104
  if (found === void 0) {
26086
26105
  throw new Refuse(`Hermes gives this agent no file called ${frame.name}.`, copyNow(), "failed");
26087
26106
  }
@@ -26148,6 +26167,8 @@ function numbersOf(profile, body) {
26148
26167
  if (typeof body !== "object" || body === null) return void 0;
26149
26168
  const top = body;
26150
26169
  if (top["hook_event_name"] !== "post_api_request") return void 0;
26170
+ const sender = top["profile"];
26171
+ if (typeof sender === "string" && sender !== profile && sender !== "custom") return void 0;
26151
26172
  const extra = typeof top["extra"] === "object" && top["extra"] !== null ? top["extra"] : {};
26152
26173
  const named = typeof top["session_id"] === "string" ? top["session_id"] : "";
26153
26174
  const task2 = typeof extra["task_id"] === "string" && extra["task_id"] !== "" ? extra["task_id"] : void 0;
@@ -26453,6 +26474,13 @@ var HermesClient = class {
26453
26474
  const latest = read["session_id"];
26454
26475
  return typeof latest === "string" && latest !== "" ? latest : sessionId;
26455
26476
  }
26477
+ /**
26478
+ * The models Hermes has for this profile, as its own picker lists them: `GET /api/model/options`,
26479
+ * only the providers the profile is set up for (`models.ts`).
26480
+ */
26481
+ async modelOptions() {
26482
+ return await this.call("GET", "/api/model/options?include_unconfigured=false");
26483
+ }
26456
26484
  /** A run in a session: what it is told, and under a key a retry of the same turn repeats. */
26457
26485
  async startRun(input2) {
26458
26486
  const started = await this.call(
@@ -26462,6 +26490,7 @@ var HermesClient = class {
26462
26490
  input: input2.text,
26463
26491
  session_id: input2.sessionId,
26464
26492
  ...input2.model === void 0 ? {} : { model: input2.model },
26493
+ ...input2.provider === void 0 ? {} : { provider: input2.provider },
26465
26494
  ...input2.instructions === void 0 ? {} : { instructions: input2.instructions }
26466
26495
  },
26467
26496
  { "Idempotency-Key": input2.key.slice(0, 255) }
@@ -26558,7 +26587,7 @@ async function failureOf(answer) {
26558
26587
  }
26559
26588
 
26560
26589
  // src/hooks.ts
26561
- import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
26590
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
26562
26591
  import { join as join7 } from "node:path";
26563
26592
  var HOOK_NAME = "chips";
26564
26593
  var SECRET_ENV = "CHIPS_HOOK_SECRET";
@@ -26595,12 +26624,7 @@ function entryLines(indent, url2, secret) {
26595
26624
  function secretInEnv(home2, secret) {
26596
26625
  const path = join7(home2, ".env");
26597
26626
  if (!existsSync7(path)) return false;
26598
- if (envValue(home2, SECRET_ENV) !== void 0) return true;
26599
- const text = readFileSync7(path, "utf8");
26600
- const mode = statSync3(path).mode & 511;
26601
- appendFileSync(path, `${text === "" || text.endsWith("\n") ? "" : "\n"}${SECRET_ENV}=${secret}
26602
- `);
26603
- chmodSync3(path, mode);
26627
+ if (envValue(home2, SECRET_ENV) === void 0) appendEnv(home2, SECRET_ENV, secret);
26604
26628
  return true;
26605
26629
  }
26606
26630
  function addHook(home2, port, profile, secret) {
@@ -26661,1417 +26685,1804 @@ function addHook(home2, port, profile, secret) {
26661
26685
  return { state: "added", path };
26662
26686
  }
26663
26687
 
26664
- // src/bridge.ts
26665
- var WANTS = ["agents", "new_agents", "forks", "context_files", "context_windows", "restarts"];
26666
- function settingsFor(active) {
26667
- const bridge2 = () => {
26668
- const there = active();
26669
- if (there === void 0) throw new Refuse("The bridge is starting. Try again in a moment.");
26670
- return there;
26671
- };
26688
+ // src/models.ts
26689
+ var MODEL_CHOICES_MAX2 = 40;
26690
+ var LABEL_MAX = 80;
26691
+ var DEFAULT_LABEL = "Hermes's default";
26692
+ function choiceId(provider, model) {
26693
+ return `${provider}/${model}`;
26694
+ }
26695
+ function runModelOf(id2) {
26696
+ const at = id2.indexOf("/");
26697
+ return at <= 0 ? { model: id2 } : { provider: id2.slice(0, at), model: id2.slice(at + 1) };
26698
+ }
26699
+ function providersOf(options) {
26700
+ const ready = (options.providers ?? []).filter(
26701
+ (provider) => provider.authenticated !== false && (provider.models ?? []).length > 0
26702
+ );
26703
+ const current = (provider) => provider.is_current === true || provider.slug === options.provider;
26704
+ return [...ready.filter(current), ...ready.filter((provider) => !current(provider))];
26705
+ }
26706
+ function choicesOf(options) {
26707
+ const choices = [];
26708
+ for (const provider of providersOf(options)) {
26709
+ for (const model of provider.models ?? []) {
26710
+ const id2 = choiceId(provider.slug, model);
26711
+ if (choices.some((choice) => choice.id === id2)) continue;
26712
+ choices.push({ id: id2, label: `${model} \xB7 ${provider.name ?? provider.slug}`.slice(0, LABEL_MAX), efforts: [] });
26713
+ }
26714
+ }
26715
+ return choices;
26716
+ }
26717
+ function defaultOf(options) {
26718
+ const model = options.model?.trim();
26719
+ if (model === void 0 || model === "") return void 0;
26720
+ return options.provider === void 0 || options.provider === "" ? model : choiceId(options.provider, model);
26721
+ }
26722
+ function matched(choices, model) {
26723
+ if (choices.some((choice) => choice.id === model)) return model;
26724
+ return choices.find((choice) => runModelOf(choice.id).model === model)?.id ?? model;
26725
+ }
26726
+ function copyOf(options, own2) {
26727
+ let choices = choicesOf(options);
26728
+ const fallback = defaultOf(options);
26729
+ const byDefault = fallback === void 0 ? void 0 : matched(choices, fallback);
26730
+ const mine = own2 === void 0 ? void 0 : matched(choices, own2);
26731
+ const model = mine ?? byDefault ?? choices[0]?.id;
26732
+ if (model === void 0) return void 0;
26733
+ const keep = [model, ...byDefault === void 0 ? [] : [byDefault]];
26734
+ for (const id2 of keep) {
26735
+ if (!choices.some((choice) => choice.id === id2)) choices = [{ id: id2, label: id2.slice(0, LABEL_MAX), efforts: [] }, ...choices];
26736
+ }
26737
+ let others = MODEL_CHOICES_MAX2 - keep.filter((id2, at) => keep.indexOf(id2) === at).length;
26738
+ choices = choices.filter((choice) => keep.includes(choice.id) || others-- > 0);
26672
26739
  return {
26673
- context: (edit) => bridge2().onContextEdit(edit),
26674
- contextWindows: {
26675
- compact: async (ask2) => bridge2().onWindowCompact(ask2),
26676
- change: async (ask2) => bridge2().onWindowChange(ask2)
26677
- }
26740
+ model,
26741
+ own: mine !== void 0,
26742
+ ...byDefault === void 0 ? {} : { default: { label: DEFAULT_LABEL, model: byDefault } },
26743
+ choices,
26744
+ editable: true
26678
26745
  };
26679
26746
  }
26680
- function filesHere(project = false) {
26681
- return `${sendingFiles({ project })} Hermes's note that this API server sends only pictures, and shows a MEDIA: tag on the runs endpoint as text, does not hold for these runs: they come from Chips, whose bridge takes each MEDIA: line out of your answer and attaches that file to your message, whatever its type, up to 100 MB.`;
26747
+
26748
+ // src/profiles.ts
26749
+ import { randomBytes as randomBytes2 } from "node:crypto";
26750
+ import { existsSync as existsSync9, readFileSync as readFileSync9, rmSync as rmSync4 } from "node:fs";
26751
+ import { createConnection } from "node:net";
26752
+ import { join as join9 } from "node:path";
26753
+
26754
+ // src/gateway.ts
26755
+ import { spawn as spawn2 } from "node:child_process";
26756
+ import { createHash as createHash3 } from "node:crypto";
26757
+ import { accessSync as accessSync2, appendFileSync as appendFileSync2, closeSync as closeSync2, constants as constants2, existsSync as existsSync8, openSync as openSync2, readFileSync as readFileSync8, readSync, statSync as statSync3 } from "node:fs";
26758
+ import { homedir as homedir5 } from "node:os";
26759
+ import { basename as basename4, delimiter as delimiter2, dirname as dirname5, join as join8, resolve as resolve3 } from "node:path";
26760
+ var TIMING = {
26761
+ stopMs: 15e3,
26762
+ killMs: 5e3,
26763
+ earlyMs: 1e4,
26764
+ upMs: 9e4,
26765
+ serviceMs: 15e4,
26766
+ pollMs: 500
26767
+ };
26768
+ var EXPAT = "/opt/homebrew/opt/expat/lib/libexpat.1.dylib";
26769
+ var EXPAT_SYMBOL = "_XML_SetAllocTrackerActivationThreshold";
26770
+ function gatewaysOf(agents) {
26771
+ return [...new Set(agents.map(({ address }) => address.url.replace(/\/+$/, "")))].map((url2) => ({ url: url2 }));
26682
26772
  }
26683
- var FILES_HERE = filesHere();
26684
- function instructionsFor(folder2, project = false) {
26685
- const files = filesHere(project);
26686
- return folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
26773
+ function isLocal(url2) {
26774
+ try {
26775
+ return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(new URL(url2).hostname);
26776
+ } catch {
26777
+ return false;
26778
+ }
26687
26779
  }
26688
- function freeFolder(root2, name2, taken) {
26689
- const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
26690
- for (let count = 1; ; count += 1) {
26691
- const folder2 = join8(root2, count === 1 ? slug : `${slug}-${count}`);
26692
- if (!taken.includes(folder2)) return folder2;
26780
+ function portOf(url2) {
26781
+ const parsed = new URL(url2);
26782
+ return Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
26783
+ }
26784
+ function line(words4) {
26785
+ const one = words4.replace(/\s+/g, " ").trim();
26786
+ return one.length <= 200 ? one : `${one.slice(0, 199)}\u2026`;
26787
+ }
26788
+ function whyFrom(output2, code) {
26789
+ const lines2 = output2.split("\n").map((each) => each.trim()).filter((each) => each !== "");
26790
+ const why = lines2.findLast((each) => /^[A-Za-z_][\w.]*(Error|Exception)\b/.test(each)) ?? lines2.at(-1);
26791
+ if (why === void 0) return code === null || code === void 0 ? "it was stopped" : `it exited ${code}`;
26792
+ return why.length <= 200 ? why : why.replace(/\/(?:[^\s/(),:'"]+\/)+([^\s/(),:'"]+)/g, "$1");
26793
+ }
26794
+ function gatewayRunArgs(command) {
26795
+ const words4 = command.trim().split(/\s+/);
26796
+ const at = words4.findIndex((word) => basename4(word) === "hermes");
26797
+ if (at === -1) return void 0;
26798
+ const args = words4.slice(at + 1);
26799
+ const plain = [];
26800
+ for (let next = 0; next < args.length; next += 1) {
26801
+ const word = args[next] ?? "";
26802
+ if (word === "-p" || word === "--profile") next += 1;
26803
+ else if (!word.startsWith("-")) plain.push(word);
26693
26804
  }
26805
+ return plain[0] === "gateway" && plain[1] === "run" ? args : void 0;
26694
26806
  }
26695
- var ids = createIdGenerator();
26696
- var QUIET_MS = 1500;
26697
- var LOST_STREAM_MS = 12e4;
26698
- var KEPT_FEEDBACK = 500;
26699
- var KINDS = {
26700
- terminal: "shell",
26701
- process: "shell",
26702
- execute_code: "shell",
26703
- web_search: "search",
26704
- search_files: "search",
26705
- session_search: "search",
26706
- web_extract: "web",
26707
- read_file: "file",
26708
- write_file: "edit",
26709
- patch: "edit",
26710
- vision_analyze: "image",
26711
- image_generate: "image",
26712
- memory: "memory",
26713
- delegate_task: "handoff"
26714
- };
26715
- function keyOf(agent) {
26716
- return `hermes:${(agent.address.profile ?? "default").toLowerCase()}:${agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
26807
+ function serviceOf(system, root2, env = {}) {
26808
+ const home2 = resolve3(root2);
26809
+ const native = resolve3(system.home, ".hermes");
26810
+ const suffix = home2 === native ? "" : dirname5(home2) === join8(native, "profiles") ? basename4(home2) : createHash3("sha256").update(home2).digest("hex").slice(0, 8);
26811
+ if (system.platform === "darwin") {
26812
+ const path = join8(system.home, "Library", "LaunchAgents", `ai.hermes.gateway${suffix ? `-${suffix}` : ""}.plist`);
26813
+ return system.exists(path) ? { kind: "launchd", path } : void 0;
26814
+ }
26815
+ if (system.platform === "linux") {
26816
+ const unit = `hermes-gateway${suffix ? `-${suffix}` : ""}.service`;
26817
+ const user = join8(env["XDG_CONFIG_HOME"] || join8(system.home, ".config"), "systemd", "user", unit);
26818
+ if (system.exists(user)) return { kind: "systemd", path: user };
26819
+ const machine = join8("/etc/systemd/system", unit);
26820
+ if (system.exists(machine)) return { kind: "systemd", path: machine, system: true };
26821
+ }
26822
+ return void 0;
26717
26823
  }
26718
- var Bridge = class {
26824
+ function expand(text, env) {
26825
+ let out = "";
26826
+ let at = 0;
26827
+ while (at < text.length) {
26828
+ const char = text[at] ?? "";
26829
+ if (char === "\\" && at + 1 < text.length) {
26830
+ out += text[at + 1];
26831
+ at += 2;
26832
+ } else if (char !== "$") {
26833
+ out += char;
26834
+ at += 1;
26835
+ } else if (text[at + 1] === "{") {
26836
+ let depth = 1;
26837
+ let end2 = at + 2;
26838
+ for (; end2 < text.length; end2 += 1) {
26839
+ if (text[end2] === "{") depth += 1;
26840
+ else if (text[end2] === "}" && --depth === 0) break;
26841
+ }
26842
+ const inner = /^([A-Za-z_][A-Za-z0-9_]*)(?:(:?)-([\s\S]*))?$/.exec(text.slice(at + 2, end2));
26843
+ if (inner !== null) {
26844
+ const value = env[inner[1] ?? ""];
26845
+ const unset = inner[2] === ":" ? value === void 0 || value === "" : value === void 0;
26846
+ out += inner[3] !== void 0 && unset ? expand(inner[3], env) : value ?? "";
26847
+ }
26848
+ at = end2 + 1;
26849
+ } else {
26850
+ const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(at + 1))?.[0];
26851
+ out += name2 === void 0 ? "$" : env[name2] ?? "";
26852
+ at += 1 + (name2?.length ?? 0);
26853
+ }
26854
+ }
26855
+ return out;
26856
+ }
26857
+ function wrapperOf(text, env) {
26858
+ for (const each of text.split("\n")) {
26859
+ const found = /^\s*((?:[A-Za-z_][A-Za-z0-9_]*="(?:[^"\\]|\\.)*"\s+)*)exec\s+"([^"]+)"/.exec(each);
26860
+ if (found === null) continue;
26861
+ const vars = {};
26862
+ for (const set2 of (found[1] ?? "").matchAll(/([A-Za-z_][A-Za-z0-9_]*)="((?:[^"\\]|\\.)*)"/g)) {
26863
+ vars[set2[1] ?? ""] = expand(set2[2] ?? "", env);
26864
+ }
26865
+ return { entry: expand(found[2] ?? "", env), vars };
26866
+ }
26867
+ return void 0;
26868
+ }
26869
+ var NO_HERMES = "The hermes command isn't on this machine's PATH, so Hermes's gateway can't be started.";
26870
+ var messageOf2 = (error62) => error62 instanceof Error ? error62.message : String(error62);
26871
+ function gatewayEnv(env) {
26872
+ return Object.fromEntries(Object.entries(env).filter(([name2]) => !name2.startsWith("CHIPS_")));
26873
+ }
26874
+ async function runHermes(system, env, args, timeoutMs, log2) {
26875
+ const hermes = system.which("hermes", env);
26876
+ if (hermes === void 0) return { code: null, output: "The hermes command isn't on this machine's PATH." };
26877
+ const plain = gatewayEnv(env);
26878
+ const ran = await system.run(hermes, args, plain, timeoutMs);
26879
+ if (ran.code === 0 || !ran.output.includes(EXPAT_SYMBOL) || !system.exists(EXPAT)) return ran;
26880
+ const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
26881
+ if (wrapper === void 0) return ran;
26882
+ const expat = dirname5(EXPAT);
26883
+ log2(
26884
+ `hermes ${args.slice(0, 2).join(" ")} stopped at once: Homebrew's Python can't load macOS's expat (${EXPAT_SYMBOL}). Running ${wrapper.entry} directly with DYLD_LIBRARY_PATH=${expat}.`
26885
+ );
26886
+ const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
26887
+ return system.run(wrapper.entry, args, { ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries }, timeoutMs);
26888
+ }
26889
+ var Gateways = class {
26719
26890
  constructor(options) {
26720
26891
  this.options = options;
26721
- this.log = options.log ?? (() => void 0);
26722
- const { chips: chips2, hermesRoot: hermesRoot2 } = options;
26723
- if (hermesRoot2 === void 0) return;
26724
- this.contexts = new Contexts(
26725
- () => this.roster(),
26726
- {
26727
- agent: (agentId2, copy) => chips2.settings.context(agentId2, copy),
26728
- harness: (copy) => chips2.settings.harnessContext(copy)
26729
- },
26730
- (line2) => this.log(`context: ${line2}`),
26731
- options.env ?? process.env
26732
- );
26733
- if (options.fills === void 0) return;
26734
- const kept = options.state.fills ??= {};
26735
- this.fills = new Fills({
26736
- whose: (sessionId) => this.whoseSession(sessionId),
26737
- profileOf: (agentId2) => {
26738
- const profile = this.profileFor(agentId2);
26739
- return profile === void 0 ? void 0 : { profile: profile.name, home: profile.home };
26740
- },
26741
- secretFor: (profile) => envValue(profileHome(hermesRoot2, profile), SECRET_ENV) ?? kept.secret ?? options.fills?.secret,
26742
- send: (agentId2, copy) => chips2.settings.contextWindow(agentId2, copy),
26743
- kept,
26744
- save: () => options.saveState(options.state),
26745
- log: (line2) => this.log(`context window: ${line2}`),
26746
- sendEveryMs: options.fillsEveryMs
26747
- });
26748
26892
  }
26749
26893
  options;
26750
- agents = [];
26751
- rooms = /* @__PURE__ */ new Map();
26752
- /** Every Chips agent of the harness's, configured here or not. */
26753
- ours = /* @__PURE__ */ new Set();
26754
- /** The harness's agents' names, by Chips id, for the folders named after them. */
26755
- names = /* @__PURE__ */ new Map();
26756
- /** The feedback events answered with a turn, the newest `KEPT_FEEDBACK`. */
26757
- lookedAt = /* @__PURE__ */ new Set();
26758
- /** The copies being made, one after another: a message for one waits for it (see `copy`). */
26759
- copying = Promise.resolve();
26760
- log;
26761
- health;
26762
- /** The harness's agents in Chips as last listed: whose context and window copies are sent. */
26763
- listed = [];
26764
- /** The agents' context files (Chips ADR 0134), when the bridge knows where Hermes keeps them. */
26765
- contexts;
26766
- contextTimer;
26767
- /** The agents' context windows (Chips ADR 0146), when Hermes's webhook has somewhere to reach. */
26768
- fills;
26769
- /** The port Hermes's webhook reaches the bridge on, once it listens. */
26770
- fillsPort;
26771
- toldNoReadings = false;
26772
- /** While Hermes's gateway restarts (Chips ADR 0094), the health watch says nothing: the restart says how it went. */
26773
- held = false;
26774
- /** What went wrong in the last restart, said in place of the watch's own words while Hermes doesn't answer. */
26775
- why;
26776
- /** Register the agents, start hearing what is said to them, and say how their gateways are. */
26777
- async start() {
26778
- const { chips: chips2 } = this.options;
26779
- const known = await chips2.agents.list();
26780
- for (const [index, config3] of this.options.agents.entries()) {
26781
- const key = keyOf(config3);
26782
- const registered = index === 0 ? await this.seatMain(config3, known) : await chips2.agents.register({ key, name: config3.name });
26783
- const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
26784
- this.agents.push({ config: config3, key, client, agentId: registered.agentId, up: true });
26785
- }
26786
- this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
26787
- chips2.on("message.new", (message) => void this.hear(message));
26788
- chips2.on("agent.asked", (asked) => {
26789
- this.make(asked).catch((error62) => this.log(`a new agent went wrong: ${messageOf2(error62)}`));
26790
- });
26791
- chips2.on("agent.fork", (fork) => {
26792
- this.copying = this.copying.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf2(error62)}`));
26793
- });
26794
- chips2.on("feedback", (feedback2) => {
26795
- this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf2(error62)}`));
26796
- });
26797
- chips2.on("agent.attached", () => void this.agentsMoved());
26798
- chips2.on("agent.released", () => void this.agentsMoved());
26799
- await this.sayHowItIs();
26800
- this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
26801
- this.health.unref();
26802
- await this.startSettings();
26894
+ timing = TIMING;
26895
+ /** Each gateway restarted, one after another: what went wrong, in a line, or nothing. */
26896
+ async restart() {
26897
+ const troubles = [];
26898
+ for (const gateway of this.options.gateways) {
26899
+ let trouble;
26900
+ try {
26901
+ trouble = await this.restartOne(gateway);
26902
+ } catch (error62) {
26903
+ trouble = `Restart failed: ${messageOf2(error62)}`;
26904
+ }
26905
+ if (trouble !== void 0) troubles.push(trouble);
26906
+ }
26907
+ return troubles.length === 0 ? void 0 : line(troubles.join(" "));
26803
26908
  }
26804
- /** Hold the health watch's words while Hermes's gateway restarts, and let them go after. */
26805
- hold(held) {
26806
- this.held = held;
26909
+ /** Each local gateway that doesn't answer, started: the bridge is starting. */
26910
+ async startIfDown() {
26911
+ const { system, log: log2 } = this.options;
26912
+ for (const gateway of this.options.gateways) {
26913
+ if (!isLocal(gateway.url)) continue;
26914
+ const port = portOf(gateway.url);
26915
+ try {
26916
+ if (await system.healthy(gateway.url)) continue;
26917
+ let trouble;
26918
+ if (this.serviceFor(gateway) !== void 0) {
26919
+ log2(`Hermes isn't answering on port ${port}: starting its service, hermes gateway start`);
26920
+ trouble = await this.byService(gateway, "start", void 0);
26921
+ } else {
26922
+ const pid = await system.listener(port);
26923
+ if (pid !== void 0) {
26924
+ log2(`Hermes isn't answering on port ${port}, but pid ${pid} listens there: no second gateway started`);
26925
+ continue;
26926
+ }
26927
+ log2(`Hermes isn't answering on port ${port}: starting hermes gateway run`);
26928
+ trouble = await this.start(gateway, ["gateway", "run"], void 0);
26929
+ }
26930
+ if (trouble !== void 0) log2(trouble);
26931
+ } catch (error62) {
26932
+ log2(`Hermes's gateway on port ${port} couldn't be started: ${messageOf2(error62)}`);
26933
+ }
26934
+ }
26807
26935
  }
26808
26936
  /**
26809
- * How a restart went (Chips ADR 0094): fine as the gateways now are, or its words, which stand
26810
- * while Hermes doesn't answer. The health watch speaks for itself again after.
26937
+ * The service of Hermes's root, for the Main agent's gateway: a gateway at another address is
26938
+ * another profile's own, which `hermes gateway status` of the root doesn't speak for.
26811
26939
  */
26812
- async restarted(trouble) {
26813
- this.held = false;
26814
- this.why = trouble;
26815
- if (trouble === void 0) return this.sayHowItIs();
26816
- await this.options.chips.reportTrouble(trouble).catch(() => void 0);
26817
- }
26818
- stop() {
26819
- if (this.health !== void 0) clearInterval(this.health);
26820
- if (this.contextTimer !== void 0) clearInterval(this.contextTimer);
26821
- void this.fills?.close();
26940
+ serviceFor(gateway) {
26941
+ if (gateway !== this.options.gateways[0]) return void 0;
26942
+ return serviceOf(this.options.system, this.options.root, this.options.env);
26822
26943
  }
26823
- // ------------------------------------------------------------------ an agent's page
26824
- /**
26825
- * The agents' context and context windows, sent at start and kept current: the files read
26826
- * again every so often and after each turn, a window copy on each reading Hermes sends.
26827
- */
26828
- async startSettings() {
26829
- if (this.contexts === void 0) return;
26830
- this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26831
- await this.contexts.refresh();
26832
- this.contextTimer = setInterval(() => void this.contexts?.refresh(), this.options.contextEveryMs ?? 15e3);
26833
- this.contextTimer.unref();
26834
- const fills2 = this.fills;
26835
- const asked = this.options.fills;
26836
- if (fills2 === void 0 || asked === void 0) return;
26837
- const kept = this.options.state.fills ??= {};
26838
- try {
26839
- this.fillsPort = await fills2.listen(asked.port);
26840
- if (kept.port !== this.fillsPort) {
26841
- kept.port = this.fillsPort;
26842
- this.options.saveState(this.options.state);
26843
- }
26844
- } catch (error62) {
26845
- this.log(`context window: nothing can listen on port ${asked.port} (${messageOf2(error62)}), so Chips won't show how full each agent's context is`);
26944
+ async restartOne(gateway) {
26945
+ const { system, log: log2 } = this.options;
26946
+ if (!isLocal(gateway.url)) {
26947
+ return `Hermes's gateway at ${new URL(gateway.url).host} isn't on this machine, so it can't be restarted from here.`;
26846
26948
  }
26847
- await fills2.sendAll();
26848
- }
26849
- /** The address Hermes's webhook POSTs a profile's readings to, once the bridge listens. */
26850
- hookUrl(profile = "default") {
26851
- return this.fillsPort === void 0 ? void 0 : hookUrl(this.fillsPort, profileName(profile));
26852
- }
26853
- /** The harness's agents listed again, the copies of any gone forgotten, and the context sent. */
26854
- async agentsMoved() {
26855
- if (this.contexts === void 0) return;
26856
- const before = this.listed;
26857
- this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26858
- const now = new Set(this.active().map((agent) => agent.agentId));
26859
- for (const gone of before) {
26860
- if (now.has(gone.agentId)) continue;
26861
- this.options.chips.settings.forget(gone.agentId);
26862
- this.fills?.forget(gone.agentId);
26949
+ const port = portOf(gateway.url);
26950
+ const pid = await system.listener(port);
26951
+ if (this.serviceFor(gateway) !== void 0) {
26952
+ log2("restarting Hermes's service: hermes gateway restart");
26953
+ return this.byService(gateway, "restart", pid);
26863
26954
  }
26864
- await this.contexts.refresh();
26865
- }
26866
- /** The harness's agents that read anything now: the active ones listed. */
26867
- active() {
26868
- return this.listed.filter((agent) => agent.state === "active");
26869
- }
26870
- /** A profile as the context needs it: its name and its home under Hermes's root. */
26871
- profileOfRunning(agent) {
26872
- const root2 = this.options.hermesRoot;
26873
- if (root2 === void 0) return void 0;
26874
- return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
26875
- }
26876
- /** The profile an agent runs on: its own, a copy's original's, else the first (as `profileOf` picks). */
26877
- profileFor(agentId2) {
26878
- const own2 = this.agents.find((agent) => agent.agentId === agentId2);
26879
- const copied = this.options.state.profiles?.[agentId2];
26880
- const original = this.agents.find((agent) => copied !== void 0 && agent.key === copied);
26881
- const running = own2 ?? original ?? this.agents[0];
26882
- return running === void 0 ? void 0 : this.profileOfRunning(running);
26883
- }
26884
- /** Who reads what: each active agent with its profile, and the first profile, which new agents run on. */
26885
- roster() {
26886
- const readers = [];
26887
- const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
26888
- for (const agentId2 of ids2) {
26889
- if (this.listed.some((agent) => agent.agentId === agentId2 && agent.state !== "active")) continue;
26890
- const profile = this.profileFor(agentId2);
26891
- if (profile !== void 0) readers.push({ agentId: agentId2, profile });
26955
+ if (pid === void 0) {
26956
+ if (await system.healthy(gateway.url)) return `Can't tell what answers on port ${port}, so nothing was restarted.`;
26957
+ log2(`nothing listens on port ${port}: starting hermes gateway run`);
26958
+ return this.start(gateway, ["gateway", "run"], void 0);
26892
26959
  }
26893
- const first = this.agents[0];
26894
- return { readers, first: first === void 0 ? void 0 : this.profileOfRunning(first) };
26895
- }
26896
- /** The agent a Hermes session the bridge keeps is, with its profile: nothing for any other. */
26897
- whoseSession(sessionId) {
26898
- const entry = Object.entries(this.options.state.sessions).find(([, session]) => session === sessionId);
26899
- if (entry === void 0) return void 0;
26900
- const who = entry[0].slice(0, entry[0].lastIndexOf("|"));
26901
- const agentId2 = this.agents.find((agent) => agent.key === who)?.agentId ?? who;
26902
- const profile = this.profileFor(agentId2);
26903
- return profile === void 0 ? void 0 : { agentId: agentId2, profile: profile.name, home: profile.home };
26904
- }
26905
- /** A person's edit of a context file from Chips (Chips ADR 0134), saved and answered. */
26906
- async onContextEdit(frame) {
26907
- if (this.contexts === void 0) throw new Refuse("This bridge reads no context files.");
26908
- if (frame.agent_id !== void 0 && !this.listed.some((agent) => agent.agentId === frame.agent_id)) {
26909
- this.listed = await this.options.chips.agents.list().catch(() => this.listed);
26960
+ const info = await system.describe(pid);
26961
+ if (info === void 0) return this.start(gateway, ["gateway", "run"], void 0);
26962
+ const args = this.ours(info);
26963
+ if (args === void 0) {
26964
+ log2(`pid ${pid} on port ${port} isn't this user's hermes gateway run, so it was left alone: ${info.command.slice(0, 160)}`);
26965
+ return `Port ${port} is held by something other than hermes gateway run (pid ${pid}), so nothing was restarted.`;
26910
26966
  }
26911
- return this.contexts.edit(frame);
26912
- }
26913
- /** A person's Compact now (Chips ADR 0146): Hermes can't be asked to compact from outside a run. */
26914
- onWindowCompact(frame) {
26915
- throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
26967
+ if (system.which("hermes", this.options.env) === void 0) return NO_HERMES;
26968
+ log2(`stopping Hermes's gateway, pid ${pid}`);
26969
+ if (!await this.stop(pid)) return `Hermes's gateway (pid ${pid}) didn't stop, so it wasn't started again.`;
26970
+ return this.start(gateway, args, info.cwd);
26916
26971
  }
26917
- /** A person's limit (Chips ADR 0146): Hermes sets it in its profile's config, for all its agents. */
26918
- onWindowChange(frame) {
26919
- const profile = this.profileFor(frame.agent_id);
26920
- throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
26972
+ /** A `hermes gateway run` of this user's: the words to start it again with. */
26973
+ ours(info) {
26974
+ return info.uid === this.options.system.uid ? gatewayRunArgs(info.command) : void 0;
26921
26975
  }
26922
- /** The Main agent, the first registered, once started: who says the harness's first words. */
26923
- get mainAgentId() {
26924
- return this.agents[0]?.agentId;
26976
+ /** SIGTERM, then SIGKILL once it has had a while, and only while it is still that gateway. */
26977
+ async stop(pid) {
26978
+ const { system, log: log2 } = this.options;
26979
+ system.signal(pid, "SIGTERM");
26980
+ if (await this.gone(pid, this.timing.stopMs)) return true;
26981
+ const still = await system.describe(pid);
26982
+ if (still === void 0) return true;
26983
+ if (this.ours(still) === void 0) return false;
26984
+ log2(`Hermes's gateway, pid ${pid}, is still running ${this.timing.stopMs / 1e3} s after SIGTERM: SIGKILL`);
26985
+ system.signal(pid, "SIGKILL");
26986
+ return this.gone(pid, this.timing.killMs);
26925
26987
  }
26926
- /**
26927
- * The Main agent: the one Chips made for the harness when it joined, keyed by the harness's own
26928
- * id, registered as Main (Chips ADR 0183), so it is called what the config says and has no
26929
- * second beside it. It goes home, and a Main of ours from before that, out of its room now, is
26930
- * archived, which gives its name up. With no such agent (a harness from before them), the Main
26931
- * Chips has is kept: a harness has one at most.
26932
- */
26933
- async seatMain(config3, known) {
26934
- const { chips: chips2 } = this.options;
26935
- const before = known.find((agent) => agent.lead);
26936
- const made = known.some((agent) => agent.key === chips2.harnessId);
26937
- if (!made && before !== void 0) {
26938
- await this.goHome(before, known);
26939
- return before;
26940
- }
26941
- const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
26942
- await this.goHome(main2, known);
26943
- if (before !== void 0 && before.agentId !== main2.agentId) {
26944
- await chips2.agents.register({ key: before.key, name: before.name, state: "archived" }).catch((error62) => this.log(`${before.name} stays: ${String(error62)}`));
26988
+ async gone(pid, withinMs) {
26989
+ const { system } = this.options;
26990
+ const by = system.now() + withinMs;
26991
+ while (system.alive(pid)) {
26992
+ if (system.now() >= by) return false;
26993
+ await system.sleep(this.timing.pollMs);
26945
26994
  }
26946
- return main2;
26995
+ return true;
26947
26996
  }
26948
- /** The Main agent into the harness's own room, in the place of whichever of ours is there. */
26949
- async goHome(main2, known) {
26950
- const { chips: chips2, home: home2 } = this.options;
26951
- if (home2 === void 0 || main2.spaces.includes(home2)) return;
26952
- const there = known.find((agent) => agent.agentId !== main2.agentId && agent.spaces.includes(home2));
26953
- await chips2.agents.attach(main2.agentId, { spaceId: home2, replacing: there?.agentId ?? chips2.harnessId }).catch((error62) => this.log(`${main2.name} stays out of its home: ${String(error62)}`));
26997
+ /** Hermes's own `hermes gateway restart|start`, then the gateway answering, from a new process. */
26998
+ async byService(gateway, verb, before) {
26999
+ const { system, env } = this.options;
27000
+ const hermes = system.which("hermes", env);
27001
+ if (hermes === void 0) return NO_HERMES;
27002
+ const scope = this.serviceFor(gateway)?.system === true ? ["--system"] : [];
27003
+ const ran = await system.run(hermes, ["gateway", verb, ...scope], gatewayEnv(env), this.timing.serviceMs);
27004
+ if (ran.code !== 0) return line(`hermes gateway ${verb} failed: ${whyFrom(ran.output, ran.code)}`);
27005
+ const by = system.now() + this.timing.serviceMs;
27006
+ for (; ; ) {
27007
+ const old = before !== void 0 && system.alive(before);
27008
+ if (!old && await system.healthy(gateway.url)) return void 0;
27009
+ if (system.now() >= by) {
27010
+ if (await system.healthy(gateway.url)) return void 0;
27011
+ return `Hermes's service ${verb === "start" ? "started" : "restarted"}, but nothing answers on port ${portOf(gateway.url)}.`;
27012
+ }
27013
+ await system.sleep(this.timing.pollMs);
27014
+ }
26954
27015
  }
26955
27016
  /**
26956
- * A person asked for a new agent in a room (Chips ADR 0108): one of the harness's, keyed by the
26957
- * ask so an ask heard twice makes one, registered for the room, which Chips puts it in while the
26958
- * room waits. It answers through the first profile, in a session of its own.
27017
+ * `hermes <args>` (`gateway run`), on its own, in `cwd` (else the home), then through Homebrew's
27018
+ * expat when it died of macOS's.
26959
27019
  */
26960
- async make(asked) {
26961
- const { chips: chips2 } = this.options;
26962
- if (asked.harness_id !== chips2.harnessId || asked.space_id === void 0) return;
26963
- const agent = await chips2.agents.register({
26964
- key: `hermes:asked:${asked.id}`,
26965
- name: asked.name,
26966
- madeFor: asked.id,
26967
- spaceId: asked.space_id
26968
- });
26969
- this.ours.add(agent.agentId);
26970
- this.names.set(agent.agentId, agent.name);
26971
- this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
26972
- void this.agentsMoved();
27020
+ async start(gateway, args, cwd) {
27021
+ const { system, env, log: log2 } = this.options;
27022
+ const port = portOf(gateway.url);
27023
+ const hermes = system.which("hermes", env);
27024
+ if (hermes === void 0) return NO_HERMES;
27025
+ const folder2 = cwd ?? system.home;
27026
+ const plain = gatewayEnv(env);
27027
+ let outcome = await this.launch(gateway, hermes, args, plain, folder2, `hermes ${args.join(" ")}`);
27028
+ if (outcome.state === "ended" && outcome.early && outcome.output.includes(EXPAT_SYMBOL) && system.exists(EXPAT)) {
27029
+ const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
27030
+ if (wrapper !== void 0) {
27031
+ const expat = dirname5(EXPAT);
27032
+ log2(
27033
+ `hermes gateway run stopped at once: Homebrew's Python can't load macOS's expat (${EXPAT_SYMBOL}). Starting ${wrapper.entry} directly with DYLD_LIBRARY_PATH=${expat}.`
27034
+ );
27035
+ const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
27036
+ outcome = await this.launch(
27037
+ gateway,
27038
+ wrapper.entry,
27039
+ args,
27040
+ { ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries },
27041
+ folder2,
27042
+ `${wrapper.entry} ${args.join(" ")} with DYLD_LIBRARY_PATH=${expat}`
27043
+ );
27044
+ }
27045
+ }
27046
+ if (outcome.state === "up") {
27047
+ log2(`Hermes's gateway${outcome.pid === void 0 ? "" : `, pid ${outcome.pid},`} answers on port ${port}. It logs to ${this.options.logFile}.`);
27048
+ return void 0;
27049
+ }
27050
+ if (outcome.state === "silent") return `Hermes's gateway started, but nothing answers on port ${port}.`;
27051
+ return line(`Hermes's gateway stopped as it started: ${whyFrom(outcome.output, outcome.code)}`);
27052
+ }
27053
+ /** A program started, and watched until it answers, ends, or has had its time. */
27054
+ async launch(gateway, command, args, env, cwd, what) {
27055
+ const { system, logFile } = this.options;
27056
+ system.note(logFile, `--- ${new Date(system.now()).toISOString()} chips-hermes started ${what}`);
27057
+ const from = system.size(logFile);
27058
+ const startedAt = system.now();
27059
+ const child = system.launch({ command, args, env, cwd, log: logFile });
27060
+ let ended;
27061
+ void child.exited.then(({ code }) => {
27062
+ ended = { code, at: system.now() };
27063
+ });
27064
+ for (; ; ) {
27065
+ await Promise.resolve();
27066
+ if (ended !== void 0) {
27067
+ return { state: "ended", early: ended.at - startedAt <= this.timing.earlyMs, output: system.since(logFile, from), code: ended.code };
27068
+ }
27069
+ if (await system.healthy(gateway.url)) return { state: "up", pid: child.pid };
27070
+ if (system.now() - startedAt >= this.timing.upMs) return { state: "silent" };
27071
+ await system.sleep(this.timing.pollMs);
27072
+ }
27073
+ }
27074
+ };
27075
+ function restarter(deps) {
27076
+ let running;
27077
+ return (ask2) => {
27078
+ if (running !== void 0) {
27079
+ deps.log(`restart asked by ${ask2.askedBy} while one runs: it is the same restart`);
27080
+ return running;
27081
+ }
27082
+ running = (async () => {
27083
+ deps.log(`restart asked by ${ask2.askedBy}: restarting Hermes's gateway`);
27084
+ deps.hold(true);
27085
+ let trouble;
27086
+ try {
27087
+ trouble = await deps.restart();
27088
+ } catch (error62) {
27089
+ trouble = line(`Restart failed: ${error62 instanceof Error ? error62.message : String(error62)}`);
27090
+ }
27091
+ deps.log(trouble === void 0 ? "restarted, and Hermes answers" : `restart: ${trouble}`);
27092
+ try {
27093
+ await deps.say(trouble);
27094
+ } catch (error62) {
27095
+ deps.log(`could not tell Chips how the restart went: ${String(error62)}`);
27096
+ } finally {
27097
+ deps.hold(false);
27098
+ }
27099
+ })().finally(() => {
27100
+ running = void 0;
27101
+ });
27102
+ return running;
27103
+ };
27104
+ }
27105
+ function gatewayLog(configFolder, platform = process.platform) {
27106
+ return platform === "darwin" ? join8(homedir5(), "Library", "Logs", "chips-hermes-gateway.log") : join8(configFolder, "gateway.log");
27107
+ }
27108
+ function runToEnd(command, args, options) {
27109
+ return new Promise((done) => {
27110
+ const child = spawn2(command, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"] });
27111
+ let output2 = "";
27112
+ const keep = (data) => {
27113
+ output2 = (output2 + String(data)).slice(-8e3);
27114
+ };
27115
+ child.stdout.on("data", keep);
27116
+ child.stderr.on("data", keep);
27117
+ const limit = setTimeout(() => child.kill("SIGTERM"), options.timeoutMs);
27118
+ child.on("error", (error62) => {
27119
+ clearTimeout(limit);
27120
+ done({ code: null, output: error62.message });
27121
+ });
27122
+ child.on("close", (code) => {
27123
+ clearTimeout(limit);
27124
+ done({ code, output: output2 });
27125
+ });
27126
+ });
27127
+ }
27128
+ function thisMachine() {
27129
+ return {
27130
+ platform: process.platform,
27131
+ uid: process.getuid?.() ?? -1,
27132
+ home: homedir5(),
27133
+ exists: existsSync8,
27134
+ read(path) {
27135
+ try {
27136
+ return readFileSync8(path, "utf8");
27137
+ } catch {
27138
+ return void 0;
27139
+ }
27140
+ },
27141
+ which(command, env) {
27142
+ for (const folder2 of (env["PATH"] ?? "").split(delimiter2)) {
27143
+ if (folder2 === "") continue;
27144
+ const path = join8(folder2, command);
27145
+ try {
27146
+ accessSync2(path, constants2.X_OK);
27147
+ return path;
27148
+ } catch {
27149
+ }
27150
+ }
27151
+ return void 0;
27152
+ },
27153
+ async listener(port) {
27154
+ const { output: output2 } = await runToEnd("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], { timeoutMs: 5e3 });
27155
+ const pid = Number(/^p(\d+)$/m.exec(output2)?.[1]);
27156
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
27157
+ },
27158
+ async describe(pid) {
27159
+ const { code, output: output2 } = await runToEnd("ps", ["-ww", "-o", "uid=", "-o", "command=", "-p", String(pid)], { timeoutMs: 5e3 });
27160
+ const found = /^\s*(\d+)\s+(.*)$/m.exec(output2);
27161
+ if (code !== 0 || found === null) return void 0;
27162
+ return { uid: Number(found[1]), command: found[2] ?? "", cwd: processFolder(pid) };
27163
+ },
27164
+ alive(pid) {
27165
+ try {
27166
+ process.kill(pid, 0);
27167
+ return true;
27168
+ } catch (error62) {
27169
+ return error62.code === "EPERM";
27170
+ }
27171
+ },
27172
+ signal(pid, signal) {
27173
+ try {
27174
+ process.kill(pid, signal);
27175
+ } catch {
27176
+ }
27177
+ },
27178
+ run: (command, args, env, timeoutMs) => runToEnd(command, args, { env, timeoutMs }),
27179
+ launch({ command, args, env, cwd, log: log2 }) {
27180
+ const out = openSync2(log2, "a");
27181
+ try {
27182
+ const child = spawn2(command, args, { cwd, env, detached: true, stdio: ["ignore", out, out] });
27183
+ const exited = new Promise((done) => {
27184
+ child.once("exit", (code, signal) => done({ code, signal }));
27185
+ child.once("error", (error62) => {
27186
+ appendFileSync2(log2, `${error62.message}
27187
+ `);
27188
+ done({ code: null, signal: null });
27189
+ });
27190
+ });
27191
+ child.unref();
27192
+ return { pid: child.pid, exited };
27193
+ } finally {
27194
+ closeSync2(out);
27195
+ }
27196
+ },
27197
+ note(log2, words4) {
27198
+ appendFileSync2(log2, `${words4}
27199
+ `);
27200
+ },
27201
+ size(log2) {
27202
+ try {
27203
+ return statSync3(log2).size;
27204
+ } catch {
27205
+ return 0;
27206
+ }
27207
+ },
27208
+ since(log2, from) {
27209
+ try {
27210
+ const to = statSync3(log2).size;
27211
+ const start = Math.max(from, to - 64e3);
27212
+ const buffer = Buffer.alloc(Math.max(0, to - start));
27213
+ const fd = openSync2(log2, "r");
27214
+ try {
27215
+ readSync(fd, buffer, 0, buffer.length, start);
27216
+ } finally {
27217
+ closeSync2(fd);
27218
+ }
27219
+ return buffer.toString("utf8");
27220
+ } catch {
27221
+ return "";
27222
+ }
27223
+ },
27224
+ healthy: (url2) => new HermesClient({ url: url2, key: "" }, (input2, init) => fetch(input2, { ...init, signal: AbortSignal.timeout(5e3) })).healthy(),
27225
+ sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
27226
+ now: () => Date.now()
27227
+ };
27228
+ }
27229
+
27230
+ // src/profiles.ts
27231
+ var KEY_ENV = "API_SERVER_KEY";
27232
+ var RESERVED = /* @__PURE__ */ new Set(["hermes", "default", "test", "tmp", "root", "sudo"]);
27233
+ var CREATE_MS = 12e4;
27234
+ function freeProfile(root2, name2, taken) {
27235
+ const slug = name2.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 56).replace(/^-+|-+$/g, "") || "agent";
27236
+ for (let count = 1; ; count += 1) {
27237
+ if (count === 1 && RESERVED.has(slug)) continue;
27238
+ const profile = count === 1 ? slug : `${slug}-${count}`;
27239
+ if (!taken.includes(profile) && !existsSync9(join9(root2, "profiles", profile))) return profile;
27240
+ }
27241
+ }
27242
+ function readyProfile(home2, names, hook) {
27243
+ for (const file3 of ["MEMORY.md", "USER.md"]) rmSync4(join9(home2, "memories", file3), { force: true });
27244
+ if (envValue(home2, KEY_ENV) === void 0) appendEnv(home2, KEY_ENV, randomBytes2(32).toString("hex"));
27245
+ if (hook === void 0) return void 0;
27246
+ const path = join9(home2, "config.yaml");
27247
+ if (existsSync9(path)) {
27248
+ const theirs = hookUrl(hook.port, names.from).replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
27249
+ const entry = new RegExp(`^(\\s*(?:-\\s*)?url:\\s*["']?)${theirs}(["']?\\s*(?:#.*)?)$`, "m");
27250
+ const text = readFileSync9(path, "utf8");
27251
+ if (entry.test(text)) writeWhole(path, text.replace(entry, `$1${hookUrl(hook.port, names.profile)}$2`));
27252
+ }
27253
+ return addHook(home2, hook.port, names.profile, hook.secret);
27254
+ }
27255
+ function hermesProfiles(options) {
27256
+ return {
27257
+ async create(name2, from) {
27258
+ const args = ["profile", "create", name2, "--clone-from", from, "--no-alias"];
27259
+ const ran = await runHermes(options.system, options.env, args, CREATE_MS, options.log);
27260
+ if (ran.code !== 0) throw new Error(whyFrom(ran.output, ran.code).replace(/^Error:\s*/, ""));
27261
+ },
27262
+ async serve() {
27263
+ const result = await askGateway(options.root, "rescan-profiles");
27264
+ return typeof result?.["multiplex"] === "boolean" ? result["multiplex"] : void 0;
27265
+ }
27266
+ };
27267
+ }
27268
+ function controlSocketOf(root2) {
27269
+ const direct = join9(root2, "gateway.sock");
27270
+ if (existsSync9(direct)) return direct;
27271
+ try {
27272
+ const pointed = readFileSync9(join9(root2, "gateway.sock.path"), "utf8").trim();
27273
+ return pointed !== "" && existsSync9(pointed) ? pointed : void 0;
27274
+ } catch {
27275
+ return void 0;
27276
+ }
27277
+ }
27278
+ function askGateway(root2, verb, timeoutMs = 8e3) {
27279
+ const path = controlSocketOf(root2);
27280
+ if (path === void 0) return Promise.resolve(void 0);
27281
+ return new Promise((done) => {
27282
+ let heard = "";
27283
+ let over = false;
27284
+ const socket = createConnection(path);
27285
+ const finish = (line2) => {
27286
+ if (over) return;
27287
+ over = true;
27288
+ clearTimeout(timer);
27289
+ socket.destroy();
27290
+ done(line2 === void 0 ? void 0 : resultOf(line2));
27291
+ };
27292
+ const timer = setTimeout(() => finish(void 0), timeoutMs);
27293
+ socket.on("connect", () => socket.write(`${JSON.stringify({ verb, id: 1, protocol: 1 })}
27294
+ `));
27295
+ socket.on("data", (chunk) => {
27296
+ heard += String(chunk);
27297
+ const end2 = heard.indexOf("\n");
27298
+ if (end2 >= 0) finish(heard.slice(0, end2));
27299
+ });
27300
+ socket.on("end", () => finish(heard));
27301
+ socket.on("error", () => finish(void 0));
27302
+ });
27303
+ }
27304
+ function resultOf(line2) {
27305
+ try {
27306
+ const answer = JSON.parse(line2);
27307
+ return answer.ok === true && typeof answer.result === "object" && answer.result !== null ? answer.result : void 0;
27308
+ } catch {
27309
+ return void 0;
27310
+ }
27311
+ }
27312
+
27313
+ // src/bridge.ts
27314
+ var WANTS = [
27315
+ "agents",
27316
+ "new_spaces",
27317
+ "new_agents",
27318
+ "forks",
27319
+ "context_files",
27320
+ "models",
27321
+ "context_windows",
27322
+ "restarts"
27323
+ ];
27324
+ function settingsFor(active) {
27325
+ const bridge2 = () => {
27326
+ const there = active();
27327
+ if (there === void 0) throw new Refuse("The bridge is starting. Try again in a moment.");
27328
+ return there;
27329
+ };
27330
+ return {
27331
+ context: (edit) => bridge2().onContextEdit(edit),
27332
+ models: (change) => bridge2().onModelChange(change),
27333
+ contextWindows: {
27334
+ compact: async (ask2) => bridge2().onWindowCompact(ask2),
27335
+ change: async (ask2) => bridge2().onWindowChange(ask2)
27336
+ }
27337
+ };
27338
+ }
27339
+ function filesHere(project = false) {
27340
+ return `${sendingFiles({ project })} Hermes's note that this API server sends only pictures, and shows a MEDIA: tag on the runs endpoint as text, does not hold for these runs: they come from Chips, whose bridge takes each MEDIA: line out of your answer and attaches that file to your message, whatever its type, up to 100 MB.`;
27341
+ }
27342
+ var FILES_HERE = filesHere();
27343
+ function instructionsFor(folder2, project = false) {
27344
+ const files = filesHere(project);
27345
+ return folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
27346
+ }
27347
+ function freeFolder(root2, name2, taken) {
27348
+ const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
27349
+ for (let count = 1; ; count += 1) {
27350
+ const folder2 = join10(root2, count === 1 ? slug : `${slug}-${count}`);
27351
+ if (!taken.includes(folder2)) return folder2;
27352
+ }
27353
+ }
27354
+ var ids = createIdGenerator();
27355
+ var QUIET_MS = 1500;
27356
+ var LOST_STREAM_MS = 12e4;
27357
+ var KEPT_FEEDBACK = 500;
27358
+ var KINDS = {
27359
+ terminal: "shell",
27360
+ process: "shell",
27361
+ execute_code: "shell",
27362
+ web_search: "search",
27363
+ search_files: "search",
27364
+ session_search: "search",
27365
+ web_extract: "web",
27366
+ read_file: "file",
27367
+ write_file: "edit",
27368
+ patch: "edit",
27369
+ vision_analyze: "image",
27370
+ image_generate: "image",
27371
+ memory: "memory",
27372
+ delegate_task: "handoff"
27373
+ };
27374
+ function keyOf(agent) {
27375
+ return `hermes:${(agent.address.profile ?? "default").toLowerCase()}:${agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
27376
+ }
27377
+ var Bridge = class {
27378
+ constructor(options) {
27379
+ this.options = options;
27380
+ this.log = options.log ?? (() => void 0);
27381
+ const { chips: chips2, hermesRoot: hermesRoot2 } = options;
27382
+ if (hermesRoot2 === void 0) return;
27383
+ this.contexts = new Contexts(
27384
+ () => this.roster(),
27385
+ {
27386
+ agent: (agentId2, copy) => chips2.settings.context(agentId2, copy),
27387
+ harness: (copy) => chips2.settings.harnessContext(copy)
27388
+ },
27389
+ (line2) => this.log(`context: ${line2}`),
27390
+ options.env ?? process.env
27391
+ );
27392
+ if (options.fills === void 0) return;
27393
+ const kept = options.state.fills ??= {};
27394
+ this.fills = new Fills({
27395
+ whose: (sessionId) => this.whoseSession(sessionId),
27396
+ profileOf: (agentId2) => {
27397
+ const profile = this.profileFor(agentId2);
27398
+ return profile === void 0 ? void 0 : { profile: profile.name, home: profile.home };
27399
+ },
27400
+ secretFor: (profile) => envValue(profileHome(hermesRoot2, profile), SECRET_ENV) ?? kept.secret ?? options.fills?.secret,
27401
+ send: (agentId2, copy) => chips2.settings.contextWindow(agentId2, copy),
27402
+ kept,
27403
+ save: () => options.saveState(options.state),
27404
+ log: (line2) => this.log(`context window: ${line2}`),
27405
+ sendEveryMs: options.fillsEveryMs
27406
+ });
26973
27407
  }
27408
+ options;
27409
+ agents = [];
27410
+ rooms = /* @__PURE__ */ new Map();
27411
+ /** Every Chips agent of the harness's, configured here or not. */
27412
+ ours = /* @__PURE__ */ new Set();
27413
+ /** The harness's agents' names, by Chips id, for the folders named after them. */
27414
+ names = /* @__PURE__ */ new Map();
27415
+ /** The agents made in Chips with a profile of their own, by Chips id (Chips ADR 0241). */
27416
+ own = /* @__PURE__ */ new Map();
27417
+ /** The feedback events answered with a turn, the newest `KEPT_FEEDBACK`. */
27418
+ lookedAt = /* @__PURE__ */ new Set();
26974
27419
  /**
26975
- * A person asked for a copy of one of the harness's agents in another room (ADR 0087, Fork): the
26976
- * agent's session in the room it is in, else the last it had, forked in Hermes, is the copy's
26977
- * session in the room asked. The copy answers through its original's profile, whose Hermes has
26978
- * that session, and works in its original's folder. It is registered with `forkedFrom` for the
26979
- * room, which Chips puts it in while the room waits. The fork and the copy are keyed by the ask,
26980
- * so an ask heard twice forks once (Hermes refuses the id again) and registers the same copy. An
26981
- * agent that never spoke is copied with nothing to remember. A turn still going is not in the
26982
- * copy: Hermes copies what the session holds. Nothing is made for an ask the room has stopped
26983
- * waiting for (`FORK_WAIT_MS`), nor for an agent that isn't the harness's.
27420
+ * The agents made, the spaces claimed and the copies made, one after another: a message for an
27421
+ * agent not settled yet waits for them (see `runnerOf`).
26984
27422
  */
26985
- async copy(fork) {
26986
- const { chips: chips2, state: state2 } = this.options;
26987
- const spaceId = fork.space_id;
26988
- if (spaceId === void 0) return;
26989
- const late = Date.now() - Date.parse(fork.ts);
26990
- if (late > FORK_WAIT_MS) {
26991
- this.log(`no copy of ${fork.agent_id}: asked ${Math.round(late / 1e3)} s ago, and the room waits a minute`);
26992
- return;
27423
+ making = Promise.resolve();
27424
+ log;
27425
+ health;
27426
+ /** The harness's agents in Chips as last listed: whose context and window copies are sent. */
27427
+ listed = [];
27428
+ /** The agents' context files (Chips ADR 0134), when the bridge knows where Hermes keeps them. */
27429
+ contexts;
27430
+ contextTimer;
27431
+ /** The agents' context windows (Chips ADR 0146), when Hermes's webhook has somewhere to reach. */
27432
+ fills;
27433
+ /** The port Hermes's webhook reaches the bridge on, once it listens. */
27434
+ fillsPort;
27435
+ toldNoReadings = false;
27436
+ /** Whether the log has said once that a model copy wasn't sent. */
27437
+ toldModels = false;
27438
+ /** While Hermes's gateway restarts (Chips ADR 0094), the health watch says nothing: the restart says how it went. */
27439
+ held = false;
27440
+ /** What went wrong in the last restart, said in place of the watch's own words while Hermes doesn't answer. */
27441
+ why;
27442
+ /** Register the agents, start hearing what is said to them, and say how their gateways are. */
27443
+ async start() {
27444
+ const { chips: chips2 } = this.options;
27445
+ const known = await chips2.agents.list();
27446
+ for (const [index, config3] of this.options.agents.entries()) {
27447
+ const key = keyOf(config3);
27448
+ const registered = index === 0 ? await this.seatMain(config3, known) : await chips2.agents.register({ key, name: config3.name });
27449
+ const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
27450
+ this.agents.push({ config: config3, key, client, agentId: registered.agentId, up: true });
26993
27451
  }
26994
- const original = await this.profileOf(fork.agent_id);
26995
- if (original === void 0) {
26996
- this.log(`no copy of ${fork.agent_id}: it isn't one of this harness's agents`);
26997
- return;
27452
+ for (const [agentId2, profile] of Object.entries(this.options.state.made ?? {})) {
27453
+ const running = this.runningOn(agentId2, profile, known.find((agent) => agent.agentId === agentId2)?.name ?? profile);
27454
+ if (running !== void 0) this.own.set(agentId2, running);
26998
27455
  }
26999
- const name2 = await this.nameOf(fork.agent_id, original.config.name);
27000
- const key = `hermes:fork:${fork.id}`;
27001
- const who = fork.agent_id === original.agentId ? original.key : fork.agent_id;
27002
- const listed = await chips2.agents.list().catch(() => []);
27003
- const room = listed.find((agent) => agent.agentId === fork.agent_id)?.room ?? void 0;
27004
- const from = (room === void 0 ? void 0 : state2.sessions[`${who}|${room}`]) ?? Object.entries(state2.sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
27005
- if (from === void 0) this.log(`${name2} never spoke: its copy remembers nothing`);
27006
- const session = from === void 0 ? void 0 : await original.client.forkSession(from, { id: `chips-fork-${fork.id}`, title: `Chips space ${spaceId} \xB7 ${key}` });
27007
- const folder2 = this.folderOf(original, fork.agent_id, name2);
27008
- const copy = await chips2.agents.register({ key, name: forkName(name2), forkedFrom: fork.agent_id, spaceId });
27009
- if (session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] ??= session;
27010
- state2.profiles = { ...state2.profiles, [copy.agentId]: original.key };
27011
- if (folder2 !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: folder2 };
27012
- this.options.saveState(state2);
27013
- this.ours.add(copy.agentId);
27014
- this.names.set(copy.agentId, copy.name);
27015
- const away = copy.attached === false && !copy.spaces.includes(spaceId);
27016
- this.log(`${copy.name}, a copy of ${name2}, made${away ? `, not in the room: ${copy.why ?? "too late"}` : ""}`);
27017
- void this.agentsMoved();
27456
+ this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
27457
+ chips2.on("message.new", (message) => void this.hear(message));
27458
+ chips2.on("agent.asked", (asked) => {
27459
+ this.making = this.making.then(() => this.make(asked)).catch((error62) => this.log(`a new agent went wrong: ${messageOf3(error62)}`));
27460
+ });
27461
+ chips2.on("agent.fork", (fork) => {
27462
+ this.making = this.making.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf3(error62)}`));
27463
+ });
27464
+ chips2.onSpaceAsked((asked) => {
27465
+ this.making = this.making.then(() => this.claim(asked)).catch((error62) => this.log(`a space from the + went wrong: ${messageOf3(error62)}`));
27466
+ });
27467
+ chips2.on("feedback", (feedback2) => {
27468
+ this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf3(error62)}`));
27469
+ });
27470
+ chips2.on("agent.attached", () => void this.agentsMoved());
27471
+ chips2.on("agent.released", () => void this.agentsMoved());
27472
+ await this.sayHowItIs();
27473
+ this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
27474
+ this.health.unref();
27475
+ await this.startSettings();
27018
27476
  }
27019
- /** An agent's name, from Chips's list when it isn't known yet. */
27020
- async nameOf(agentId2, otherwise) {
27021
- if (!this.names.has(agentId2)) {
27022
- const listed = await this.options.chips.agents.list().catch(() => []);
27023
- for (const agent of listed) this.names.set(agent.agentId, agent.name);
27024
- }
27025
- return this.names.get(agentId2) ?? otherwise;
27477
+ /** Hold the health watch's words while Hermes's gateway restarts, and let them go after. */
27478
+ hold(held) {
27479
+ this.held = held;
27026
27480
  }
27027
27481
  /**
27028
- * The folder an agent works in and sends its files from: the one the config names for them all
27029
- * (`folder`, as it was), else one of its own under `folders`, named after it the first time it
27030
- * takes a turn and kept, so a rename keeps it. Every agent once shared one folder, and a team's
27031
- * files overwrote a one-agent room's game (Chips TT-16); and the one folder had to be Hermes's
27032
- * own working folder, which a plain Hermes doesn't set (the setup audit, 17 October 2026).
27482
+ * How a restart went (Chips ADR 0094): fine as the gateways now are, or its words, which stand
27483
+ * while Hermes doesn't answer. The health watch speaks for itself again after.
27033
27484
  */
27034
- folderOf(agent, agentId2, name2) {
27035
- if (agent.config.folder !== void 0) return agent.config.folder;
27036
- const { folders: root2, state: state2 } = this.options;
27037
- if (root2 === void 0) return void 0;
27038
- const kept = state2.folders?.[agentId2];
27039
- const folder2 = kept ?? freeFolder(root2, name2, Object.values(state2.folders ?? {}));
27040
- mkdirSync5(folder2, { recursive: true });
27041
- if (kept === void 0) {
27042
- state2.folders = { ...state2.folders, [agentId2]: folder2 };
27043
- this.options.saveState(state2);
27044
- }
27045
- return folder2;
27485
+ async restarted(trouble) {
27486
+ this.held = false;
27487
+ this.why = trouble;
27488
+ if (trouble === void 0) return this.sayHowItIs();
27489
+ await this.options.chips.reportTrouble(trouble).catch(() => void 0);
27490
+ }
27491
+ stop() {
27492
+ if (this.health !== void 0) clearInterval(this.health);
27493
+ if (this.contextTimer !== void 0) clearInterval(this.contextTimer);
27494
+ void this.fills?.close();
27046
27495
  }
27496
+ // ------------------------------------------------------------------ an agent's page
27047
27497
  /**
27048
- * The room's project for a turn (Chips ADR 0221): the agent's copy, in its folder, brought up to
27049
- * date, and what the turn is told of it. None for an agent a person pointed at a folder of their
27050
- * own, nor where this machine has no git for it.
27498
+ * The agents' models, context and context windows, sent at start and kept current: the models
27499
+ * again when an agent comes or goes, the files read again every so often and after each turn, a
27500
+ * window copy on each reading Hermes sends.
27051
27501
  */
27052
- async projectFor(turn, spaceId, agentId2, name2) {
27053
- if (this.options.projects !== true || turn.agent.config.folder !== void 0) return void 0;
27054
- const folder2 = turn.folder;
27055
- if (folder2 === void 0) return void 0;
27502
+ async startSettings() {
27503
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
27504
+ await this.sendModels();
27505
+ if (this.contexts === void 0) return;
27506
+ await this.contexts.refresh();
27507
+ this.contextTimer = setInterval(() => void this.contexts?.refresh(), this.options.contextEveryMs ?? 15e3);
27508
+ this.contextTimer.unref();
27509
+ const fills2 = this.fills;
27510
+ const asked = this.options.fills;
27511
+ if (fills2 === void 0 || asked === void 0) return;
27512
+ const kept = this.options.state.fills ??= {};
27056
27513
  try {
27057
- const project = this.options.chips.project({ spaceId, agentId: agentId2, name: name2, folder: folder2, log: (line2) => this.log(line2) });
27058
- const facts = await project.pull();
27059
- turn.project = { copy: project, name: name2 };
27060
- return facts;
27514
+ this.fillsPort = await fills2.listen(asked.port);
27515
+ if (kept.port !== this.fillsPort) {
27516
+ kept.port = this.fillsPort;
27517
+ this.options.saveState(this.options.state);
27518
+ }
27061
27519
  } catch (error62) {
27062
- this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf2(error62)}`);
27063
- return void 0;
27520
+ this.log(`context window: nothing can listen on port ${asked.port} (${messageOf3(error62)}), so Chips won't show how full each agent's context is`);
27064
27521
  }
27522
+ await fills2.sendAll();
27065
27523
  }
27066
- /**
27067
- * What the turn changed in its copy, sent to the room's project before its words are posted: the
27068
- * commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one; else, when
27069
- * the change didn't go, the line the reply says so in (Chips AP-6).
27070
- */
27071
- async sendProject(turn, words4) {
27072
- if (turn.project === void 0) return {};
27073
- const { copy, name: name2 } = turn.project;
27074
- try {
27075
- const pushed = await copy.push({ words: words4 });
27076
- this.log(`${name2}'s project: ${pushedInWords(pushed)}`);
27077
- if (pushed.state === "sent") return { change: pushed.commit };
27078
- const note = heldNote(pushed, name2);
27079
- return note === void 0 ? {} : { note };
27080
- } catch (error62) {
27081
- this.log(`${name2}'s project couldn't be sent: ${messageOf2(error62)}`);
27082
- const note = heldNote({ state: "failed", why: messageOf2(error62) }, name2);
27083
- return note === void 0 ? {} : { note };
27524
+ /** The address Hermes's webhook POSTs a profile's readings to, once the bridge listens. */
27525
+ hookUrl(profile = "default") {
27526
+ return this.fillsPort === void 0 ? void 0 : hookUrl(this.fillsPort, profileName(profile));
27527
+ }
27528
+ /** The harness's agents listed again, the copies of any gone forgotten, and the context and models sent. */
27529
+ async agentsMoved() {
27530
+ const before = this.listed;
27531
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
27532
+ const now = new Set(this.active().map((agent) => agent.agentId));
27533
+ for (const gone of before) {
27534
+ if (now.has(gone.agentId)) continue;
27535
+ this.options.chips.settings.forget(gone.agentId);
27536
+ this.fills?.forget(gone.agentId);
27084
27537
  }
27538
+ await this.contexts?.refresh();
27539
+ await this.sendModels();
27540
+ }
27541
+ /** The harness's agents that read anything now: the active ones listed. */
27542
+ active() {
27543
+ return this.listed.filter((agent) => agent.state === "active");
27544
+ }
27545
+ /** A profile as the context needs it: its name and its home under Hermes's root. */
27546
+ profileOfRunning(agent) {
27547
+ const root2 = this.options.hermesRoot;
27548
+ if (root2 === void 0) return void 0;
27549
+ return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
27550
+ }
27551
+ /** The profile an agent runs on: its own, a copy's original's, else the first (as `profileOf` picks). */
27552
+ profileFor(agentId2) {
27553
+ const running = this.runningFor(agentId2);
27554
+ return running === void 0 ? void 0 : this.profileOfRunning(running);
27085
27555
  }
27086
- /** Trouble while a profile's gateway doesn't answer, and fine once all do (ADR 0083); nothing while a restart runs. */
27087
- async sayHowItIs() {
27088
- if (this.held) return;
27089
- for (const agent of this.agents) agent.up = await agent.client.healthy();
27090
- if (this.held) return;
27091
- const down = this.agents.filter((agent) => !agent.up);
27092
- if (down.length === 0) this.why = void 0;
27093
- const chips2 = this.options.chips;
27094
- const said2 = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(this.why ?? `Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
27095
- await said2.catch(() => void 0);
27556
+ /** What answers for an agent: its own profile, a copy's original's, else the first. */
27557
+ runningFor(agentId2) {
27558
+ const own2 = this.agents.find((agent) => agent.agentId === agentId2) ?? this.own.get(agentId2);
27559
+ const copied = this.options.state.profiles?.[agentId2];
27560
+ return own2 ?? (copied === void 0 ? void 0 : this.byKey(copied)) ?? this.agents[0];
27096
27561
  }
27097
- // ------------------------------------------------------------------ what is said to it
27098
- /** A message for some of its agents: each takes a turn in its room, one after another there. */
27099
- async hear(message, how = {}) {
27100
- const spaceId = message.space_id;
27101
- if (spaceId === void 0) return;
27102
- const harnessId = this.options.chips.harnessId;
27103
- for (const taker of message.for ?? [harnessId]) {
27104
- const agent = await this.runnerOf(taker);
27105
- if (agent === void 0) continue;
27106
- const room = this.roomOf(agent, taker, spaceId);
27107
- room.queue.push({ message, speakerId: taker === harnessId ? void 0 : taker, ...how });
27108
- void this.pump(room);
27109
- }
27562
+ /** A profile the bridge speaks to, by its key: a configured agent's, or a made agent's Chips id. */
27563
+ byKey(key) {
27564
+ return this.agents.find((agent) => agent.key === key) ?? this.own.get(key);
27110
27565
  }
27111
27566
  /**
27112
- * "Not right?" on one of its agents' replies (Chips ADR 0200): a person's note on it, or a bare
27113
- * 👎 from an older phone, is a turn of the agent that said it, in its session in that room and
27114
- * in line with the room's other turns, to look again and answer in its place. Any other
27115
- * reaction is the person's own. Once per event, however often it is heard.
27567
+ * Who reads what: each active agent with its profile, and the first profile, whose SOUL.md each
27568
+ * new agent starts from (and whose every file each new agent reads, where none gets a profile of
27569
+ * its own).
27116
27570
  */
27117
- async notRight(feedback2) {
27118
- const { space_id: spaceId, message_id: messageId } = feedback2;
27119
- const note = feedback2.kind === "correction" ? (feedback2.text ?? "").trim() : void 0;
27120
- const flagged = note === void 0 ? feedback2.kind === "reaction" && feedback2.reaction === "\u{1F44E}" : note !== "";
27121
- if (spaceId === void 0 || !messageId || !flagged || this.lookedAt.has(feedback2.id)) return;
27122
- this.lookedAt.add(feedback2.id);
27123
- for (const old of this.lookedAt) {
27124
- if (this.lookedAt.size <= KEPT_FEEDBACK) break;
27125
- this.lookedAt.delete(old);
27571
+ roster() {
27572
+ const readers = [];
27573
+ const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
27574
+ for (const agentId2 of ids2) {
27575
+ if (this.listed.some((agent) => agent.agentId === agentId2 && agent.state !== "active")) continue;
27576
+ const profile = this.profileFor(agentId2);
27577
+ if (profile !== void 0) readers.push({ agentId: agentId2, profile });
27126
27578
  }
27127
- const context = await this.options.chips.roomContext(spaceId, { limit: 200 }).catch(() => void 0);
27128
- const line2 = context?.lines.find((each) => each.message_id === messageId);
27129
- const message = {
27130
- type: "message.new",
27131
- space_id: spaceId,
27132
- // An id of its own, so nothing of the turn is taken for the reply's.
27133
- message_id: messageIdOf(feedback2.id),
27134
- text: lookAgain(feedback2.from.display_name, line2?.text, note),
27135
- from: feedback2.from,
27136
- ...line2 === void 0 ? {} : { for: [line2.from.id] }
27579
+ const first = this.agents[0];
27580
+ return {
27581
+ readers,
27582
+ first: first === void 0 ? void 0 : this.profileOfRunning(first),
27583
+ ownProfiles: this.options.profiles !== void 0
27137
27584
  };
27138
- await this.hear(message, { goal: lookingAgain(note), sayWho: false });
27139
27585
  }
27586
+ /** The agent a Hermes session the bridge keeps is, with its profile: nothing for any other. */
27587
+ whoseSession(sessionId) {
27588
+ const entry = Object.entries(this.options.state.sessions).find(([, session]) => session === sessionId);
27589
+ if (entry === void 0) return void 0;
27590
+ const who = entry[0].slice(0, entry[0].lastIndexOf("|"));
27591
+ const agentId2 = this.agents.find((agent) => agent.key === who)?.agentId ?? who;
27592
+ const profile = this.profileFor(agentId2);
27593
+ return profile === void 0 ? void 0 : { agentId: agentId2, profile: profile.name, home: profile.home };
27594
+ }
27595
+ /** A person's edit of a context file from Chips (Chips ADR 0134), saved and answered. */
27596
+ async onContextEdit(frame) {
27597
+ if (this.contexts === void 0) throw new Refuse("This bridge reads no context files.");
27598
+ if (frame.agent_id !== void 0 && !this.listed.some((agent) => agent.agentId === frame.agent_id)) {
27599
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
27600
+ }
27601
+ return this.contexts.edit(frame);
27602
+ }
27603
+ // ------------------------------------------------------------------ an agent's model
27140
27604
  /**
27141
- * Which profile answers for a Chips agent, as `profileOf` says. An agent not known yet may be a
27142
- * copy being made, which is waited for, so that its first message is answered in its session.
27605
+ * The model a person picked for an agent, as its copy names it: a choice's id, or the model its
27606
+ * config says each of its profile's runs asks for; nothing for Hermes's default.
27143
27607
  */
27144
- async runnerOf(taker) {
27145
- if (taker !== this.options.chips.harnessId && !this.ours.has(taker)) await this.copying;
27146
- return this.profileOf(taker);
27608
+ ownModel(agentId2, running) {
27609
+ const picked = this.options.state.models?.[agentId2];
27610
+ return picked === null ? void 0 : picked ?? running.config.model;
27611
+ }
27612
+ /** What a turn's runs ask Hermes for: the agent's own model, else nothing, and the profile's runs. */
27613
+ runModel(turn) {
27614
+ const picked = this.options.state.models?.[turn.who];
27615
+ if (picked === null) return {};
27616
+ if (picked !== void 0) return runModelOf(picked);
27617
+ return turn.agent.config.model === void 0 ? {} : { model: turn.agent.config.model };
27147
27618
  }
27148
27619
  /**
27149
- * Which profile answers for a Chips agent: its own; a copy's original's; or the first for the
27150
- * harness itself and for any other of the harness's — its default agent, one Chips made since —
27151
- * read afresh once when a name is new.
27620
+ * Each agent's model, sent to Chips (Chips ADR 0141): the models Hermes has for its profile, read
27621
+ * once a profile, with the one it runs on. One whose Hermes doesn't answer is sent none, and the
27622
+ * log says so once.
27152
27623
  */
27153
- async profileOf(taker) {
27154
- const own2 = this.agents.find((agent) => agent.agentId === taker);
27155
- if (own2 !== void 0) return own2;
27156
- const copied = this.options.state.profiles?.[taker];
27157
- const original = this.agents.find((agent) => copied !== void 0 && agent.key === copied);
27158
- if (original !== void 0) return original;
27159
- if (taker !== this.options.chips.harnessId && !this.ours.has(taker)) {
27160
- const listed = await this.options.chips.agents.list().catch(() => []);
27161
- for (const agent of listed) {
27162
- this.ours.add(agent.agentId);
27163
- this.names.set(agent.agentId, agent.name);
27624
+ async sendModels() {
27625
+ const { chips: chips2 } = this.options;
27626
+ const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
27627
+ const read = /* @__PURE__ */ new Map();
27628
+ for (const agentId2 of ids2) {
27629
+ const running = this.runningFor(agentId2);
27630
+ if (running === void 0) continue;
27631
+ try {
27632
+ let options = read.get(running);
27633
+ if (options === void 0) {
27634
+ options = running.client.modelOptions();
27635
+ read.set(running, options);
27636
+ }
27637
+ const copy = copyOf(await options, this.ownModel(agentId2, running));
27638
+ if (copy !== void 0) await chips2.settings.model(agentId2, copy);
27639
+ } catch (error62) {
27640
+ if (!this.toldModels) this.log(`models: none sent for ${running.config.name}: ${messageOf3(error62)}`);
27641
+ this.toldModels = true;
27164
27642
  }
27165
27643
  }
27166
- return taker === this.options.chips.harnessId || this.ours.has(taker) ? this.agents[0] : void 0;
27167
27644
  }
27168
27645
  /**
27169
- * The session a turn is taken in: the profile's own in the room, for its agent or the harness
27170
- * itself, and one of its own for any other of the harness's, so two in a room are two.
27646
+ * A person picked an agent's model (Chips ADR 0141): one Hermes has for its profile, kept as the
27647
+ * model each of its runs asks for from now on, or Hermes's default (`null`), the profile's own.
27648
+ * The model it runs on already changes nothing. Hermes says no levels of effort, so none is taken.
27171
27649
  */
27172
- roomOf(agent, taker, spaceId) {
27173
- const own2 = taker === agent.agentId || taker === this.options.chips.harnessId;
27174
- const id2 = `${own2 ? agent.key : taker}|${spaceId}`;
27175
- let room = this.rooms.get(id2);
27176
- if (room === void 0) {
27177
- room = { id: id2, agent, spaceId, queue: [], busy: false };
27178
- this.rooms.set(id2, room);
27179
- }
27180
- return room;
27181
- }
27182
- async pump(room) {
27183
- if (room.busy) return;
27184
- const asked = room.queue.shift();
27185
- if (asked === void 0) return;
27186
- room.busy = true;
27187
- try {
27188
- await this.turn(room, asked);
27189
- } catch (error62) {
27190
- this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf2(error62)}`);
27191
- } finally {
27192
- room.busy = false;
27193
- void this.contexts?.refresh();
27194
- void this.pump(room);
27195
- }
27196
- }
27197
- async turn(room, { message, speakerId, goal, sayWho }) {
27198
- const { agent, spaceId } = room;
27199
- const speaker = speakerId === void 0 ? this.options.chips : this.options.chips.as(speakerId);
27200
- const run2 = await speaker.runs.start({ spaceId, goal: goal ?? goalOf(message, agent.config.name) });
27201
- const turn = {
27202
- run: run2,
27203
- speaker,
27204
- agent,
27205
- hermesRun: "",
27206
- reply: "",
27207
- saidAt: 0,
27208
- cancelled: false,
27209
- next: [],
27210
- open: /* @__PURE__ */ new Map(),
27211
- counted: 0,
27212
- approvals: /* @__PURE__ */ new Map(),
27213
- allowed: [],
27214
- interrupted: false
27215
- };
27216
- run2.on("cancel", () => {
27217
- turn.cancelled = true;
27218
- if (turn.hermesRun !== "") void agent.client.stop(turn.hermesRun).catch(() => void 0);
27219
- });
27220
- run2.on("steer", (steer) => {
27221
- const words4 = `${steer.from?.display_name ?? "The person"} adds: ${steer.text}`;
27222
- void this.steered(turn, words4, steer.mode);
27223
- });
27224
- let session;
27650
+ async onModelChange(frame) {
27651
+ const { state: state2 } = this.options;
27652
+ const agentId2 = frame.agent_id;
27653
+ const ours2 = this.ours.has(agentId2) || this.agents.some((agent) => agent.agentId === agentId2) || this.own.has(agentId2);
27654
+ const running = ours2 ? this.runningFor(agentId2) : void 0;
27655
+ if (running === void 0) throw new Refuse(`${agentId2} isn't one of this harness's agents.`);
27656
+ let options;
27225
27657
  try {
27226
- session = await this.sessionFor(room);
27658
+ options = await running.client.modelOptions();
27227
27659
  } catch (error62) {
27228
- await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf2(error62)}`, true);
27229
- return;
27660
+ throw new Refuse(`Hermes didn't say which models it has: ${messageOf3(error62)}`);
27230
27661
  }
27231
- const name2 = speakerId === void 0 ? agent.config.name : await this.nameOf(speakerId, agent.config.name);
27232
- turn.folder = this.folderOf(agent, speakerId ?? agent.agentId, name2);
27233
- const project = await this.projectFor(turn, room.spaceId, speakerId ?? agent.agentId, name2);
27234
- let text = promptFor(message, { taker: speakerId, sayWho, project });
27235
- for (let round = 0; text !== void 0 && !turn.cancelled && !run2.over; round += 1) {
27236
- const last = await this.once(turn, session, text, `${run2.id}:${round}`);
27237
- if (last === void 0) return;
27238
- if (last.event === "run.cancelled" && turn.interrupted && turn.next.length > 0) {
27239
- turn.interrupted = false;
27240
- text = turn.next.shift();
27241
- continue;
27242
- }
27243
- if (last.event !== "run.completed") {
27244
- await this.ended(turn, last);
27245
- return;
27246
- }
27247
- text = turn.next.shift();
27248
- if (text === void 0) await this.finish(turn, last);
27662
+ const now = copyOf(options, this.ownModel(agentId2, running));
27663
+ if (now === void 0) throw new Refuse("Hermes has no model for this agent's profile.");
27664
+ if (frame.model === now.model) return now;
27665
+ if (frame.model !== null && !now.choices.some((choice) => choice.id === frame.model)) {
27666
+ throw new Refuse(`Hermes has no ${frame.model} for this agent's profile.`, now);
27249
27667
  }
27668
+ state2.models = { ...state2.models, [agentId2]: frame.model };
27669
+ this.options.saveState(state2);
27670
+ const name2 = await this.nameOf(agentId2, running.config.name);
27671
+ this.log(`${name2} runs on ${frame.model ?? `Hermes's default (${now.default?.model ?? "its profile's"})`}, from Chips`);
27672
+ return copyOf(options, this.ownModel(agentId2, running)) ?? now;
27250
27673
  }
27251
- /** One Hermes run of the turn: started, its events heard to the end. The end, or nothing on failure. */
27252
- async once(turn, session, text, key) {
27253
- const { agent } = turn;
27254
- try {
27255
- turn.hermesRun = await agent.client.startRun({ text, sessionId: session, model: agent.config.model, key, instructions: instructionsFor(turn.folder, turn.project !== void 0) });
27256
- } catch (error62) {
27257
- await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf2(error62)}`, true);
27258
- return void 0;
27674
+ /** A person's Compact now (Chips ADR 0146): Hermes can't be asked to compact from outside a run. */
27675
+ onWindowCompact(frame) {
27676
+ throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
27677
+ }
27678
+ /** A person's limit (Chips ADR 0146): Hermes sets it in its profile's config, for all its agents. */
27679
+ onWindowChange(frame) {
27680
+ const profile = this.profileFor(frame.agent_id);
27681
+ throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
27682
+ }
27683
+ /** The Main agent, the first registered, once started: who says the harness's first words. */
27684
+ get mainAgentId() {
27685
+ return this.agents[0]?.agentId;
27686
+ }
27687
+ /**
27688
+ * The Main agent: the one Chips made for the harness when it joined, keyed by the harness's own
27689
+ * id, registered as Main (Chips ADR 0183), so it is called what the config says and has no
27690
+ * second beside it. It goes home, and a Main of ours from before that, out of its room now, is
27691
+ * archived, which gives its name up. With no such agent (a harness from before them), the Main
27692
+ * Chips has is kept: a harness has one at most.
27693
+ */
27694
+ async seatMain(config3, known) {
27695
+ const { chips: chips2 } = this.options;
27696
+ const before = known.find((agent) => agent.lead);
27697
+ const made = known.some((agent) => agent.key === chips2.harnessId);
27698
+ if (!made && before !== void 0) {
27699
+ await this.goHome(before, known);
27700
+ return before;
27259
27701
  }
27260
- if (turn.cancelled) await agent.client.stop(turn.hermesRun).catch(() => void 0);
27261
- try {
27262
- const last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event));
27263
- return last ?? await this.endOf(turn);
27264
- } catch (error62) {
27265
- this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf2(error62)}`);
27266
- return await this.endOf(turn);
27702
+ const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
27703
+ await this.goHome(main2, known);
27704
+ if (before !== void 0 && before.agentId !== main2.agentId) {
27705
+ await chips2.agents.register({ key: before.key, name: before.name, state: "archived" }).catch((error62) => this.log(`${before.name} stays: ${String(error62)}`));
27267
27706
  }
27707
+ return main2;
27268
27708
  }
27269
- /** The end of a run whose stream was lost: its state read until it is over, as a terminal event. */
27270
- async endOf(turn) {
27271
- const by = Date.now() + LOST_STREAM_MS;
27272
- while (Date.now() < by) {
27273
- const state2 = await turn.agent.client.run(turn.hermesRun).catch(() => void 0);
27274
- const status = typeof state2?.["status"] === "string" ? state2["status"] : void 0;
27275
- if (status !== void 0 && TERMINAL.has(`run.${status}`)) {
27276
- return { ...state2, event: `run.${status}` };
27277
- }
27278
- await new Promise((resolve7) => setTimeout(resolve7, 1e3));
27279
- }
27280
- await this.failed(turn, `${turn.agent.config.name}'s run in Hermes stopped answering.`, true);
27281
- return void 0;
27709
+ /** The Main agent into the harness's own room, in the place of whichever of ours is there. */
27710
+ async goHome(main2, known) {
27711
+ const { chips: chips2, home: home2 } = this.options;
27712
+ if (home2 === void 0 || main2.spaces.includes(home2)) return;
27713
+ const there = known.find((agent) => agent.agentId !== main2.agentId && agent.spaces.includes(home2));
27714
+ await chips2.agents.attach(main2.agentId, { spaceId: home2, replacing: there?.agentId ?? chips2.harnessId }).catch((error62) => this.log(`${main2.name} stays out of its home: ${String(error62)}`));
27282
27715
  }
27283
- /** The room's session: the one it had, or a new one Hermes makes for it. */
27284
- async sessionFor({ id: id2, agent, spaceId }) {
27285
- const kept = this.options.state.sessions[id2];
27286
- if (kept !== void 0) return kept;
27287
- const who = id2.slice(0, id2.lastIndexOf("|"));
27288
- const made = await agent.client.newSession(`Chips space ${spaceId}${who === agent.key ? "" : ` \xB7 ${who}`}`);
27289
- this.options.state.sessions[id2] = made;
27290
- this.options.saveState(this.options.state);
27291
- return made;
27716
+ /**
27717
+ * A person asked for a new agent in a room (Chips ADR 0108): one of the harness's, keyed by the
27718
+ * ask so an ask heard twice makes one, registered for the room, which Chips puts it in while the
27719
+ * room waits. It answers through a Hermes profile of its own (`ownProfile`).
27720
+ */
27721
+ async make(asked) {
27722
+ const { chips: chips2 } = this.options;
27723
+ if (asked.harness_id !== chips2.harnessId || asked.space_id === void 0) return;
27724
+ const agent = await chips2.agents.register({
27725
+ key: `hermes:asked:${asked.id}`,
27726
+ name: asked.name,
27727
+ madeFor: asked.id,
27728
+ spaceId: asked.space_id
27729
+ });
27730
+ this.names.set(agent.agentId, agent.name);
27731
+ this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
27732
+ await this.ownProfile(agent.agentId, agent.name);
27733
+ this.ours.add(agent.agentId);
27734
+ void this.agentsMoved();
27292
27735
  }
27293
27736
  /**
27294
- * A person's words for a run that is going. A steer is said to the Hermes run, or to the next one
27295
- * of the turn when Hermes won't take it now. A follow-up waits for this one to end. An
27296
- * interrupt stops it, and the turn goes on with the words.
27737
+ * A Hermes profile of its own for an agent made in Chips (Chips ADR 0241), so it remembers only
27738
+ * its own conversations: named after the agent, cloned from the Main agent's profile so it starts
27739
+ * with its model, keys, tools, skills and SOUL.md, its memory empty (`profiles.ts`). The gateway
27740
+ * is asked to serve it now; one that serves only the profile it started with, as Hermes's does
27741
+ * when it started with one, is restarted once. Where Hermes doesn't make one, the agent runs on
27742
+ * the first profile, in a session of its own, and the log says why.
27297
27743
  */
27298
- async steered(turn, text, mode) {
27299
- if (mode === "interrupt" && turn.hermesRun !== "") {
27300
- turn.next.unshift(text);
27301
- turn.interrupted = true;
27302
- await turn.agent.client.stop(turn.hermesRun).catch(() => void 0);
27744
+ async ownProfile(agentId2, name2) {
27745
+ const { profiles, hermesRoot: root2, state: state2 } = this.options;
27746
+ const main2 = this.agents[0];
27747
+ if (profiles === void 0 || root2 === void 0 || main2 === void 0 || this.own.has(agentId2)) return;
27748
+ const from = profileName(main2.config.address.profile);
27749
+ const profile = freeProfile(root2, name2, Object.values(state2.made ?? {}));
27750
+ const home2 = profileHome(root2, profile);
27751
+ const secret = state2.fills?.secret ?? this.options.fills?.secret;
27752
+ try {
27753
+ await profiles.create(profile, from);
27754
+ const port = this.fillsPort;
27755
+ const hook = readyProfile(home2, { from, profile }, port === void 0 || secret === void 0 ? void 0 : { port, secret });
27756
+ if (hook?.state === "by_hand") this.log(`context window: ${hook.path} was left as it is: ${hook.why}. Add these lines to it:
27757
+ ${hook.lines}`);
27758
+ } catch (error62) {
27759
+ this.log(`${name2} runs on the ${from} profile, in a session of its own: Hermes didn't make it one (${messageOf3(error62)})`);
27303
27760
  return;
27304
27761
  }
27305
- if (mode === "steer" && turn.hermesRun !== "") {
27306
- try {
27307
- await turn.agent.client.steer(turn.hermesRun, text);
27308
- return;
27309
- } catch (error62) {
27310
- this.log(`Hermes didn't take a steer mid-run (${messageOf2(error62)}); saying it next`);
27311
- }
27762
+ state2.made = { ...state2.made, [agentId2]: profile };
27763
+ this.options.saveState(state2);
27764
+ const running = this.runningOn(agentId2, profile, name2);
27765
+ if (running !== void 0) this.own.set(agentId2, running);
27766
+ this.log(`${name2} has a Hermes profile of its own, ${profile}, cloned from ${from}`);
27767
+ const served = await profiles.serve().catch(() => void 0);
27768
+ if (served === false) {
27769
+ this.log(`Hermes's gateway serves only the profile it started with, as it does when there was one: restarting it once, so it serves ${profile} too`);
27770
+ await this.options.restartGateway?.();
27771
+ } else if (served === void 0) {
27772
+ this.log(`Hermes's gateway didn't answer on its control socket: it serves ${profile} once it looks again, or starts again`);
27312
27773
  }
27313
- turn.next.push(text);
27314
27774
  }
27315
- // ------------------------------------------------------------------ what Hermes streams
27316
- async heard(turn, event) {
27317
- if (turn.run.over) return;
27318
- switch (event.event) {
27319
- case "message.delta":
27320
- turn.reply += typeof event["delta"] === "string" ? event["delta"] : "";
27321
- return;
27322
- case "message.interim":
27323
- case "reasoning.available":
27324
- this.sayThought(turn, String(event["text"] ?? ""), event.event === "message.interim" ? "writing" : "thinking");
27325
- return;
27326
- case "tool.started":
27327
- case "subagent.start":
27328
- await this.started(turn, event);
27329
- return;
27330
- case "tool.completed":
27331
- case "subagent.complete":
27332
- await this.completed(turn, event);
27333
- return;
27334
- case "approval.request":
27335
- void this.ask(turn, event);
27336
- return;
27337
- case "approval.responded":
27338
- await this.settled(turn, event);
27339
- return;
27340
- default:
27341
- return;
27342
- }
27775
+ /**
27776
+ * A profile made for an agent, as the bridge reaches it: under `/p/<profile>` of the gateway the
27777
+ * Main agent is reached at, with the key in the profile's own `.env`.
27778
+ */
27779
+ runningOn(agentId2, profile, name2) {
27780
+ const main2 = this.agents[0];
27781
+ const root2 = this.options.hermesRoot;
27782
+ if (main2 === void 0 || root2 === void 0) return void 0;
27783
+ const home2 = profileHome(root2, profile);
27784
+ const config3 = {
27785
+ name: name2,
27786
+ address: { url: main2.config.address.url, profile, key: envValue(home2, KEY_ENV) ?? "" },
27787
+ keyEnv: KEY_ENV,
27788
+ keyFrom: join10(home2, ".env")
27789
+ };
27790
+ const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
27791
+ return { config: config3, key: agentId2, client, agentId: agentId2, up: true };
27343
27792
  }
27344
- sayThought(turn, text, phase) {
27345
- const now = Date.now();
27346
- const line2 = lastLine(text);
27347
- if (line2 === "" || now - turn.saidAt < QUIET_MS) return;
27348
- turn.saidAt = now;
27349
- void turn.run.status({ phase, verbPhrase: line2.slice(0, 200) }).catch(() => void 0);
27793
+ /**
27794
+ * A space a person asked the harness for from the + (Chips ADR 0241), as the ACP bridge takes
27795
+ * one: claimed under a key made from the space, so a space handed over twice is claimed once,
27796
+ * and named from its first words. Chips makes the agent of that key and puts it in the space; it
27797
+ * gets a Hermes profile of its own (`ownProfile`). It answers the first words, which are the
27798
+ * ask's and aren't heard again.
27799
+ */
27800
+ async claim(asked) {
27801
+ const { chips: chips2 } = this.options;
27802
+ const { spaceId, firstMessage } = asked;
27803
+ const key = `hermes:space:${spaceId}`;
27804
+ await chips2.claimSpace(spaceId, key, titleFrom(firstMessage.text));
27805
+ const agent = (await chips2.agents.list()).find((each) => each.key === key);
27806
+ if (agent === void 0) {
27807
+ this.log(`claimed ${spaceId}, and Chips put no agent there`);
27808
+ return;
27809
+ }
27810
+ this.names.set(agent.agentId, agent.name);
27811
+ this.log(`${agent.name} claimed ${spaceId}`);
27812
+ await this.ownProfile(agent.agentId, agent.name);
27813
+ this.ours.add(agent.agentId);
27814
+ void this.agentsMoved();
27815
+ const message = {
27816
+ type: "message.new",
27817
+ space_id: spaceId,
27818
+ message_id: firstMessage.messageId,
27819
+ text: firstMessage.text,
27820
+ from: { participant_id: asked.askedBy, kind: "person", display_name: await this.whoSaid(asked), role: "member" },
27821
+ for: [agent.agentId]
27822
+ };
27823
+ void this.hear(message);
27350
27824
  }
27351
- async started(turn, event) {
27352
- const sub = event.event === "subagent.start";
27353
- const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
27354
- turn.counted += 1;
27355
- const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
27356
- const stepId = callId ?? `${tool}-${turn.counted}`;
27357
- const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
27358
- turn.open.set(lane, [...turn.open.get(lane) ?? [], stepId]);
27359
- const said2 = sub ? event["goal"] : event["preview"];
27360
- await turn.run.step({
27361
- stepId,
27362
- status: "started",
27363
- title: `${tool}${typeof said2 === "string" && said2 !== "" ? ` \xB7 ${said2}` : ""}`.slice(0, 300),
27364
- tool,
27365
- ...KINDS[tool] === void 0 ? {} : { kind: KINDS[tool] }
27366
- }).catch(() => void 0);
27825
+ /** Who said a space's first words, from its lines. */
27826
+ async whoSaid(asked) {
27827
+ const context = await this.options.chips.roomContext(asked.spaceId, { limit: 20 }).catch(() => void 0);
27828
+ return context?.lines.find((line2) => line2.message_id === asked.firstMessage.messageId)?.from.name ?? "Someone";
27367
27829
  }
27368
- async completed(turn, event) {
27369
- const sub = event.event === "subagent.complete";
27370
- const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
27371
- const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
27372
- const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
27373
- const waiting = turn.open.get(lane) ?? [];
27374
- const stepId = callId ?? waiting.shift() ?? `${tool}-${turn.counted += 1}`;
27375
- turn.open.set(lane, waiting.filter((each) => each !== stepId));
27376
- const failed = event["error"] === true || sub && event["status"] === "failed";
27377
- const approvalId = sub ? void 0 : turn.allowed.shift();
27378
- if (approvalId !== void 0) {
27379
- await turn.run.executed(approvalId, failed ? "failed" : "executed").catch(() => void 0);
27830
+ /**
27831
+ * A person asked for a copy of one of the harness's agents in another room (ADR 0087, Fork): the
27832
+ * agent's session in the room it is in, else the last it had, forked in Hermes, is the copy's
27833
+ * session in the room asked. The copy answers through its original's profile, whose Hermes has
27834
+ * that session (Hermes forks a session only within its profile, so a copy gets no profile of its
27835
+ * own: Chips ADR 0241), and works in its original's folder. It is registered with `forkedFrom` for the
27836
+ * room, which Chips puts it in while the room waits. The fork and the copy are keyed by the ask,
27837
+ * so an ask heard twice forks once (Hermes refuses the id again) and registers the same copy. An
27838
+ * agent that never spoke is copied with nothing to remember. A turn still going is not in the
27839
+ * copy: Hermes copies what the session holds. Nothing is made for an ask the room has stopped
27840
+ * waiting for (`FORK_WAIT_MS`), nor for an agent that isn't the harness's.
27841
+ */
27842
+ async copy(fork) {
27843
+ const { chips: chips2, state: state2 } = this.options;
27844
+ const spaceId = fork.space_id;
27845
+ if (spaceId === void 0) return;
27846
+ const late = Date.now() - Date.parse(fork.ts);
27847
+ if (late > FORK_WAIT_MS) {
27848
+ this.log(`no copy of ${fork.agent_id}: asked ${Math.round(late / 1e3)} s ago, and the room waits a minute`);
27849
+ return;
27380
27850
  }
27381
- const summary = sub ? event["summary"] : resultWords(event["preview"]);
27382
- const seconds = typeof event["duration"] === "number" ? event["duration"] : void 0;
27383
- await turn.run.step({
27384
- stepId,
27385
- status: failed ? "failed" : "done",
27386
- tool,
27387
- ...typeof summary === "string" && summary !== "" ? { summary: summary.slice(0, 2e3) } : {},
27388
- ...seconds === void 0 ? {} : { durationMs: Math.round(seconds * 1e3) }
27389
- }).catch(() => void 0);
27390
- const cost3 = event["cost_usd"];
27391
- if (sub && typeof cost3 === "number" && cost3 > 0) {
27392
- const model = typeof event["model"] === "string" ? event["model"] : "subagent";
27393
- await turn.run.usage({ scope: "step", model, costUsd: cost3 }).catch(() => void 0);
27851
+ const original = await this.profileOf(fork.agent_id);
27852
+ if (original === void 0) {
27853
+ this.log(`no copy of ${fork.agent_id}: it isn't one of this harness's agents`);
27854
+ return;
27394
27855
  }
27856
+ const name2 = await this.nameOf(fork.agent_id, original.config.name);
27857
+ const key = `hermes:fork:${fork.id}`;
27858
+ const who = fork.agent_id === original.agentId ? original.key : fork.agent_id;
27859
+ const listed = await chips2.agents.list().catch(() => []);
27860
+ const room = listed.find((agent) => agent.agentId === fork.agent_id)?.room ?? void 0;
27861
+ const from = (room === void 0 ? void 0 : state2.sessions[`${who}|${room}`]) ?? Object.entries(state2.sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
27862
+ if (from === void 0) this.log(`${name2} never spoke: its copy remembers nothing`);
27863
+ const session = from === void 0 ? void 0 : await original.client.forkSession(from, { id: `chips-fork-${fork.id}`, title: `Chips space ${spaceId} \xB7 ${key}` });
27864
+ const folder2 = this.folderOf(original, fork.agent_id, name2);
27865
+ const copy = await chips2.agents.register({ key, name: forkName(name2), forkedFrom: fork.agent_id, spaceId });
27866
+ if (session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] ??= session;
27867
+ state2.profiles = { ...state2.profiles, [copy.agentId]: original.key };
27868
+ if (folder2 !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: folder2 };
27869
+ this.options.saveState(state2);
27870
+ this.ours.add(copy.agentId);
27871
+ this.names.set(copy.agentId, copy.name);
27872
+ const away = copy.attached === false && !copy.spaces.includes(spaceId);
27873
+ this.log(`${copy.name}, a copy of ${name2}, made${away ? `, not in the room: ${copy.why ?? "too late"}` : ""}`);
27874
+ void this.agentsMoved();
27395
27875
  }
27396
- // ------------------------------------------------------------------ what Hermes asks
27397
- /** An approval Hermes wants before it acts, as a card; the answer is Hermes's choice. */
27398
- async ask(turn, event) {
27399
- const requestId = idOf(event);
27400
- const command = typeof event["command"] === "string" ? event["command"] : void 0;
27401
- const description = typeof event["description"] === "string" ? event["description"] : void 0;
27402
- const choices = Array.isArray(event["choices"]) ? event["choices"].map(String) : ["once", "deny"];
27403
- const approvalId = ids.next("approval");
27404
- turn.approvals.set(requestId ?? approvalId, { approvalId, answered: false });
27405
- const asked = turn.run.approval({
27406
- approvalId,
27407
- action: {
27408
- kind: command === void 0 ? "hermes.approval" : "shell.exec",
27409
- input: { ...command === void 0 ? {} : { command }, ...description === void 0 ? {} : { description } }
27410
- },
27411
- question: (description === void 0 ? "Run this command?" : `Allow it to ${lowerFirst(description)}?`).slice(0, 300),
27412
- lasts: "once",
27413
- risk: [],
27414
- ...command === void 0 ? {} : { preview: { format: "markdown", title: "Run a command", body: `\`\`\`sh
27415
- ${command}
27416
- \`\`\`` } }
27417
- });
27418
- const stopped2 = new Promise((resolve7) => turn.run.on("cancel", () => resolve7(void 0)));
27419
- const decision = await Promise.race([asked, stopped2]);
27420
- const pending = turn.approvals.get(requestId ?? approvalId);
27421
- if (decision === void 0 || pending === void 0 || pending.answered) return;
27422
- pending.answered = true;
27423
- if (decision.allowed) turn.allowed.push(approvalId);
27424
- const generation = event["execution_generation"];
27425
- await turn.agent.client.approve(turn.hermesRun, {
27426
- choice: choiceOf(decision, choices, command !== void 0),
27427
- ...requestId === void 0 ? {} : { request_id: requestId },
27428
- ...typeof generation === "number" ? { execution_generation: generation } : {}
27429
- }).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf2(error62)}`));
27430
- }
27431
- /** An approval answered in Hermes's own tools: settled in Chips, once, so the card cools (ADR 0079). */
27432
- async settled(turn, event) {
27433
- const requestId = idOf(event);
27434
- const pending = requestId === void 0 ? void 0 : turn.approvals.get(requestId);
27435
- if (pending === void 0 || pending.answered) return;
27436
- pending.answered = true;
27437
- const denied = event["choice"] === "deny";
27438
- await this.options.chips.send(
27439
- "approval.resolved",
27440
- { run_id: turn.run.id },
27441
- {
27442
- approval_id: pending.approvalId,
27443
- decision: denied ? "denied" : "approved",
27444
- scope: "once",
27445
- reason: "Answered in Hermes."
27446
- },
27447
- `hermes-approval:${turn.hermesRun}:${requestId}:settled`
27448
- ).catch(() => void 0);
27449
- }
27450
- // ------------------------------------------------------------------ the end
27451
- /** The turn is over: its words in the room, its tokens, and the run ended. */
27452
- async finish(turn, last) {
27453
- const { run: run2, agent } = turn;
27454
- if (run2.over) return;
27455
- await this.closeSteps(turn, "done");
27456
- for (const approvalId of turn.allowed.splice(0)) {
27457
- await run2.executed(approvalId, "executed", "Hermes didn't say how it went.").catch(() => void 0);
27458
- }
27459
- await this.counted(turn, last);
27460
- const output2 = typeof last["output"] === "string" ? last["output"].trim() : "";
27461
- const answer = output2 !== "" ? output2 : turn.reply.trim();
27462
- const { change, note } = await this.sendProject(turn, answer);
27463
- const { folder: folder2 } = turn;
27464
- const { chips: chips2 } = this.options;
27465
- const words4 = [answer, note ?? ""].filter((part) => part !== "").join("\n\n");
27466
- const post = await sendAnswer(words4, {
27467
- folders: folder2 === void 0 ? [] : [folder2],
27468
- upload: (input2) => chips2.upload(input2)
27469
- });
27470
- const reply = post.text;
27471
- if (reply !== "" || post.uploadIds.length > 0) {
27472
- await turn.speaker.post({ spaceId: run2.spaceId, runId: run2.id, text: reply, uploadIds: post.uploadIds, change });
27876
+ /** An agent's name, from Chips's list when it isn't known yet. */
27877
+ async nameOf(agentId2, otherwise) {
27878
+ if (!this.names.has(agentId2)) {
27879
+ const listed = await this.options.chips.agents.list().catch(() => []);
27880
+ for (const agent of listed) this.names.set(agent.agentId, agent.name);
27473
27881
  }
27474
- const said2 = post.summary;
27475
- const summary = said2 === "" ? "Done." : said2.length <= 2e3 ? said2 : `${said2.slice(0, 1990)}\u2026`;
27476
- await run2.complete({ summary });
27477
- this.log(`${agent.config.name} answered in ${run2.spaceId}`);
27478
- this.missingReadings();
27882
+ return this.names.get(agentId2) ?? otherwise;
27883
+ }
27884
+ /**
27885
+ * The folder an agent works in and sends its files from: the one the config names for them all
27886
+ * (`folder`, as it was), else one of its own under `folders`, named after it the first time it
27887
+ * takes a turn and kept, so a rename keeps it. Every agent once shared one folder, and a team's
27888
+ * files overwrote a one-agent room's game (Chips TT-16); and the one folder had to be Hermes's
27889
+ * own working folder, which a plain Hermes doesn't set (the setup audit, 17 October 2026).
27890
+ */
27891
+ folderOf(agent, agentId2, name2) {
27892
+ if (agent.config.folder !== void 0) return agent.config.folder;
27893
+ const { folders: root2, state: state2 } = this.options;
27894
+ if (root2 === void 0) return void 0;
27895
+ const kept = state2.folders?.[agentId2];
27896
+ const folder2 = kept ?? freeFolder(root2, name2, Object.values(state2.folders ?? {}));
27897
+ mkdirSync5(folder2, { recursive: true });
27898
+ if (kept === void 0) {
27899
+ state2.folders = { ...state2.folders, [agentId2]: folder2 };
27900
+ this.options.saveState(state2);
27901
+ }
27902
+ return folder2;
27479
27903
  }
27480
27904
  /**
27481
- * A turn ended and Hermes sent no reading at all since the bridge started: its gateway most
27482
- * likely started before Chips's webhook was in its config. Said once, a little after, since a
27483
- * reading comes a moment after its model call.
27905
+ * The room's project for a turn (Chips ADR 0221): the agent's copy, in its folder, brought up to
27906
+ * date, and what the turn is told of it. None for an agent a person pointed at a folder of their
27907
+ * own, nor where this machine has no git for it.
27484
27908
  */
27485
- missingReadings() {
27486
- if (this.fills === void 0 || this.fillsPort === void 0 || this.fills.anyHeard || this.toldNoReadings) return;
27487
- this.toldNoReadings = true;
27488
- setTimeout(() => {
27489
- if (this.fills?.anyHeard === false) {
27490
- this.log(
27491
- "context window: Hermes sent no reading after a turn. Start hermes gateway again once, so it takes Chips's webhook from its config."
27492
- );
27493
- }
27494
- }, 1e4).unref();
27495
- }
27496
- /** A run that failed, was stopped or was cut short in Hermes. */
27497
- async ended(turn, last) {
27498
- await this.closeSteps(turn, "failed");
27499
- await this.counted(turn, last);
27500
- if (turn.cancelled || turn.run.over) return;
27501
- const name2 = turn.agent.config.name;
27502
- if (last.event === "run.cancelled") await this.failed(turn, `${name2}'s run was stopped in Hermes.`, false);
27503
- else if (last.event === "run.interrupted") await this.failed(turn, `Hermes restarted during ${name2}'s run.`, true);
27504
- else {
27505
- const why = typeof last["error"] === "string" && last["error"] !== "" ? last["error"] : "it failed in Hermes";
27506
- await this.failed(turn, `${name2}: ${why}`, true);
27909
+ async projectFor(turn, spaceId, agentId2, name2) {
27910
+ if (this.options.projects !== true || turn.agent.config.folder !== void 0) return void 0;
27911
+ const folder2 = turn.folder;
27912
+ if (folder2 === void 0) return void 0;
27913
+ try {
27914
+ const project = this.options.chips.project({ spaceId, agentId: agentId2, name: name2, folder: folder2, log: (line2) => this.log(line2) });
27915
+ const facts = await project.pull();
27916
+ turn.project = { copy: project, name: name2 };
27917
+ return facts;
27918
+ } catch (error62) {
27919
+ this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf3(error62)}`);
27920
+ return void 0;
27507
27921
  }
27508
27922
  }
27509
27923
  /**
27510
- * The tokens Hermes counted, as a step's share, so they add up over the turn's runs. Hermes's
27511
- * `input_tokens` is already every token in, its cache's reads and writes among them (its
27512
- * `CanonicalUsage.prompt_tokens`), which is what Chips counts (Chips ADR 0214); the cache's two
27513
- * counts go with it, and are left out where Hermes doesn't say them.
27924
+ * What the turn changed in its copy, sent to the room's project before its words are posted: the
27925
+ * commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one; else, when
27926
+ * the change didn't go, the line the reply says so in (Chips AP-6).
27514
27927
  */
27515
- async counted(turn, last) {
27516
- const usage2 = last["usage"] ?? {};
27517
- const count = (key) => typeof usage2[key] === "number" ? usage2[key] : void 0;
27518
- const input2 = count("input_tokens") ?? 0;
27519
- const output2 = count("output_tokens") ?? 0;
27520
- if (input2 + output2 === 0) return;
27521
- const runtime = last["runtime"] ?? {};
27522
- const model = typeof runtime["model"] === "string" ? runtime["model"] : turn.agent.config.model ?? "hermes";
27523
- await turn.run.usage({
27524
- scope: "step",
27525
- model,
27526
- inputTokens: input2,
27527
- cacheReadTokens: count("cache_read_tokens"),
27528
- cacheWriteTokens: count("cache_write_tokens"),
27529
- outputTokens: output2
27530
- }).catch(() => void 0);
27531
- }
27532
- async closeSteps(turn, status) {
27533
- for (const [, ids2] of turn.open) {
27534
- for (const stepId of ids2) await turn.run.step({ stepId, status }).catch(() => void 0);
27928
+ async sendProject(turn, words4) {
27929
+ if (turn.project === void 0) return {};
27930
+ const { copy, name: name2 } = turn.project;
27931
+ try {
27932
+ const pushed = await copy.push({ words: words4 });
27933
+ this.log(`${name2}'s project: ${pushedInWords(pushed)}`);
27934
+ if (pushed.state === "sent") return { change: pushed.commit };
27935
+ const note = heldNote(pushed, name2);
27936
+ return note === void 0 ? {} : { note };
27937
+ } catch (error62) {
27938
+ this.log(`${name2}'s project couldn't be sent: ${messageOf3(error62)}`);
27939
+ const note = heldNote({ state: "failed", why: messageOf3(error62) }, name2);
27940
+ return note === void 0 ? {} : { note };
27535
27941
  }
27536
- turn.open.clear();
27537
27942
  }
27538
- async failed(turn, error62, retryable) {
27539
- if (!turn.run.over) await turn.run.fail({ error: error62, retryable }).catch(() => void 0);
27540
- if (retryable) void this.sayHowItIs();
27943
+ /** Trouble while a profile's gateway doesn't answer, and fine once all do (ADR 0083); nothing while a restart runs. */
27944
+ async sayHowItIs() {
27945
+ if (this.held) return;
27946
+ for (const agent of this.agents) agent.up = await agent.client.healthy();
27947
+ if (this.held) return;
27948
+ const down = this.agents.filter((agent) => !agent.up);
27949
+ if (down.length === 0) this.why = void 0;
27950
+ const chips2 = this.options.chips;
27951
+ const said2 = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(this.why ?? `Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
27952
+ await said2.catch(() => void 0);
27541
27953
  }
27542
- };
27543
- function choiceOf(decision, choices, command = false) {
27544
- if (!decision.allowed) return "deny";
27545
- if (command) return "once";
27546
- if (decision.scope === "always" && choices.includes("always")) return "always";
27547
- if (decision.scope !== "once" && choices.includes("session")) return "session";
27548
- return "once";
27549
- }
27550
- function idOf(event) {
27551
- const id2 = event["request_id"] ?? event["prompt_id"];
27552
- return typeof id2 === "string" && id2 !== "" ? id2 : void 0;
27553
- }
27554
- function messageOf2(error62) {
27555
- if (error62 instanceof HermesError) return `${error62.message} (${error62.status})`;
27556
- return error62 instanceof Error ? error62.message : String(error62);
27557
- }
27558
- function forkName(original) {
27559
- return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
27560
- }
27561
- function goalOf(message, name2) {
27562
- const first = message.text.split("\n").find((line2) => line2.trim() !== "")?.trim() ?? "";
27563
- const named = first.toLowerCase().startsWith(`@${name2.toLowerCase()}`);
27564
- const goal = named ? first.slice(name2.length + 1).trim() : first.replace(/^@\S+\s*/, "");
27565
- return (goal === "" ? `Answer ${message.from.display_name}` : goal).slice(0, 300);
27566
- }
27567
- var REPLY_SHOWN = 1500;
27568
- var GOAL_SHOWN = 120;
27569
- function lookAgain(who, reply, note) {
27570
- const said2 = reply?.trim() ?? "";
27571
- return [
27572
- `${who} says your reply in this space wasn't right.`,
27573
- ...said2 === "" ? [] : [`Your reply:
27574
- \xAB${cut(said2, REPLY_SHOWN)}\xBB`],
27575
- note === void 0 || note === "" ? "They didn't say what. Read your reply again and find what was wrong." : `What wasn't right: \xAB${note}\xBB`,
27576
- "Look at it again and answer properly in its place. If it is not clear what was wrong, ask them in one short line. Say plainly what you changed."
27577
- ].join("\n\n");
27578
- }
27579
- function lookingAgain(note) {
27580
- const first = note?.split("\n").find((line2) => line2.trim() !== "")?.trim();
27581
- return first === void 0 ? "Looking again at its reply" : cut(`Looking again: ${first}`, GOAL_SHOWN);
27582
- }
27583
- function cut(text, most) {
27584
- return text.length <= most ? text : `${text.slice(0, most - 1)}\u2026`;
27585
- }
27586
- function resultWords(preview2) {
27587
- if (typeof preview2 !== "string" || !preview2.trimStart().startsWith("{")) return preview2;
27588
- let result;
27589
- try {
27590
- result = JSON.parse(preview2);
27591
- } catch {
27592
- return preview2;
27954
+ // ------------------------------------------------------------------ what is said to it
27955
+ /** A message for some of its agents: each takes a turn in its room, one after another there. */
27956
+ async hear(message, how = {}) {
27957
+ const spaceId = message.space_id;
27958
+ if (spaceId === void 0) return;
27959
+ const harnessId = this.options.chips.harnessId;
27960
+ for (const taker of message.for ?? [harnessId]) {
27961
+ const agent = await this.runnerOf(taker);
27962
+ if (agent === void 0) continue;
27963
+ const room = this.roomOf(agent, taker, spaceId);
27964
+ room.queue.push({ message, speakerId: taker === harnessId ? void 0 : taker, ...how });
27965
+ void this.pump(room);
27966
+ }
27593
27967
  }
27594
- if (typeof result !== "object" || result === null || !("output" in result)) return preview2;
27595
- const { output: output2, error: error62 } = result;
27596
- const said2 = [output2, error62].find((each) => typeof each === "string" && each.trim() !== "");
27597
- return typeof said2 === "string" ? said2.trim() : "";
27598
- }
27599
- function lastLine(text) {
27600
- const lines2 = text.split(/(?<=[.!?])\s+|\n+/).map((line2) => line2.trim()).filter((line2) => line2 !== "");
27601
- return lines2.at(-1) ?? "";
27602
- }
27603
- function lowerFirst(text) {
27604
- return text.length === 0 ? text : `${text[0]?.toLowerCase() ?? ""}${text.slice(1)}`;
27605
- }
27606
-
27607
- // src/gateway.ts
27608
- import { spawn as spawn2 } from "node:child_process";
27609
- import { createHash as createHash3 } from "node:crypto";
27610
- import { accessSync as accessSync2, appendFileSync as appendFileSync2, closeSync as closeSync2, constants as constants2, existsSync as existsSync8, openSync as openSync2, readFileSync as readFileSync8, readSync, statSync as statSync4 } from "node:fs";
27611
- import { homedir as homedir5 } from "node:os";
27612
- import { basename as basename4, delimiter as delimiter2, dirname as dirname5, join as join9, resolve as resolve3 } from "node:path";
27613
- var TIMING = {
27614
- stopMs: 15e3,
27615
- killMs: 5e3,
27616
- earlyMs: 1e4,
27617
- upMs: 9e4,
27618
- serviceMs: 15e4,
27619
- pollMs: 500
27620
- };
27621
- var EXPAT = "/opt/homebrew/opt/expat/lib/libexpat.1.dylib";
27622
- var EXPAT_SYMBOL = "_XML_SetAllocTrackerActivationThreshold";
27623
- function gatewaysOf(agents) {
27624
- return [...new Set(agents.map(({ address }) => address.url.replace(/\/+$/, "")))].map((url2) => ({ url: url2 }));
27625
- }
27626
- function isLocal(url2) {
27627
- try {
27628
- return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(new URL(url2).hostname);
27629
- } catch {
27630
- return false;
27968
+ /**
27969
+ * "Not right?" on one of its agents' replies (Chips ADR 0200): a person's note on it, or a bare
27970
+ * 👎 from an older phone, is a turn of the agent that said it, in its session in that room and
27971
+ * in line with the room's other turns, to look again and answer in its place. Any other
27972
+ * reaction is the person's own. Once per event, however often it is heard.
27973
+ */
27974
+ async notRight(feedback2) {
27975
+ const { space_id: spaceId, message_id: messageId } = feedback2;
27976
+ const note = feedback2.kind === "correction" ? (feedback2.text ?? "").trim() : void 0;
27977
+ const flagged = note === void 0 ? feedback2.kind === "reaction" && feedback2.reaction === "\u{1F44E}" : note !== "";
27978
+ if (spaceId === void 0 || !messageId || !flagged || this.lookedAt.has(feedback2.id)) return;
27979
+ this.lookedAt.add(feedback2.id);
27980
+ for (const old of this.lookedAt) {
27981
+ if (this.lookedAt.size <= KEPT_FEEDBACK) break;
27982
+ this.lookedAt.delete(old);
27983
+ }
27984
+ const context = await this.options.chips.roomContext(spaceId, { limit: 200 }).catch(() => void 0);
27985
+ const line2 = context?.lines.find((each) => each.message_id === messageId);
27986
+ const message = {
27987
+ type: "message.new",
27988
+ space_id: spaceId,
27989
+ // An id of its own, so nothing of the turn is taken for the reply's.
27990
+ message_id: messageIdOf(feedback2.id),
27991
+ text: lookAgain(feedback2.from.display_name, line2?.text, note),
27992
+ from: feedback2.from,
27993
+ ...line2 === void 0 ? {} : { for: [line2.from.id] }
27994
+ };
27995
+ await this.hear(message, { goal: lookingAgain(note), sayWho: false });
27631
27996
  }
27632
- }
27633
- function portOf(url2) {
27634
- const parsed = new URL(url2);
27635
- return Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
27636
- }
27637
- function line(words4) {
27638
- const one = words4.replace(/\s+/g, " ").trim();
27639
- return one.length <= 200 ? one : `${one.slice(0, 199)}\u2026`;
27640
- }
27641
- function whyFrom(output2, code) {
27642
- const lines2 = output2.split("\n").map((each) => each.trim()).filter((each) => each !== "");
27643
- const why = lines2.findLast((each) => /^[A-Za-z_][\w.]*(Error|Exception)\b/.test(each)) ?? lines2.at(-1);
27644
- if (why === void 0) return code === null || code === void 0 ? "it was stopped" : `it exited ${code}`;
27645
- return why.length <= 200 ? why : why.replace(/\/(?:[^\s/(),:'"]+\/)+([^\s/(),:'"]+)/g, "$1");
27646
- }
27647
- function gatewayRunArgs(command) {
27648
- const words4 = command.trim().split(/\s+/);
27649
- const at = words4.findIndex((word) => basename4(word) === "hermes");
27650
- if (at === -1) return void 0;
27651
- const args = words4.slice(at + 1);
27652
- const plain = [];
27653
- for (let next = 0; next < args.length; next += 1) {
27654
- const word = args[next] ?? "";
27655
- if (word === "-p" || word === "--profile") next += 1;
27656
- else if (!word.startsWith("-")) plain.push(word);
27997
+ /**
27998
+ * Which profile answers for a Chips agent, as `profileOf` says. An agent not settled may be one
27999
+ * being made, or a copy, which is waited for, so that its first message is answered on its own
28000
+ * profile, or in the session it was forked.
28001
+ */
28002
+ async runnerOf(taker) {
28003
+ const settled = taker === this.options.chips.harnessId || this.own.has(taker) || this.agents.some((agent) => agent.agentId === taker);
28004
+ if (!settled) await this.making;
28005
+ return this.profileOf(taker);
28006
+ }
28007
+ /**
28008
+ * Which profile answers for a Chips agent: its own, configured or made for it; a copy's
28009
+ * original's; or the first for the harness itself and for any other of the harness's — its
28010
+ * default agent, one made before profiles were — read afresh once when a name is new.
28011
+ */
28012
+ async profileOf(taker) {
28013
+ const own2 = this.agents.find((agent) => agent.agentId === taker) ?? this.own.get(taker);
28014
+ if (own2 !== void 0) return own2;
28015
+ const copied = this.options.state.profiles?.[taker];
28016
+ const original = copied === void 0 ? void 0 : this.byKey(copied);
28017
+ if (original !== void 0) return original;
28018
+ if (taker !== this.options.chips.harnessId && !this.ours.has(taker)) {
28019
+ const listed = await this.options.chips.agents.list().catch(() => []);
28020
+ for (const agent of listed) {
28021
+ this.ours.add(agent.agentId);
28022
+ this.names.set(agent.agentId, agent.name);
28023
+ }
28024
+ }
28025
+ return taker === this.options.chips.harnessId || this.ours.has(taker) ? this.agents[0] : void 0;
27657
28026
  }
27658
- return plain[0] === "gateway" && plain[1] === "run" ? args : void 0;
27659
- }
27660
- function serviceOf(system, root2, env = {}) {
27661
- const home2 = resolve3(root2);
27662
- const native = resolve3(system.home, ".hermes");
27663
- const suffix = home2 === native ? "" : dirname5(home2) === join9(native, "profiles") ? basename4(home2) : createHash3("sha256").update(home2).digest("hex").slice(0, 8);
27664
- if (system.platform === "darwin") {
27665
- const path = join9(system.home, "Library", "LaunchAgents", `ai.hermes.gateway${suffix ? `-${suffix}` : ""}.plist`);
27666
- return system.exists(path) ? { kind: "launchd", path } : void 0;
28027
+ /**
28028
+ * The session a turn is taken in: the profile's own in the room, for its agent or the harness
28029
+ * itself, and one of its own for any other of the harness's, so two in a room are two.
28030
+ */
28031
+ roomOf(agent, taker, spaceId) {
28032
+ const own2 = taker === agent.agentId || taker === this.options.chips.harnessId;
28033
+ const id2 = `${own2 ? agent.key : taker}|${spaceId}`;
28034
+ let room = this.rooms.get(id2);
28035
+ if (room === void 0) {
28036
+ room = { id: id2, agent, spaceId, queue: [], busy: false };
28037
+ this.rooms.set(id2, room);
28038
+ }
28039
+ return room;
27667
28040
  }
27668
- if (system.platform === "linux") {
27669
- const unit = `hermes-gateway${suffix ? `-${suffix}` : ""}.service`;
27670
- const user = join9(env["XDG_CONFIG_HOME"] || join9(system.home, ".config"), "systemd", "user", unit);
27671
- if (system.exists(user)) return { kind: "systemd", path: user };
27672
- const machine = join9("/etc/systemd/system", unit);
27673
- if (system.exists(machine)) return { kind: "systemd", path: machine, system: true };
28041
+ async pump(room) {
28042
+ if (room.busy) return;
28043
+ const asked = room.queue.shift();
28044
+ if (asked === void 0) return;
28045
+ room.busy = true;
28046
+ try {
28047
+ await this.turn(room, asked);
28048
+ } catch (error62) {
28049
+ this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf3(error62)}`);
28050
+ } finally {
28051
+ room.busy = false;
28052
+ void this.contexts?.refresh();
28053
+ void this.pump(room);
28054
+ }
27674
28055
  }
27675
- return void 0;
27676
- }
27677
- function expand(text, env) {
27678
- let out = "";
27679
- let at = 0;
27680
- while (at < text.length) {
27681
- const char = text[at] ?? "";
27682
- if (char === "\\" && at + 1 < text.length) {
27683
- out += text[at + 1];
27684
- at += 2;
27685
- } else if (char !== "$") {
27686
- out += char;
27687
- at += 1;
27688
- } else if (text[at + 1] === "{") {
27689
- let depth = 1;
27690
- let end2 = at + 2;
27691
- for (; end2 < text.length; end2 += 1) {
27692
- if (text[end2] === "{") depth += 1;
27693
- else if (text[end2] === "}" && --depth === 0) break;
28056
+ async turn(room, { message, speakerId, goal, sayWho }) {
28057
+ const { agent, spaceId } = room;
28058
+ const speaker = speakerId === void 0 ? this.options.chips : this.options.chips.as(speakerId);
28059
+ const run2 = await speaker.runs.start({ spaceId, goal: goal ?? goalOf(message, agent.config.name) });
28060
+ const turn = {
28061
+ run: run2,
28062
+ speaker,
28063
+ agent,
28064
+ who: speakerId ?? agent.agentId,
28065
+ hermesRun: "",
28066
+ reply: "",
28067
+ saidAt: 0,
28068
+ cancelled: false,
28069
+ next: [],
28070
+ open: /* @__PURE__ */ new Map(),
28071
+ counted: 0,
28072
+ approvals: /* @__PURE__ */ new Map(),
28073
+ allowed: [],
28074
+ interrupted: false
28075
+ };
28076
+ run2.on("cancel", () => {
28077
+ turn.cancelled = true;
28078
+ if (turn.hermesRun !== "") void agent.client.stop(turn.hermesRun).catch(() => void 0);
28079
+ });
28080
+ run2.on("steer", (steer) => {
28081
+ const words4 = `${steer.from?.display_name ?? "The person"} adds: ${steer.text}`;
28082
+ void this.steered(turn, words4, steer.mode);
28083
+ });
28084
+ let session;
28085
+ try {
28086
+ session = await this.sessionFor(room);
28087
+ } catch (error62) {
28088
+ await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf3(error62)}`, true);
28089
+ return;
28090
+ }
28091
+ const name2 = speakerId === void 0 ? agent.config.name : await this.nameOf(speakerId, agent.config.name);
28092
+ turn.folder = this.folderOf(agent, speakerId ?? agent.agentId, name2);
28093
+ const project = await this.projectFor(turn, room.spaceId, speakerId ?? agent.agentId, name2);
28094
+ let text = promptFor(message, { taker: speakerId, sayWho, project });
28095
+ for (let round = 0; text !== void 0 && !turn.cancelled && !run2.over; round += 1) {
28096
+ const last = await this.once(turn, session, text, `${run2.id}:${round}`);
28097
+ if (last === void 0) return;
28098
+ if (last.event === "run.cancelled" && turn.interrupted && turn.next.length > 0) {
28099
+ turn.interrupted = false;
28100
+ text = turn.next.shift();
28101
+ continue;
27694
28102
  }
27695
- const inner = /^([A-Za-z_][A-Za-z0-9_]*)(?:(:?)-([\s\S]*))?$/.exec(text.slice(at + 2, end2));
27696
- if (inner !== null) {
27697
- const value = env[inner[1] ?? ""];
27698
- const unset = inner[2] === ":" ? value === void 0 || value === "" : value === void 0;
27699
- out += inner[3] !== void 0 && unset ? expand(inner[3], env) : value ?? "";
28103
+ if (last.event !== "run.completed") {
28104
+ await this.ended(turn, last);
28105
+ return;
27700
28106
  }
27701
- at = end2 + 1;
27702
- } else {
27703
- const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(at + 1))?.[0];
27704
- out += name2 === void 0 ? "$" : env[name2] ?? "";
27705
- at += 1 + (name2?.length ?? 0);
28107
+ text = turn.next.shift();
28108
+ if (text === void 0) await this.finish(turn, last);
27706
28109
  }
27707
28110
  }
27708
- return out;
27709
- }
27710
- function wrapperOf(text, env) {
27711
- for (const each of text.split("\n")) {
27712
- const found = /^\s*((?:[A-Za-z_][A-Za-z0-9_]*="(?:[^"\\]|\\.)*"\s+)*)exec\s+"([^"]+)"/.exec(each);
27713
- if (found === null) continue;
27714
- const vars = {};
27715
- for (const set2 of (found[1] ?? "").matchAll(/([A-Za-z_][A-Za-z0-9_]*)="((?:[^"\\]|\\.)*)"/g)) {
27716
- vars[set2[1] ?? ""] = expand(set2[2] ?? "", env);
28111
+ /** One Hermes run of the turn: started, its events heard to the end. The end, or nothing on failure. */
28112
+ async once(turn, session, text, key) {
28113
+ const { agent } = turn;
28114
+ try {
28115
+ turn.hermesRun = await agent.client.startRun({
28116
+ text,
28117
+ sessionId: session,
28118
+ ...this.runModel(turn),
28119
+ key,
28120
+ instructions: instructionsFor(turn.folder, turn.project !== void 0)
28121
+ });
28122
+ } catch (error62) {
28123
+ await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf3(error62)}`, true);
28124
+ return void 0;
28125
+ }
28126
+ if (turn.cancelled) await agent.client.stop(turn.hermesRun).catch(() => void 0);
28127
+ try {
28128
+ const last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event));
28129
+ return last ?? await this.endOf(turn);
28130
+ } catch (error62) {
28131
+ this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf3(error62)}`);
28132
+ return await this.endOf(turn);
27717
28133
  }
27718
- return { entry: expand(found[2] ?? "", env), vars };
27719
- }
27720
- return void 0;
27721
- }
27722
- var NO_HERMES = "The hermes command isn't on this machine's PATH, so Hermes's gateway can't be started.";
27723
- var messageOf3 = (error62) => error62 instanceof Error ? error62.message : String(error62);
27724
- function gatewayEnv(env) {
27725
- return Object.fromEntries(Object.entries(env).filter(([name2]) => !name2.startsWith("CHIPS_")));
27726
- }
27727
- var Gateways = class {
27728
- constructor(options) {
27729
- this.options = options;
27730
28134
  }
27731
- options;
27732
- timing = TIMING;
27733
- /** Each gateway restarted, one after another: what went wrong, in a line, or nothing. */
27734
- async restart() {
27735
- const troubles = [];
27736
- for (const gateway of this.options.gateways) {
27737
- let trouble;
27738
- try {
27739
- trouble = await this.restartOne(gateway);
27740
- } catch (error62) {
27741
- trouble = `Restart failed: ${messageOf3(error62)}`;
28135
+ /** The end of a run whose stream was lost: its state read until it is over, as a terminal event. */
28136
+ async endOf(turn) {
28137
+ const by = Date.now() + LOST_STREAM_MS;
28138
+ while (Date.now() < by) {
28139
+ const state2 = await turn.agent.client.run(turn.hermesRun).catch(() => void 0);
28140
+ const status = typeof state2?.["status"] === "string" ? state2["status"] : void 0;
28141
+ if (status !== void 0 && TERMINAL.has(`run.${status}`)) {
28142
+ return { ...state2, event: `run.${status}` };
27742
28143
  }
27743
- if (trouble !== void 0) troubles.push(trouble);
28144
+ await new Promise((resolve7) => setTimeout(resolve7, 1e3));
27744
28145
  }
27745
- return troubles.length === 0 ? void 0 : line(troubles.join(" "));
28146
+ await this.failed(turn, `${turn.agent.config.name}'s run in Hermes stopped answering.`, true);
28147
+ return void 0;
27746
28148
  }
27747
- /** Each local gateway that doesn't answer, started: the bridge is starting. */
27748
- async startIfDown() {
27749
- const { system, log: log2 } = this.options;
27750
- for (const gateway of this.options.gateways) {
27751
- if (!isLocal(gateway.url)) continue;
27752
- const port = portOf(gateway.url);
28149
+ /** The room's session: the one it had, or a new one Hermes makes for it. */
28150
+ async sessionFor({ id: id2, agent, spaceId }) {
28151
+ const kept = this.options.state.sessions[id2];
28152
+ if (kept !== void 0) return kept;
28153
+ const who = id2.slice(0, id2.lastIndexOf("|"));
28154
+ const made = await agent.client.newSession(`Chips space ${spaceId}${who === agent.key ? "" : ` \xB7 ${who}`}`);
28155
+ this.options.state.sessions[id2] = made;
28156
+ this.options.saveState(this.options.state);
28157
+ return made;
28158
+ }
28159
+ /**
28160
+ * A person's words for a run that is going. A steer is said to the Hermes run, or to the next one
28161
+ * of the turn when Hermes won't take it now. A follow-up waits for this one to end. An
28162
+ * interrupt stops it, and the turn goes on with the words.
28163
+ */
28164
+ async steered(turn, text, mode) {
28165
+ if (mode === "interrupt" && turn.hermesRun !== "") {
28166
+ turn.next.unshift(text);
28167
+ turn.interrupted = true;
28168
+ await turn.agent.client.stop(turn.hermesRun).catch(() => void 0);
28169
+ return;
28170
+ }
28171
+ if (mode === "steer" && turn.hermesRun !== "") {
27753
28172
  try {
27754
- if (await system.healthy(gateway.url)) continue;
27755
- let trouble;
27756
- if (this.serviceFor(gateway) !== void 0) {
27757
- log2(`Hermes isn't answering on port ${port}: starting its service, hermes gateway start`);
27758
- trouble = await this.byService(gateway, "start", void 0);
27759
- } else {
27760
- const pid = await system.listener(port);
27761
- if (pid !== void 0) {
27762
- log2(`Hermes isn't answering on port ${port}, but pid ${pid} listens there: no second gateway started`);
27763
- continue;
27764
- }
27765
- log2(`Hermes isn't answering on port ${port}: starting hermes gateway run`);
27766
- trouble = await this.start(gateway, ["gateway", "run"], void 0);
27767
- }
27768
- if (trouble !== void 0) log2(trouble);
28173
+ await turn.agent.client.steer(turn.hermesRun, text);
28174
+ return;
27769
28175
  } catch (error62) {
27770
- log2(`Hermes's gateway on port ${port} couldn't be started: ${messageOf3(error62)}`);
28176
+ this.log(`Hermes didn't take a steer mid-run (${messageOf3(error62)}); saying it next`);
27771
28177
  }
27772
28178
  }
28179
+ turn.next.push(text);
28180
+ }
28181
+ // ------------------------------------------------------------------ what Hermes streams
28182
+ async heard(turn, event) {
28183
+ if (turn.run.over) return;
28184
+ switch (event.event) {
28185
+ case "message.delta":
28186
+ turn.reply += typeof event["delta"] === "string" ? event["delta"] : "";
28187
+ return;
28188
+ case "message.interim":
28189
+ case "reasoning.available":
28190
+ this.sayThought(turn, String(event["text"] ?? ""), event.event === "message.interim" ? "writing" : "thinking");
28191
+ return;
28192
+ case "tool.started":
28193
+ case "subagent.start":
28194
+ await this.started(turn, event);
28195
+ return;
28196
+ case "tool.completed":
28197
+ case "subagent.complete":
28198
+ await this.completed(turn, event);
28199
+ return;
28200
+ case "approval.request":
28201
+ void this.ask(turn, event);
28202
+ return;
28203
+ case "approval.responded":
28204
+ await this.settled(turn, event);
28205
+ return;
28206
+ default:
28207
+ return;
28208
+ }
27773
28209
  }
27774
- /**
27775
- * The service of Hermes's root, for the Main agent's gateway: a gateway at another address is
27776
- * another profile's own, which `hermes gateway status` of the root doesn't speak for.
27777
- */
27778
- serviceFor(gateway) {
27779
- if (gateway !== this.options.gateways[0]) return void 0;
27780
- return serviceOf(this.options.system, this.options.root, this.options.env);
28210
+ sayThought(turn, text, phase) {
28211
+ const now = Date.now();
28212
+ const line2 = lastLine(text);
28213
+ if (line2 === "" || now - turn.saidAt < QUIET_MS) return;
28214
+ turn.saidAt = now;
28215
+ void turn.run.status({ phase, verbPhrase: line2.slice(0, 200) }).catch(() => void 0);
27781
28216
  }
27782
- async restartOne(gateway) {
27783
- const { system, log: log2 } = this.options;
27784
- if (!isLocal(gateway.url)) {
27785
- return `Hermes's gateway at ${new URL(gateway.url).host} isn't on this machine, so it can't be restarted from here.`;
27786
- }
27787
- const port = portOf(gateway.url);
27788
- const pid = await system.listener(port);
27789
- if (this.serviceFor(gateway) !== void 0) {
27790
- log2("restarting Hermes's service: hermes gateway restart");
27791
- return this.byService(gateway, "restart", pid);
27792
- }
27793
- if (pid === void 0) {
27794
- if (await system.healthy(gateway.url)) return `Can't tell what answers on port ${port}, so nothing was restarted.`;
27795
- log2(`nothing listens on port ${port}: starting hermes gateway run`);
27796
- return this.start(gateway, ["gateway", "run"], void 0);
28217
+ async started(turn, event) {
28218
+ const sub = event.event === "subagent.start";
28219
+ const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
28220
+ turn.counted += 1;
28221
+ const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
28222
+ const stepId = callId ?? `${tool}-${turn.counted}`;
28223
+ const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
28224
+ turn.open.set(lane, [...turn.open.get(lane) ?? [], stepId]);
28225
+ const said2 = sub ? event["goal"] : event["preview"];
28226
+ await turn.run.step({
28227
+ stepId,
28228
+ status: "started",
28229
+ title: `${tool}${typeof said2 === "string" && said2 !== "" ? ` \xB7 ${said2}` : ""}`.slice(0, 300),
28230
+ tool,
28231
+ ...KINDS[tool] === void 0 ? {} : { kind: KINDS[tool] }
28232
+ }).catch(() => void 0);
28233
+ }
28234
+ async completed(turn, event) {
28235
+ const sub = event.event === "subagent.complete";
28236
+ const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
28237
+ const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
28238
+ const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
28239
+ const waiting = turn.open.get(lane) ?? [];
28240
+ const stepId = callId ?? waiting.shift() ?? `${tool}-${turn.counted += 1}`;
28241
+ turn.open.set(lane, waiting.filter((each) => each !== stepId));
28242
+ const failed = event["error"] === true || sub && event["status"] === "failed";
28243
+ const approvalId = sub ? void 0 : turn.allowed.shift();
28244
+ if (approvalId !== void 0) {
28245
+ await turn.run.executed(approvalId, failed ? "failed" : "executed").catch(() => void 0);
27797
28246
  }
27798
- const info = await system.describe(pid);
27799
- if (info === void 0) return this.start(gateway, ["gateway", "run"], void 0);
27800
- const args = this.ours(info);
27801
- if (args === void 0) {
27802
- log2(`pid ${pid} on port ${port} isn't this user's hermes gateway run, so it was left alone: ${info.command.slice(0, 160)}`);
27803
- return `Port ${port} is held by something other than hermes gateway run (pid ${pid}), so nothing was restarted.`;
28247
+ const summary = sub ? event["summary"] : resultWords(event["preview"]);
28248
+ const seconds = typeof event["duration"] === "number" ? event["duration"] : void 0;
28249
+ await turn.run.step({
28250
+ stepId,
28251
+ status: failed ? "failed" : "done",
28252
+ tool,
28253
+ ...typeof summary === "string" && summary !== "" ? { summary: summary.slice(0, 2e3) } : {},
28254
+ ...seconds === void 0 ? {} : { durationMs: Math.round(seconds * 1e3) }
28255
+ }).catch(() => void 0);
28256
+ const cost3 = event["cost_usd"];
28257
+ if (sub && typeof cost3 === "number" && cost3 > 0) {
28258
+ const model = typeof event["model"] === "string" ? event["model"] : "subagent";
28259
+ await turn.run.usage({ scope: "step", model, costUsd: cost3 }).catch(() => void 0);
27804
28260
  }
27805
- if (system.which("hermes", this.options.env) === void 0) return NO_HERMES;
27806
- log2(`stopping Hermes's gateway, pid ${pid}`);
27807
- if (!await this.stop(pid)) return `Hermes's gateway (pid ${pid}) didn't stop, so it wasn't started again.`;
27808
- return this.start(gateway, args, info.cwd);
27809
28261
  }
27810
- /** A `hermes gateway run` of this user's: the words to start it again with. */
27811
- ours(info) {
27812
- return info.uid === this.options.system.uid ? gatewayRunArgs(info.command) : void 0;
28262
+ // ------------------------------------------------------------------ what Hermes asks
28263
+ /** An approval Hermes wants before it acts, as a card; the answer is Hermes's choice. */
28264
+ async ask(turn, event) {
28265
+ const requestId = idOf(event);
28266
+ const command = typeof event["command"] === "string" ? event["command"] : void 0;
28267
+ const description = typeof event["description"] === "string" ? event["description"] : void 0;
28268
+ const choices = Array.isArray(event["choices"]) ? event["choices"].map(String) : ["once", "deny"];
28269
+ const approvalId = ids.next("approval");
28270
+ turn.approvals.set(requestId ?? approvalId, { approvalId, answered: false });
28271
+ const asked = turn.run.approval({
28272
+ approvalId,
28273
+ action: {
28274
+ kind: command === void 0 ? "hermes.approval" : "shell.exec",
28275
+ input: { ...command === void 0 ? {} : { command }, ...description === void 0 ? {} : { description } }
28276
+ },
28277
+ question: (description === void 0 ? "Run this command?" : `Allow it to ${lowerFirst(description)}?`).slice(0, 300),
28278
+ lasts: "once",
28279
+ risk: [],
28280
+ ...command === void 0 ? {} : { preview: { format: "markdown", title: "Run a command", body: `\`\`\`sh
28281
+ ${command}
28282
+ \`\`\`` } }
28283
+ });
28284
+ const stopped2 = new Promise((resolve7) => turn.run.on("cancel", () => resolve7(void 0)));
28285
+ const decision = await Promise.race([asked, stopped2]);
28286
+ const pending = turn.approvals.get(requestId ?? approvalId);
28287
+ if (decision === void 0 || pending === void 0 || pending.answered) return;
28288
+ pending.answered = true;
28289
+ if (decision.allowed) turn.allowed.push(approvalId);
28290
+ const generation = event["execution_generation"];
28291
+ await turn.agent.client.approve(turn.hermesRun, {
28292
+ choice: choiceOf(decision, choices, command !== void 0),
28293
+ ...requestId === void 0 ? {} : { request_id: requestId },
28294
+ ...typeof generation === "number" ? { execution_generation: generation } : {}
28295
+ }).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf3(error62)}`));
27813
28296
  }
27814
- /** SIGTERM, then SIGKILL once it has had a while, and only while it is still that gateway. */
27815
- async stop(pid) {
27816
- const { system, log: log2 } = this.options;
27817
- system.signal(pid, "SIGTERM");
27818
- if (await this.gone(pid, this.timing.stopMs)) return true;
27819
- const still = await system.describe(pid);
27820
- if (still === void 0) return true;
27821
- if (this.ours(still) === void 0) return false;
27822
- log2(`Hermes's gateway, pid ${pid}, is still running ${this.timing.stopMs / 1e3} s after SIGTERM: SIGKILL`);
27823
- system.signal(pid, "SIGKILL");
27824
- return this.gone(pid, this.timing.killMs);
28297
+ /** An approval answered in Hermes's own tools: settled in Chips, once, so the card cools (ADR 0079). */
28298
+ async settled(turn, event) {
28299
+ const requestId = idOf(event);
28300
+ const pending = requestId === void 0 ? void 0 : turn.approvals.get(requestId);
28301
+ if (pending === void 0 || pending.answered) return;
28302
+ pending.answered = true;
28303
+ const denied = event["choice"] === "deny";
28304
+ await this.options.chips.send(
28305
+ "approval.resolved",
28306
+ { run_id: turn.run.id },
28307
+ {
28308
+ approval_id: pending.approvalId,
28309
+ decision: denied ? "denied" : "approved",
28310
+ scope: "once",
28311
+ reason: "Answered in Hermes."
28312
+ },
28313
+ `hermes-approval:${turn.hermesRun}:${requestId}:settled`
28314
+ ).catch(() => void 0);
27825
28315
  }
27826
- async gone(pid, withinMs) {
27827
- const { system } = this.options;
27828
- const by = system.now() + withinMs;
27829
- while (system.alive(pid)) {
27830
- if (system.now() >= by) return false;
27831
- await system.sleep(this.timing.pollMs);
28316
+ // ------------------------------------------------------------------ the end
28317
+ /** The turn is over: its words in the room, its tokens, and the run ended. */
28318
+ async finish(turn, last) {
28319
+ const { run: run2, agent } = turn;
28320
+ if (run2.over) return;
28321
+ await this.closeSteps(turn, "done");
28322
+ for (const approvalId of turn.allowed.splice(0)) {
28323
+ await run2.executed(approvalId, "executed", "Hermes didn't say how it went.").catch(() => void 0);
27832
28324
  }
27833
- return true;
27834
- }
27835
- /** Hermes's own `hermes gateway restart|start`, then the gateway answering, from a new process. */
27836
- async byService(gateway, verb, before) {
27837
- const { system, env } = this.options;
27838
- const hermes = system.which("hermes", env);
27839
- if (hermes === void 0) return NO_HERMES;
27840
- const scope = this.serviceFor(gateway)?.system === true ? ["--system"] : [];
27841
- const ran = await system.run(hermes, ["gateway", verb, ...scope], gatewayEnv(env), this.timing.serviceMs);
27842
- if (ran.code !== 0) return line(`hermes gateway ${verb} failed: ${whyFrom(ran.output, ran.code)}`);
27843
- const by = system.now() + this.timing.serviceMs;
27844
- for (; ; ) {
27845
- const old = before !== void 0 && system.alive(before);
27846
- if (!old && await system.healthy(gateway.url)) return void 0;
27847
- if (system.now() >= by) {
27848
- if (await system.healthy(gateway.url)) return void 0;
27849
- return `Hermes's service ${verb === "start" ? "started" : "restarted"}, but nothing answers on port ${portOf(gateway.url)}.`;
27850
- }
27851
- await system.sleep(this.timing.pollMs);
28325
+ await this.counted(turn, last);
28326
+ const output2 = typeof last["output"] === "string" ? last["output"].trim() : "";
28327
+ const answer = output2 !== "" ? output2 : turn.reply.trim();
28328
+ const { change, note } = await this.sendProject(turn, answer);
28329
+ const { folder: folder2 } = turn;
28330
+ const { chips: chips2 } = this.options;
28331
+ const words4 = [answer, note ?? ""].filter((part) => part !== "").join("\n\n");
28332
+ const post = await sendAnswer(words4, {
28333
+ folders: folder2 === void 0 ? [] : [folder2],
28334
+ upload: (input2) => chips2.upload(input2)
28335
+ });
28336
+ const reply = post.text;
28337
+ if (reply !== "" || post.uploadIds.length > 0) {
28338
+ await turn.speaker.post({ spaceId: run2.spaceId, runId: run2.id, text: reply, uploadIds: post.uploadIds, change });
27852
28339
  }
28340
+ const said2 = post.summary;
28341
+ const summary = said2 === "" ? "Done." : said2.length <= 2e3 ? said2 : `${said2.slice(0, 1990)}\u2026`;
28342
+ await run2.complete({ summary });
28343
+ this.log(`${agent.config.name} answered in ${run2.spaceId}`);
28344
+ this.missingReadings();
27853
28345
  }
27854
28346
  /**
27855
- * `hermes <args>` (`gateway run`), on its own, in `cwd` (else the home), then through Homebrew's
27856
- * expat when it died of macOS's.
28347
+ * A turn ended and Hermes sent no reading at all since the bridge started: its gateway most
28348
+ * likely started before Chips's webhook was in its config. Said once, a little after, since a
28349
+ * reading comes a moment after its model call.
27857
28350
  */
27858
- async start(gateway, args, cwd) {
27859
- const { system, env, log: log2 } = this.options;
27860
- const port = portOf(gateway.url);
27861
- const hermes = system.which("hermes", env);
27862
- if (hermes === void 0) return NO_HERMES;
27863
- const folder2 = cwd ?? system.home;
27864
- const plain = gatewayEnv(env);
27865
- let outcome = await this.launch(gateway, hermes, args, plain, folder2, `hermes ${args.join(" ")}`);
27866
- if (outcome.state === "ended" && outcome.early && outcome.output.includes(EXPAT_SYMBOL) && system.exists(EXPAT)) {
27867
- const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
27868
- if (wrapper !== void 0) {
27869
- const expat = dirname5(EXPAT);
27870
- log2(
27871
- `hermes gateway run stopped at once: Homebrew's Python can't load macOS's expat (${EXPAT_SYMBOL}). Starting ${wrapper.entry} directly with DYLD_LIBRARY_PATH=${expat}.`
27872
- );
27873
- const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
27874
- outcome = await this.launch(
27875
- gateway,
27876
- wrapper.entry,
27877
- args,
27878
- { ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries },
27879
- folder2,
27880
- `${wrapper.entry} ${args.join(" ")} with DYLD_LIBRARY_PATH=${expat}`
27881
- );
27882
- }
27883
- }
27884
- if (outcome.state === "up") {
27885
- log2(`Hermes's gateway${outcome.pid === void 0 ? "" : `, pid ${outcome.pid},`} answers on port ${port}. It logs to ${this.options.logFile}.`);
27886
- return void 0;
27887
- }
27888
- if (outcome.state === "silent") return `Hermes's gateway started, but nothing answers on port ${port}.`;
27889
- return line(`Hermes's gateway stopped as it started: ${whyFrom(outcome.output, outcome.code)}`);
27890
- }
27891
- /** A program started, and watched until it answers, ends, or has had its time. */
27892
- async launch(gateway, command, args, env, cwd, what) {
27893
- const { system, logFile } = this.options;
27894
- system.note(logFile, `--- ${new Date(system.now()).toISOString()} chips-hermes started ${what}`);
27895
- const from = system.size(logFile);
27896
- const startedAt = system.now();
27897
- const child = system.launch({ command, args, env, cwd, log: logFile });
27898
- let ended;
27899
- void child.exited.then(({ code }) => {
27900
- ended = { code, at: system.now() };
27901
- });
27902
- for (; ; ) {
27903
- await Promise.resolve();
27904
- if (ended !== void 0) {
27905
- return { state: "ended", early: ended.at - startedAt <= this.timing.earlyMs, output: system.since(logFile, from), code: ended.code };
28351
+ missingReadings() {
28352
+ if (this.fills === void 0 || this.fillsPort === void 0 || this.fills.anyHeard || this.toldNoReadings) return;
28353
+ this.toldNoReadings = true;
28354
+ setTimeout(() => {
28355
+ if (this.fills?.anyHeard === false) {
28356
+ this.log(
28357
+ "context window: Hermes sent no reading after a turn. Start hermes gateway again once, so it takes Chips's webhook from its config."
28358
+ );
27906
28359
  }
27907
- if (await system.healthy(gateway.url)) return { state: "up", pid: child.pid };
27908
- if (system.now() - startedAt >= this.timing.upMs) return { state: "silent" };
27909
- await system.sleep(this.timing.pollMs);
28360
+ }, 1e4).unref();
28361
+ }
28362
+ /** A run that failed, was stopped or was cut short in Hermes. */
28363
+ async ended(turn, last) {
28364
+ await this.closeSteps(turn, "failed");
28365
+ await this.counted(turn, last);
28366
+ if (turn.cancelled || turn.run.over) return;
28367
+ const name2 = turn.agent.config.name;
28368
+ if (last.event === "run.cancelled") await this.failed(turn, `${name2}'s run was stopped in Hermes.`, false);
28369
+ else if (last.event === "run.interrupted") await this.failed(turn, `Hermes restarted during ${name2}'s run.`, true);
28370
+ else {
28371
+ const why = typeof last["error"] === "string" && last["error"] !== "" ? last["error"] : "it failed in Hermes";
28372
+ await this.failed(turn, `${name2}: ${why}`, true);
27910
28373
  }
27911
28374
  }
27912
- };
27913
- function restarter(deps) {
27914
- let running;
27915
- return (ask2) => {
27916
- if (running !== void 0) {
27917
- deps.log(`restart asked by ${ask2.askedBy} while one runs: it is the same restart`);
27918
- return running;
28375
+ /**
28376
+ * The tokens Hermes counted, as a step's share, so they add up over the turn's runs. Hermes's
28377
+ * `input_tokens` is already every token in, its cache's reads and writes among them (its
28378
+ * `CanonicalUsage.prompt_tokens`), which is what Chips counts (Chips ADR 0214); the cache's two
28379
+ * counts go with it, and are left out where Hermes doesn't say them.
28380
+ */
28381
+ async counted(turn, last) {
28382
+ const usage2 = last["usage"] ?? {};
28383
+ const count = (key) => typeof usage2[key] === "number" ? usage2[key] : void 0;
28384
+ const input2 = count("input_tokens") ?? 0;
28385
+ const output2 = count("output_tokens") ?? 0;
28386
+ if (input2 + output2 === 0) return;
28387
+ const runtime = last["runtime"] ?? {};
28388
+ const model = typeof runtime["model"] === "string" ? runtime["model"] : this.runModel(turn).model ?? "hermes";
28389
+ await turn.run.usage({
28390
+ scope: "step",
28391
+ model,
28392
+ inputTokens: input2,
28393
+ cacheReadTokens: count("cache_read_tokens"),
28394
+ cacheWriteTokens: count("cache_write_tokens"),
28395
+ outputTokens: output2
28396
+ }).catch(() => void 0);
28397
+ }
28398
+ async closeSteps(turn, status) {
28399
+ for (const [, ids2] of turn.open) {
28400
+ for (const stepId of ids2) await turn.run.step({ stepId, status }).catch(() => void 0);
27919
28401
  }
27920
- running = (async () => {
27921
- deps.log(`restart asked by ${ask2.askedBy}: restarting Hermes's gateway`);
27922
- deps.hold(true);
27923
- let trouble;
27924
- try {
27925
- trouble = await deps.restart();
27926
- } catch (error62) {
27927
- trouble = line(`Restart failed: ${error62 instanceof Error ? error62.message : String(error62)}`);
27928
- }
27929
- deps.log(trouble === void 0 ? "restarted, and Hermes answers" : `restart: ${trouble}`);
27930
- try {
27931
- await deps.say(trouble);
27932
- } catch (error62) {
27933
- deps.log(`could not tell Chips how the restart went: ${String(error62)}`);
27934
- } finally {
27935
- deps.hold(false);
27936
- }
27937
- })().finally(() => {
27938
- running = void 0;
27939
- });
27940
- return running;
27941
- };
28402
+ turn.open.clear();
28403
+ }
28404
+ async failed(turn, error62, retryable) {
28405
+ if (!turn.run.over) await turn.run.fail({ error: error62, retryable }).catch(() => void 0);
28406
+ if (retryable) void this.sayHowItIs();
28407
+ }
28408
+ };
28409
+ function choiceOf(decision, choices, command = false) {
28410
+ if (!decision.allowed) return "deny";
28411
+ if (command) return "once";
28412
+ if (decision.scope === "always" && choices.includes("always")) return "always";
28413
+ if (decision.scope !== "once" && choices.includes("session")) return "session";
28414
+ return "once";
27942
28415
  }
27943
- function gatewayLog(configFolder, platform = process.platform) {
27944
- return platform === "darwin" ? join9(homedir5(), "Library", "Logs", "chips-hermes-gateway.log") : join9(configFolder, "gateway.log");
28416
+ function idOf(event) {
28417
+ const id2 = event["request_id"] ?? event["prompt_id"];
28418
+ return typeof id2 === "string" && id2 !== "" ? id2 : void 0;
27945
28419
  }
27946
- function runToEnd(command, args, options) {
27947
- return new Promise((done) => {
27948
- const child = spawn2(command, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"] });
27949
- let output2 = "";
27950
- const keep = (data) => {
27951
- output2 = (output2 + String(data)).slice(-8e3);
27952
- };
27953
- child.stdout.on("data", keep);
27954
- child.stderr.on("data", keep);
27955
- const limit = setTimeout(() => child.kill("SIGTERM"), options.timeoutMs);
27956
- child.on("error", (error62) => {
27957
- clearTimeout(limit);
27958
- done({ code: null, output: error62.message });
27959
- });
27960
- child.on("close", (code) => {
27961
- clearTimeout(limit);
27962
- done({ code, output: output2 });
27963
- });
27964
- });
28420
+ function messageOf3(error62) {
28421
+ if (error62 instanceof HermesError) return `${error62.message} (${error62.status})`;
28422
+ return error62 instanceof Error ? error62.message : String(error62);
27965
28423
  }
27966
- function thisMachine() {
27967
- return {
27968
- platform: process.platform,
27969
- uid: process.getuid?.() ?? -1,
27970
- home: homedir5(),
27971
- exists: existsSync8,
27972
- read(path) {
27973
- try {
27974
- return readFileSync8(path, "utf8");
27975
- } catch {
27976
- return void 0;
27977
- }
27978
- },
27979
- which(command, env) {
27980
- for (const folder2 of (env["PATH"] ?? "").split(delimiter2)) {
27981
- if (folder2 === "") continue;
27982
- const path = join9(folder2, command);
27983
- try {
27984
- accessSync2(path, constants2.X_OK);
27985
- return path;
27986
- } catch {
27987
- }
27988
- }
27989
- return void 0;
27990
- },
27991
- async listener(port) {
27992
- const { output: output2 } = await runToEnd("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], { timeoutMs: 5e3 });
27993
- const pid = Number(/^p(\d+)$/m.exec(output2)?.[1]);
27994
- return Number.isInteger(pid) && pid > 0 ? pid : void 0;
27995
- },
27996
- async describe(pid) {
27997
- const { code, output: output2 } = await runToEnd("ps", ["-ww", "-o", "uid=", "-o", "command=", "-p", String(pid)], { timeoutMs: 5e3 });
27998
- const found = /^\s*(\d+)\s+(.*)$/m.exec(output2);
27999
- if (code !== 0 || found === null) return void 0;
28000
- return { uid: Number(found[1]), command: found[2] ?? "", cwd: processFolder(pid) };
28001
- },
28002
- alive(pid) {
28003
- try {
28004
- process.kill(pid, 0);
28005
- return true;
28006
- } catch (error62) {
28007
- return error62.code === "EPERM";
28008
- }
28009
- },
28010
- signal(pid, signal) {
28011
- try {
28012
- process.kill(pid, signal);
28013
- } catch {
28014
- }
28015
- },
28016
- run: (command, args, env, timeoutMs) => runToEnd(command, args, { env, timeoutMs }),
28017
- launch({ command, args, env, cwd, log: log2 }) {
28018
- const out = openSync2(log2, "a");
28019
- try {
28020
- const child = spawn2(command, args, { cwd, env, detached: true, stdio: ["ignore", out, out] });
28021
- const exited = new Promise((done) => {
28022
- child.once("exit", (code, signal) => done({ code, signal }));
28023
- child.once("error", (error62) => {
28024
- appendFileSync2(log2, `${error62.message}
28025
- `);
28026
- done({ code: null, signal: null });
28027
- });
28028
- });
28029
- child.unref();
28030
- return { pid: child.pid, exited };
28031
- } finally {
28032
- closeSync2(out);
28033
- }
28034
- },
28035
- note(log2, words4) {
28036
- appendFileSync2(log2, `${words4}
28037
- `);
28038
- },
28039
- size(log2) {
28040
- try {
28041
- return statSync4(log2).size;
28042
- } catch {
28043
- return 0;
28044
- }
28045
- },
28046
- since(log2, from) {
28047
- try {
28048
- const to = statSync4(log2).size;
28049
- const start = Math.max(from, to - 64e3);
28050
- const buffer = Buffer.alloc(Math.max(0, to - start));
28051
- const fd = openSync2(log2, "r");
28052
- try {
28053
- readSync(fd, buffer, 0, buffer.length, start);
28054
- } finally {
28055
- closeSync2(fd);
28056
- }
28057
- return buffer.toString("utf8");
28058
- } catch {
28059
- return "";
28060
- }
28061
- },
28062
- healthy: (url2) => new HermesClient({ url: url2, key: "" }, (input2, init) => fetch(input2, { ...init, signal: AbortSignal.timeout(5e3) })).healthy(),
28063
- sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
28064
- now: () => Date.now()
28065
- };
28424
+ function titleFrom(text) {
28425
+ const line2 = text.trim().split("\n")[0] ?? "";
28426
+ if (line2.length <= 48) return line2;
28427
+ const cut2 = line2.slice(0, 48);
28428
+ return `${cut2.slice(0, Math.max(cut2.lastIndexOf(" "), 24))}\u2026`;
28429
+ }
28430
+ function forkName(original) {
28431
+ return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
28432
+ }
28433
+ function goalOf(message, name2) {
28434
+ const first = message.text.split("\n").find((line2) => line2.trim() !== "")?.trim() ?? "";
28435
+ const named = first.toLowerCase().startsWith(`@${name2.toLowerCase()}`);
28436
+ const goal = named ? first.slice(name2.length + 1).trim() : first.replace(/^@\S+\s*/, "");
28437
+ return (goal === "" ? `Answer ${message.from.display_name}` : goal).slice(0, 300);
28438
+ }
28439
+ var REPLY_SHOWN = 1500;
28440
+ var GOAL_SHOWN = 120;
28441
+ function lookAgain(who, reply, note) {
28442
+ const said2 = reply?.trim() ?? "";
28443
+ return [
28444
+ `${who} says your reply in this space wasn't right.`,
28445
+ ...said2 === "" ? [] : [`Your reply:
28446
+ \xAB${cut(said2, REPLY_SHOWN)}\xBB`],
28447
+ note === void 0 || note === "" ? "They didn't say what. Read your reply again and find what was wrong." : `What wasn't right: \xAB${note}\xBB`,
28448
+ "Look at it again and answer properly in its place. If it is not clear what was wrong, ask them in one short line. Say plainly what you changed."
28449
+ ].join("\n\n");
28450
+ }
28451
+ function lookingAgain(note) {
28452
+ const first = note?.split("\n").find((line2) => line2.trim() !== "")?.trim();
28453
+ return first === void 0 ? "Looking again at its reply" : cut(`Looking again: ${first}`, GOAL_SHOWN);
28454
+ }
28455
+ function cut(text, most) {
28456
+ return text.length <= most ? text : `${text.slice(0, most - 1)}\u2026`;
28457
+ }
28458
+ function resultWords(preview2) {
28459
+ if (typeof preview2 !== "string" || !preview2.trimStart().startsWith("{")) return preview2;
28460
+ let result;
28461
+ try {
28462
+ result = JSON.parse(preview2);
28463
+ } catch {
28464
+ return preview2;
28465
+ }
28466
+ if (typeof result !== "object" || result === null || !("output" in result)) return preview2;
28467
+ const { output: output2, error: error62 } = result;
28468
+ const said2 = [output2, error62].find((each) => typeof each === "string" && each.trim() !== "");
28469
+ return typeof said2 === "string" ? said2.trim() : "";
28470
+ }
28471
+ function lastLine(text) {
28472
+ const lines2 = text.split(/(?<=[.!?])\s+|\n+/).map((line2) => line2.trim()).filter((line2) => line2 !== "");
28473
+ return lines2.at(-1) ?? "";
28474
+ }
28475
+ function lowerFirst(text) {
28476
+ return text.length === 0 ? text : `${text[0]?.toLowerCase() ?? ""}${text.slice(1)}`;
28066
28477
  }
28067
28478
 
28068
28479
  // src/keep.ts
28069
28480
  import { homedir as homedir6 } from "node:os";
28070
- import { basename as basename5, join as join10, resolve as resolve4 } from "node:path";
28481
+ import { basename as basename5, join as join11, resolve as resolve4 } from "node:path";
28071
28482
  import { fileURLToPath as fileURLToPath2 } from "node:url";
28072
28483
  var flags = process.argv.slice(2);
28073
28484
  var words3 = flags.filter((word) => !word.startsWith("--"));
28074
- var whole = (word) => resolve4(word.startsWith("~/") ? join10(homedir6(), word.slice(2)) : word);
28485
+ var whole = (word) => resolve4(word.startsWith("~/") ? join11(homedir6(), word.slice(2)) : word);
28075
28486
  var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
28076
28487
  var self = fileURLToPath2(import.meta.url);
28077
28488
  var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
@@ -28117,9 +28528,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
28117
28528
  }
28118
28529
 
28119
28530
  // src/setup.ts
28120
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
28531
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
28121
28532
  import { homedir as homedir7 } from "node:os";
28122
- import { join as join11, resolve as resolve5 } from "node:path";
28533
+ import { join as join12, resolve as resolve5 } from "node:path";
28123
28534
  var SetupError = class extends Error {
28124
28535
  constructor(message, words4 = {}) {
28125
28536
  super(message);
@@ -28144,16 +28555,16 @@ function readyHooks(config3, options) {
28144
28555
  const lines2 = [];
28145
28556
  let restart2 = false;
28146
28557
  const done = /* @__PURE__ */ new Set();
28147
- for (const agent of config3.agents) {
28148
- const profile = profileName(agent.address.profile);
28558
+ for (const named of [...config3.agents.map((agent) => agent.address.profile), ...options.made ?? []]) {
28559
+ const profile = profileName(named);
28149
28560
  if (done.has(profile)) continue;
28150
28561
  done.add(profile);
28151
- const home2 = profileHome(options.root, agent.address.profile);
28562
+ const home2 = profileHome(options.root, named);
28152
28563
  let outcome;
28153
28564
  try {
28154
28565
  outcome = addHook(home2, options.port, profile, options.secret);
28155
28566
  } catch (error62) {
28156
- lines2.push(`Chips's webhook wasn't added to ${shown(join11(home2, "config.yaml"))}: ${error62 instanceof Error ? error62.message : String(error62)}`);
28567
+ lines2.push(`Chips's webhook wasn't added to ${shown(join12(home2, "config.yaml"))}: ${error62 instanceof Error ? error62.message : String(error62)}`);
28157
28568
  continue;
28158
28569
  }
28159
28570
  if (outcome.state === "added") {
@@ -28179,8 +28590,8 @@ function hermesEnvPath(env = process.env) {
28179
28590
  }
28180
28591
  function fromHermes(name2, env) {
28181
28592
  const path = hermesEnvPath(env);
28182
- if (!existsSync9(path)) return void 0;
28183
- for (const line2 of readFileSync9(path, "utf8").split("\n")) {
28593
+ if (!existsSync10(path)) return void 0;
28594
+ for (const line2 of readFileSync10(path, "utf8").split("\n")) {
28184
28595
  const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line2);
28185
28596
  if (match?.[1] === name2) return match[2]?.replace(/^(["'])(.*)\1$/, "$2");
28186
28597
  }
@@ -28191,7 +28602,7 @@ function folderOf(said2) {
28191
28602
  return resolve5(said2 === "~" || said2.startsWith("~/") ? homedir7() + said2.slice(1) : said2);
28192
28603
  }
28193
28604
  function readConfig(path, env = process.env) {
28194
- const raw = existsSync9(path) ? JSON.parse(readFileSync9(path, "utf8")) : PLAIN;
28605
+ const raw = existsSync10(path) ? JSON.parse(readFileSync10(path, "utf8")) : PLAIN;
28195
28606
  const url2 = typeof raw.url === "string" ? raw.url : "http://127.0.0.1:8642";
28196
28607
  const folder2 = folderOf(raw.folder);
28197
28608
  if (!Array.isArray(raw.agents) || raw.agents.length === 0) {
@@ -28262,14 +28673,14 @@ async function checkHermes(config3, clientFor = (agent) => new HermesClient(agen
28262
28673
  }
28263
28674
 
28264
28675
  // src/main.ts
28265
- var home = (path) => path.startsWith("~/") ? join12(homedir8(), path.slice(2)) : path;
28676
+ var home = (path) => path.startsWith("~/") ? join13(homedir8(), path.slice(2)) : path;
28266
28677
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
28267
28678
  var LABEL = "chat.chips.hermes";
28268
28679
  stopIfAsked(LABEL);
28269
28680
  var configPath = resolve6(home(words3[0] ?? "~/.chips-hermes/config.json"));
28270
28681
  var folder = dirname6(configPath);
28271
- var statePath = join12(folder, "sessions.json");
28272
- var state = existsSync10(statePath) ? JSON.parse(readFileSync10(statePath, "utf8")) : { sessions: {} };
28682
+ var statePath = join13(folder, "sessions.json");
28683
+ var state = existsSync11(statePath) ? JSON.parse(readFileSync11(statePath, "utf8")) : { sessions: {} };
28273
28684
  var saveState = (next) => {
28274
28685
  mkdirSync6(folder, { recursive: true, mode: 448 });
28275
28686
  writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
@@ -28294,12 +28705,13 @@ try {
28294
28705
  reportStep("checking", { detail: "Looking for its API server" });
28295
28706
  await checkHermes(config2);
28296
28707
  }
28297
- fills.secret ??= randomBytes2(32).toString("hex");
28708
+ fills.secret ??= randomBytes3(32).toString("hex");
28298
28709
  fills.port = config2.hookPort ?? fills.port ?? hookPortIn(profileHome(root, config2.agents[0]?.address.profile)) ?? await freePort();
28299
28710
  saveState(state);
28300
28711
  const first = config2.agents[0];
28301
28712
  const gatewayUp = link !== void 0 || first !== void 0 && await new HermesClient(first.address).healthy();
28302
- const hooks = readyHooks(config2, { root, port: fills.port, secret: fills.secret, gatewayUp });
28713
+ const made = Object.values(state.made ?? {});
28714
+ const hooks = readyHooks(config2, { root, port: fills.port, secret: fills.secret, gatewayUp, made });
28303
28715
  for (const line2 of hooks.lines) log(line2);
28304
28716
  if (link !== void 0) reportStep("checked", { detail: checkedWords(config2, hooks.restart) });
28305
28717
  } catch (error62) {
@@ -28348,7 +28760,7 @@ var chips = await connect({
28348
28760
  url: joined.url,
28349
28761
  token: joined.token,
28350
28762
  client: { name: "chips-hermes", version: "1.0.0" },
28351
- store: fileStore(join12(folder, "chips-state.json")),
28763
+ store: fileStore(join13(folder, "chips-state.json")),
28352
28764
  onRestart: restart,
28353
28765
  settings: settingsFor(() => bridge)
28354
28766
  });
@@ -28364,7 +28776,10 @@ bridge = new Bridge({
28364
28776
  saveState,
28365
28777
  log,
28366
28778
  hermesRoot: root,
28367
- fills: { port: fills.port, secret: fills.secret }
28779
+ fills: { port: fills.port, secret: fills.secret },
28780
+ // Each agent made in Chips is a Hermes profile of its own (Chips ADR 0241).
28781
+ profiles: hermesProfiles({ root, env: process.env, system: thisMachine(), log: (line2) => log(`profiles: ${line2}`) }),
28782
+ restartGateway: () => restart({ askedBy: "chips-hermes, for a new agent's profile", askedAt: (/* @__PURE__ */ new Date()).toISOString() })
28368
28783
  });
28369
28784
  await bridge.start();
28370
28785
  log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through Hermes`);