@chipschat/hermes 1.0.7 → 1.0.10

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 readFileSync12, 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 = {};
@@ -19945,8 +19945,9 @@ var policyRule = external_exports.object({
19945
19945
  external: external_exports.boolean().optional(),
19946
19946
  irreversible: external_exports.boolean().optional(),
19947
19947
  money_usd_over: external_exports.number().nonnegative().optional(),
19948
- risk: external_exports.array(riskClass).max(8).optional()
19949
- }).optional().describe("Extra conditions on the action's flags."),
19948
+ risk: external_exports.array(riskClass).max(8).optional(),
19949
+ starts_with: external_exports.array(external_exports.string().min(1).max(400)).min(1).max(8).optional().describe("For an action that carries a command (ADR 0239): the words a part of it starts with. A command goes ahead on such rules only when every part starts with one that allows it.")
19950
+ }).optional().describe("Extra conditions on the action's flags, and for a command, how its parts start."),
19950
19951
  decision: external_exports.enum(["allow", "ask", "deny"]).describe("What to do when the rule matches."),
19951
19952
  approvers: external_exports.array(approverRef).max(16).optional().describe("Who may answer, when the decision is `ask`."),
19952
19953
  reason: external_exports.string().max(400).optional().describe("Shown to the harness, and recorded in the audit log."),
@@ -20465,9 +20466,17 @@ var roomIdle = external_exports.object({
20465
20466
  var roomNote = external_exports.object({
20466
20467
  members: external_exports.array(roomMember).max(500).describe("The room's people, then its agents, each by name, anybody removed left out."),
20467
20468
  notes: external_exports.record(external_exports.string().max(120), external_exports.string().max(64e3)).describe("The note for each who takes the turn (`for`), by their id: who is here, with handles, and that only those @-mentioned act. A harness puts it at the top of the turn."),
20469
+ rules: external_exports.record(external_exports.string().max(120), external_exports.string().max(4e3)).optional().describe("How the space works, for each who takes the turn, by their id: the words its note ends with (ADR 0250), that everyone here reads what it writes and, beside other agents, what an @ does and when to answer NO_REPLY. They stay the same while the space's agents do, so a harness whose session keeps its earlier turns may say them once, and the note without them after."),
20468
20470
  working: external_exports.array(roomWork).max(500).optional().describe("The room's agents with a turn going as Chips sends this one (ADR 0104), the takers' own included. A turn nothing has been heard from for ten minutes is not counted."),
20469
20471
  idle: external_exports.array(roomIdle).max(500).optional().describe("The room's other agents that have had a turn here: how long since the last one ended (ADR 0104).")
20470
20472
  });
20473
+ function noteWithoutRules(room, taker) {
20474
+ const note = room?.notes[taker];
20475
+ const rules = room?.rules?.[taker];
20476
+ if (note === void 0 || rules === void 0 || !note.endsWith(rules))
20477
+ return note;
20478
+ return note.slice(0, note.length - rules.length).trimEnd();
20479
+ }
20471
20480
  var MONTHS = [
20472
20481
  "January",
20473
20482
  "February",
@@ -20487,8 +20496,8 @@ var MONTHS = [
20487
20496
  var CONTEXT_CEILING = { lines: 200, chars: 32e3 };
20488
20497
  var CONTEXT_HEADING = "Said in this space since you last spoke:";
20489
20498
  var contextBudget = external_exports.object({
20490
- lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed, 20 when absent. Chips hands over 200 at most, and none when this is 0."),
20491
- chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line, 4,000 when absent. Chips hands over 32,000 at most.")
20499
+ lines: external_exports.int().nonnegative().optional().describe("The most lines to be handed, 40 when absent. Chips hands over 200 at most, and none when this is 0."),
20500
+ chars: external_exports.int().nonnegative().optional().describe("The most characters those lines may run to, as `Name: words` one per line, 16,000 when absent. Chips hands over 32,000 at most.")
20492
20501
  }).describe("How much of the room a harness is handed with a message for it (ADR 0088). Its own to say: a budget promises nothing to anybody else.");
20493
20502
  var contextLine = external_exports.object({
20494
20503
  seq: external_exports.int().positive().describe("Where in the log it happened."),
@@ -20868,7 +20877,7 @@ var messageNew = defineEvent({
20868
20877
  summary: "Someone said something, with any attachments as signed URLs the harness can fetch.",
20869
20878
  requiresRun: false,
20870
20879
  payload: deliveredMessage.extend({
20871
- context: roomContext.optional().describe("What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its `hello`: 20 lines and 4,000 characters when it gave none, and none when it asked for 0. When older lines were left out, the taker's room note says so."),
20880
+ context: roomContext.optional().describe("What was said in the room since this harness last spoke there, or since it joined when it has not, up to this message and without it (ADR 0088). From when the turn that said its last words began, so what was said while it worked is here, and its own words are not (ADR 0105). Worked out from the log, so a replay hands over the same lines. As many as the budget the harness gave in its `hello`: 40 lines and 16,000 characters when it gave none, and none when it asked for 0. When older lines were left out, the taker's room note says so."),
20872
20881
  room: roomNote.optional().describe("Who is in the room, with the handle that reaches each, and a note for each who takes the turn saying so and what an @ does (ADR 0092). A harness puts `notes[<taker>]` at the top of that turn. Absent on a message in no room."),
20873
20882
  exchange: deliveredExchange.optional().describe("How the agents' talk in the room stands, once it is worth a word (ADR 0097): from eight messages between agents since a person last acted or an agent last made something new. A harness puts `note` at the top of the turn, as Chips's, ahead of the room's note. Absent below that, and worked out from the log, so a replay hands over the same facts."),
20874
20883
  handed: external_exports.literal(true).optional().describe("Chips held this message when it was said and hands it over now a person has acted (ADR 0097): the newest of an agent's held messages, once; or, with `lead`, hands it to the room's lead. Its seq and id are the hand-over's, its `message_id` the message's own."),
@@ -20918,6 +20927,7 @@ var approvalResolved = defineEvent({
20918
20927
  approval_id: id("approval"),
20919
20928
  decision: external_exports.enum(["approved", "edited", "denied", "expired"]).describe("`expired` means nobody answered and the fallback applied."),
20920
20929
  scope: external_exports.enum(["once", "session", "always"]).default("once").describe("How far the answer reaches. `always` becomes a policy rule."),
20930
+ everywhere: external_exports.boolean().optional().describe("With `always`: the rule is the harness's in every space, not only this one (ADR 0239). An owner or an admin only."),
20921
20931
  updated_input: external_exports.unknown().optional().describe("The edited action input, when the decision is `edited`."),
20922
20932
  reason: external_exports.string().max(4e3).optional().describe("Why. The harness gets to read this."),
20923
20933
  resolved_by: external_exports.string().min(1).max(120).describe("The participant who answered, or `system` on expiry \u2014 a harness when it settles its own ask, answered in its own tools. Filled in by Chips from the credential that sent it; a value the sender writes is replaced."),
@@ -21182,6 +21192,10 @@ function namesInWords(names) {
21182
21192
  return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
21183
21193
  }
21184
21194
  function renderProjectNote(facts, options) {
21195
+ return `${renderProjectFacts(facts)}
21196
+ ${renderProjectAsks(facts, options)}`;
21197
+ }
21198
+ function renderProjectFacts(facts) {
21185
21199
  const files = `${facts.files} file${facts.files === 1 ? "" : "s"}`;
21186
21200
  const lines2 = [
21187
21201
  `The space's project is a git repository. Your copy is ${facts.folder} (${files}), ${facts.trouble === void 0 ? "brought up to date just now" : "not brought up to date"}.`
@@ -21199,9 +21213,9 @@ function renderProjectNote(facts, options) {
21199
21213
  for (const key of facts.keys) {
21200
21214
  lines2.push(`${key.path} holds what looks like ${key.what}, so your last change wasn't sent: take it out and read it from outside the project.`);
21201
21215
  }
21202
- return [...lines2, ...asks(facts, options)].join("\n");
21216
+ return lines2.join("\n");
21203
21217
  }
21204
- function asks(facts, options) {
21218
+ function renderProjectAsks(facts, options) {
21205
21219
  const lines2 = [
21206
21220
  "Work on the project in that folder and leave git to Chips: when your turn ends, what you changed is committed under your name and sent to everyone here. Keep scratch work outside it.",
21207
21221
  // Sent with the answer too, a project's file became a room file of its own, with versions of
@@ -21222,7 +21236,7 @@ function asks(facts, options) {
21222
21236
  if (facts.pages === true) {
21223
21237
  lines2.push("A page opens in Chips as one document: name every file it needs in its tags and imports (<link>, <script src>, <img src>, import), since it can't fetch files while it runs.");
21224
21238
  }
21225
- return lines2;
21239
+ return lines2.join("\n");
21226
21240
  }
21227
21241
 
21228
21242
  // node_modules/@chipschat/protocol/dist/src/events/harness-to-chips.js
@@ -21874,7 +21888,7 @@ var membershipRemoved = defineEvent({
21874
21888
  payload: external_exports.object({
21875
21889
  participant_id: external_exports.string().min(1).max(120).describe("Who left."),
21876
21890
  removed_by: external_exports.string().min(1).max(120).optional().describe("Who removed them, if not themselves."),
21877
- reason: external_exports.enum([...RELEASE_REASONS, "person_removed"]).optional().describe("For an agent (ADR 0087): why it left, as `agent.released` says. For a person, `person_removed`: they were removed from the workspace (ADR 0160). Written by Chips; absent, a person took them out or they left."),
21891
+ reason: external_exports.enum([...RELEASE_REASONS, "person_removed", "person_left"]).optional().describe("For an agent (ADR 0087): why it left, as `agent.released` says. For a person, `person_removed`: they were removed from the workspace (ADR 0160); `person_left`: they deleted their account and left it (ADR 0243). Written by Chips; absent, a person took them out or they left the room."),
21878
21892
  moved_to: id("space").optional().describe("For an agent moved away: the room it went to, so this room can say so.")
21879
21893
  })
21880
21894
  });
@@ -22183,14 +22197,14 @@ var personRemoved = defineEvent({
22183
22197
  type: "person.removed",
22184
22198
  direction: "workspace",
22185
22199
  sender: "chips",
22186
- summary: "A person was removed from the workspace: signed out everywhere, out of every room, and the harnesses they owned handed to whoever removed them. What they said stays, under their name.",
22200
+ summary: "A person was removed from the workspace, or deleted their account: signed out everywhere, out of every room, and the harnesses they owned handed on. What they said stays, under their name.",
22187
22201
  requiresRun: false,
22188
22202
  payload: external_exports.object({
22189
22203
  participant_id: id("person").describe("Who was removed."),
22190
22204
  display_name: external_exports.string().min(1).max(120).describe("What they were called when they were removed, so the audit needs no lookup."),
22191
- removed_by: id("person").describe("The owner or admin who removed them."),
22205
+ removed_by: id("person").describe("The owner or admin who removed them, or they themselves when they deleted their account (ADR 0243)."),
22192
22206
  spaces: external_exports.array(id("space")).max(1e3).optional().describe("The rooms they were taken out of, each with its own `membership.removed`."),
22193
- harnesses: external_exports.array(id("harness")).max(1e3).optional().describe("The harnesses they owned, handed to `removed_by` by `harness.transferred`.")
22207
+ harnesses: external_exports.array(id("harness")).max(1e3).optional().describe("The harnesses they owned, each handed on by `harness.transferred`: to `removed_by`, or, for an account deleted, to an owner.")
22194
22208
  })
22195
22209
  });
22196
22210
  var joinWho = {
@@ -22536,7 +22550,7 @@ var clientHelloFrame = external_exports.object({
22536
22550
  })).max(64).optional().describe("Not read by Chips: a connection is its token's, and the server's `hello` names the token's harness and its agents (ADR 0087). Absent for a person's client."),
22537
22551
  since: external_exports.int().nonnegative().optional().describe("Not read by Chips: a stream resumes from `subscribe`'s `since`."),
22538
22552
  accepts: external_exports.array(external_exports.string().min(1).max(40)).max(32).optional().describe("Server frames beyond the base set this client understands, by the names in `SERVER_EXTENSIONS`. A name the server does not know is ignored rather than refused, so a client may name a frame a newer server sends and still be answered by an older one (ADR 0211, Phase 3). A client that says `events` is sent its replay in pages rather than one frame per event; one that says `presence` is sent an agent's state as it changes (ADR 0083); a harness's socket that says `restart` is sent `restart` when a person asks it to restart (ADR 0094); an owner's or admin's that says `join` is sent each join request as it changes (ADR 0122); a harness's that says `context` is sent `context.edit` when a person changes one of its context files (ADR 0134); a harness's that says `models` is sent `model.change` when a person changes an agent's model or effort (ADR 0140); a harness's that says `heartbeats` is sent `heartbeat.change` when a person switches an agent's heartbeat or changes how often it beats (ADR 0143); a harness's that says `context_windows` is sent `context_window.compact` and `context_window.change` when a person asks an agent to compact now or changes the limit it compacts itself at (ADR 0146); a person's that says `folders` is sent `folder.open` when they ask, from another of their devices, for a room's folder to be opened in Finder (ADR 0225), and `folder.run` when they ask for its project to be run or stopped there (ADR 0226); a person's that says `folder_runs` is sent `folder.run_state` when their Mac says how a run of theirs stands (ADR 0226); one that says nothing is sent what it was always sent, since an unknown frame is dropped in silence."),
22539
- context: contextBudget.optional().describe("How much of a room a harness is handed with each message for it (ADR 0088): 20 lines and 4,000 characters when absent, 200 and 32,000 at most. For this connection. A person's client is handed none.")
22553
+ context: contextBudget.optional().describe("How much of a room a harness is handed with each message for it (ADR 0088): 40 lines and 16,000 characters when absent, 200 and 32,000 at most. For this connection. A person's client is handed none.")
22540
22554
  }).describe("The first frame a client sends.");
22541
22555
  var subscribeFrame = external_exports.object({
22542
22556
  frame: external_exports.literal("subscribe"),
@@ -25098,8 +25112,19 @@ async function redeem(url2, secret, options, send, machine) {
25098
25112
  throw await refused(answered);
25099
25113
  return await answered.json();
25100
25114
  }
25115
+ function reaching(send, url2) {
25116
+ return async (input2, init) => {
25117
+ try {
25118
+ return await send(input2, init);
25119
+ } catch (error62) {
25120
+ if (error62 instanceof Error && error62.name === "AbortError")
25121
+ throw error62;
25122
+ throw new JoinError("unreachable", `Can't reach Chips at ${url2} from this computer. It has to be on the same network as Chips, on its tailnet, or Chips has to be at an address this computer can reach.`);
25123
+ }
25124
+ };
25125
+ }
25101
25126
  function askOrRedeem(server, options) {
25102
- const send = options.fetch ?? fetch;
25127
+ const send = reaching(options.fetch ?? fetch, server.url);
25103
25128
  const machine = options.machine ?? hostname3();
25104
25129
  return server.secret === void 0 ? ask(server.url, options, send, machine) : redeem(server.url, server.secret, options, send, machine);
25105
25130
  }
@@ -25265,14 +25290,31 @@ async function sendAnswer(answer, options) {
25265
25290
  }
25266
25291
 
25267
25292
  // node_modules/@chipschat/harness/dist/prompt.js
25293
+ function takerOf(message, options) {
25294
+ return options.taker ?? (message.for?.length === 1 ? message.for[0] : void 0);
25295
+ }
25296
+ function othersBeside(message, taker) {
25297
+ return (message.room?.members ?? []).some((member) => member.kind === "agent" && member.participant_id !== taker);
25298
+ }
25299
+ function rulesFor(message, options = {}) {
25300
+ const taker = takerOf(message, options);
25301
+ const { project } = options;
25302
+ const rules = message.room?.rules ?? {};
25303
+ return [
25304
+ taker === void 0 ? void 0 : rules[taker],
25305
+ project === void 0 ? void 0 : renderProjectAsks(project, { othersInRoom: othersBeside(message, taker) }),
25306
+ options.sendsFiles === true ? sendingFiles({ project: project !== void 0 }) : void 0
25307
+ ].filter((part) => part !== void 0 && part !== "").join("\n\n");
25308
+ }
25268
25309
  function promptFor(message, options = {}) {
25269
- const taker = options.taker ?? (message.for?.length === 1 ? message.for[0] : void 0);
25310
+ const taker = takerOf(message, options);
25311
+ const held = options.rulesHeld === true;
25270
25312
  const notes = message.room?.notes ?? {};
25271
- const note = taker === void 0 ? void 0 : notes[taker];
25313
+ const note = taker === void 0 ? void 0 : held ? noteWithoutRules(message.room, taker) : notes[taker];
25272
25314
  const who = options.sayWho === false ? "" : `${withRole(message.from.display_name, message.from)}: `;
25273
25315
  const files = (message.attachments ?? []).map(renderAttachment);
25274
25316
  const words4 = [`${who}${message.text}`, ...files].join("\n");
25275
- const sending = options.sendsFiles === true ? sendingFiles({ project: options.project !== void 0 }) : void 0;
25317
+ const sending = options.sendsFiles === true && !held ? sendingFiles({ project: options.project !== void 0 }) : void 0;
25276
25318
  const replying = renderReplyingTo(message.replying_to, {
25277
25319
  sender: {
25278
25320
  id: message.from.participant_id,
@@ -25284,8 +25326,7 @@ function promptFor(message, options = {}) {
25284
25326
  at: message.ts,
25285
25327
  zone: options.zone
25286
25328
  });
25287
- const others = (message.room?.members ?? []).some((member) => member.kind === "agent" && member.participant_id !== taker);
25288
- const project = options.project === void 0 ? void 0 : renderProjectNote(options.project, { othersInRoom: others });
25329
+ const project = options.project === void 0 ? void 0 : held ? renderProjectFacts(options.project) : renderProjectNote(options.project, { othersInRoom: othersBeside(message, taker) });
25289
25330
  return [
25290
25331
  message.exchange?.note,
25291
25332
  note,
@@ -25472,7 +25513,10 @@ function nodeOnPath(env = process.env) {
25472
25513
  }
25473
25514
  var pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
25474
25515
  var LOAD_TRIES = 40;
25475
- var runOf = (options) => options.run ?? ((command) => void execFileSync2(command[0], command.slice(1), { stdio: "ignore" }));
25516
+ var runOf = (options) => options.run ?? ((command) => execFileSync2(command[0], command.slice(1), {
25517
+ encoding: "utf8",
25518
+ stdio: ["ignore", "pipe", "ignore"]
25519
+ }));
25476
25520
  var ours = (path) => !existsSync3(path) || readFileSync3(path, "utf8").includes(MARK);
25477
25521
  function runOne(command, run2, retryMs) {
25478
25522
  const tries = command[0] === "launchctl" && command[1] === "bootstrap" ? LOAD_TRIES : 1;
@@ -25495,6 +25539,24 @@ function runAll(commands, options) {
25495
25539
  runOne(command, run2, options.retryMs ?? 250);
25496
25540
  }
25497
25541
  var uidOf = (options) => options.uid ?? process.getuid?.() ?? 0;
25542
+ function lingers(options) {
25543
+ const run2 = runOf(options);
25544
+ const uid = String(uidOf(options));
25545
+ try {
25546
+ const on = run2(["loginctl", "show-user", uid, "--property=Linger", "--value"]);
25547
+ if (typeof on === "string" && on.trim() === "yes")
25548
+ return true;
25549
+ } catch {
25550
+ }
25551
+ try {
25552
+ run2(["loginctl", "enable-linger", uid]);
25553
+ return true;
25554
+ } catch {
25555
+ return false;
25556
+ }
25557
+ }
25558
+ var sayOf = (options) => options.say ?? ((line2) => void process.stderr.write(`${line2}
25559
+ `));
25498
25560
  function keepRunning(spec, options = {}) {
25499
25561
  if (machineOf(options.machine).env["CHIPS_KEEP"] === "off")
25500
25562
  return { state: "off" };
@@ -25509,6 +25571,9 @@ function keepRunning(spec, options = {}) {
25509
25571
  writeFileSync2(file3.path, file3.contents, { mode: 384 });
25510
25572
  chmodSync2(file3.path, 384);
25511
25573
  runAll(startCommands(file3.kind, spec.label, file3.path, uidOf(options)), options);
25574
+ if (file3.kind === "systemd" && !lingers(options)) {
25575
+ sayOf(options)("It stops when you log out. To keep it running on a machine nobody logs in to, run this once: sudo loginctl enable-linger $USER");
25576
+ }
25512
25577
  return { state: "kept", path: file3.path, logFile: file3.logFile };
25513
25578
  }
25514
25579
  function stopKeeping(label2, options = {}) {
@@ -25555,7 +25620,7 @@ function fileStore(path) {
25555
25620
 
25556
25621
  // src/bridge.ts
25557
25622
  import { mkdirSync as mkdirSync5 } from "node:fs";
25558
- import { join as join8 } from "node:path";
25623
+ import { join as join10 } from "node:path";
25559
25624
 
25560
25625
  // src/context.ts
25561
25626
  import { spawn } from "node:child_process";
@@ -25578,7 +25643,7 @@ import { basename as basename3, dirname as dirname4, join as join6 } from "node:
25578
25643
 
25579
25644
  // src/home.ts
25580
25645
  import { execFileSync as execFileSync3 } from "node:child_process";
25581
- import { existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
25646
+ import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
25582
25647
  import { homedir as homedir4 } from "node:os";
25583
25648
  import { join as join5, resolve as resolve2 } from "node:path";
25584
25649
  function hermesRoot(env = process.env) {
@@ -25605,6 +25670,14 @@ function envValue(home2, name2) {
25605
25670
  }
25606
25671
  return found;
25607
25672
  }
25673
+ function appendEnv(home2, name2, value) {
25674
+ const path = join5(home2, ".env");
25675
+ const text = readFileSync5(path, "utf8");
25676
+ const mode = statSync(path).mode & 511;
25677
+ appendFileSync(path, `${text === "" || text.endsWith("\n") ? "" : "\n"}${name2}=${value}
25678
+ `);
25679
+ chmodSync3(path, mode);
25680
+ }
25608
25681
  function configText(home2) {
25609
25682
  const path = join5(home2, "config.yaml");
25610
25683
  try {
@@ -25839,13 +25912,9 @@ function fileOf(source2, name2) {
25839
25912
  }
25840
25913
  var byOrder = (a, b) => ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind) || a.profile.localeCompare(b.profile);
25841
25914
  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) {
25915
+ constructor(readers, first, env = process.env, ownProfiles = false) {
25916
+ this.first = first;
25917
+ this.ownProfiles = ownProfiles;
25849
25918
  const byProfile = /* @__PURE__ */ new Map();
25850
25919
  const sourcesFor = (profile) => {
25851
25920
  let sources = byProfile.get(profile.home);
@@ -25867,7 +25936,7 @@ var Picture = class {
25867
25936
  }
25868
25937
  const taken = /* @__PURE__ */ new Set();
25869
25938
  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));
25939
+ 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
25940
  for (const { source: source2 } of sharing) {
25872
25941
  const name2 = taken.has(source2.file) ? `${source2.file} (${source2.profile})` : source2.file;
25873
25942
  taken.add(name2);
@@ -25877,8 +25946,24 @@ var Picture = class {
25877
25946
  this.shared.set(source2.path, { id: id2, name: name2 });
25878
25947
  }
25879
25948
  }
25949
+ first;
25950
+ ownProfiles;
25951
+ /** Each file by its path, and the agents that read it. */
25952
+ readers = /* @__PURE__ */ new Map();
25953
+ /** The shared files, each with its id and the name the set shows. */
25954
+ shared = /* @__PURE__ */ new Map();
25955
+ /** The agents and the files each reads, in page order. */
25956
+ agents = /* @__PURE__ */ new Map();
25957
+ /**
25958
+ * Whether each new agent starts with this file or a copy of it: the first profile's files, or
25959
+ * where each new agent gets a profile of its own, the first profile's SOUL.md and the project file.
25960
+ */
25961
+ forNewAgents(source2) {
25962
+ if (source2.profile !== this.first?.name) return false;
25963
+ return !this.ownProfiles || source2.kind === "soul" || source2.kind === "project";
25964
+ }
25880
25965
  /** The harness's own set, as `PUT /v1/harnesses/:id/context` takes it. */
25881
- harnessCopy(first) {
25966
+ harnessCopy() {
25882
25967
  const shown2 = [];
25883
25968
  const notShown = [];
25884
25969
  for (const [path, { id: id2, name: name2 }] of this.shared) {
@@ -25894,8 +25979,7 @@ var Picture = class {
25894
25979
  ...file3,
25895
25980
  id: id2,
25896
25981
  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 } : {}
25982
+ ...this.forNewAgents(source2) ? { for_new_agents: true } : {}
25899
25983
  });
25900
25984
  }
25901
25985
  return {
@@ -26056,9 +26140,9 @@ var Contexts = class {
26056
26140
  /** Every copy read again, and sent where it moved. */
26057
26141
  refresh() {
26058
26142
  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) => {
26143
+ const { readers, first, ownProfiles } = this.roster();
26144
+ const picture = new Picture(readers, first, this.env, ownProfiles);
26145
+ await this.port.harness(picture.harnessCopy()).catch((error62) => {
26062
26146
  this.log(`the harness's context not sent: ${messageOf(error62)}`);
26063
26147
  });
26064
26148
  for (const { agentId: agentId2 } of readers) {
@@ -26076,12 +26160,12 @@ var Contexts = class {
26076
26160
  */
26077
26161
  async edit(frame) {
26078
26162
  const answer = await this.inLine(async () => {
26079
- const { readers, first } = this.roster();
26163
+ const { readers, first, ownProfiles } = this.roster();
26080
26164
  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);
26165
+ const picture = new Picture(readers, first, this.env, ownProfiles);
26166
+ return frame.agent_id === void 0 ? picture.harnessCopy() : picture.agentCopy(frame.agent_id);
26083
26167
  };
26084
- const found = new Picture(readers, first, this.env).find(frame.agent_id, frame.name);
26168
+ const found = new Picture(readers, first, this.env, ownProfiles).find(frame.agent_id, frame.name);
26085
26169
  if (found === void 0) {
26086
26170
  throw new Refuse(`Hermes gives this agent no file called ${frame.name}.`, copyNow(), "failed");
26087
26171
  }
@@ -26148,6 +26232,8 @@ function numbersOf(profile, body) {
26148
26232
  if (typeof body !== "object" || body === null) return void 0;
26149
26233
  const top = body;
26150
26234
  if (top["hook_event_name"] !== "post_api_request") return void 0;
26235
+ const sender = top["profile"];
26236
+ if (typeof sender === "string" && sender !== profile && sender !== "custom") return void 0;
26151
26237
  const extra = typeof top["extra"] === "object" && top["extra"] !== null ? top["extra"] : {};
26152
26238
  const named = typeof top["session_id"] === "string" ? top["session_id"] : "";
26153
26239
  const task2 = typeof extra["task_id"] === "string" && extra["task_id"] !== "" ? extra["task_id"] : void 0;
@@ -26453,6 +26539,13 @@ var HermesClient = class {
26453
26539
  const latest = read["session_id"];
26454
26540
  return typeof latest === "string" && latest !== "" ? latest : sessionId;
26455
26541
  }
26542
+ /**
26543
+ * The models Hermes has for this profile, as its own picker lists them: `GET /api/model/options`,
26544
+ * only the providers the profile is set up for (`models.ts`).
26545
+ */
26546
+ async modelOptions() {
26547
+ return await this.call("GET", "/api/model/options?include_unconfigured=false");
26548
+ }
26456
26549
  /** A run in a session: what it is told, and under a key a retry of the same turn repeats. */
26457
26550
  async startRun(input2) {
26458
26551
  const started = await this.call(
@@ -26462,6 +26555,7 @@ var HermesClient = class {
26462
26555
  input: input2.text,
26463
26556
  session_id: input2.sessionId,
26464
26557
  ...input2.model === void 0 ? {} : { model: input2.model },
26558
+ ...input2.provider === void 0 ? {} : { provider: input2.provider },
26465
26559
  ...input2.instructions === void 0 ? {} : { instructions: input2.instructions }
26466
26560
  },
26467
26561
  { "Idempotency-Key": input2.key.slice(0, 255) }
@@ -26558,7 +26652,7 @@ async function failureOf(answer) {
26558
26652
  }
26559
26653
 
26560
26654
  // src/hooks.ts
26561
- import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
26655
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
26562
26656
  import { join as join7 } from "node:path";
26563
26657
  var HOOK_NAME = "chips";
26564
26658
  var SECRET_ENV = "CHIPS_HOOK_SECRET";
@@ -26595,12 +26689,7 @@ function entryLines(indent, url2, secret) {
26595
26689
  function secretInEnv(home2, secret) {
26596
26690
  const path = join7(home2, ".env");
26597
26691
  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);
26692
+ if (envValue(home2, SECRET_ENV) === void 0) appendEnv(home2, SECRET_ENV, secret);
26604
26693
  return true;
26605
26694
  }
26606
26695
  function addHook(home2, port, profile, secret) {
@@ -26661,1417 +26750,1807 @@ function addHook(home2, port, profile, secret) {
26661
26750
  return { state: "added", path };
26662
26751
  }
26663
26752
 
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
- };
26753
+ // src/models.ts
26754
+ var MODEL_CHOICES_MAX2 = 40;
26755
+ var LABEL_MAX = 80;
26756
+ var DEFAULT_LABEL = "Hermes's default";
26757
+ function choiceId(provider, model) {
26758
+ return `${provider}/${model}`;
26759
+ }
26760
+ function runModelOf(id2) {
26761
+ const at = id2.indexOf("/");
26762
+ return at <= 0 ? { model: id2 } : { provider: id2.slice(0, at), model: id2.slice(at + 1) };
26763
+ }
26764
+ function providersOf(options) {
26765
+ const ready = (options.providers ?? []).filter(
26766
+ (provider) => provider.authenticated !== false && (provider.models ?? []).length > 0
26767
+ );
26768
+ const current = (provider) => provider.is_current === true || provider.slug === options.provider;
26769
+ return [...ready.filter(current), ...ready.filter((provider) => !current(provider))];
26770
+ }
26771
+ function choicesOf(options) {
26772
+ const choices = [];
26773
+ for (const provider of providersOf(options)) {
26774
+ for (const model of provider.models ?? []) {
26775
+ const id2 = choiceId(provider.slug, model);
26776
+ if (choices.some((choice) => choice.id === id2)) continue;
26777
+ choices.push({ id: id2, label: `${model} \xB7 ${provider.name ?? provider.slug}`.slice(0, LABEL_MAX), efforts: [] });
26778
+ }
26779
+ }
26780
+ return choices;
26781
+ }
26782
+ function defaultOf(options) {
26783
+ const model = options.model?.trim();
26784
+ if (model === void 0 || model === "") return void 0;
26785
+ return options.provider === void 0 || options.provider === "" ? model : choiceId(options.provider, model);
26786
+ }
26787
+ function matched(choices, model) {
26788
+ if (choices.some((choice) => choice.id === model)) return model;
26789
+ return choices.find((choice) => runModelOf(choice.id).model === model)?.id ?? model;
26790
+ }
26791
+ function copyOf(options, own2) {
26792
+ let choices = choicesOf(options);
26793
+ const fallback = defaultOf(options);
26794
+ const byDefault = fallback === void 0 ? void 0 : matched(choices, fallback);
26795
+ const mine = own2 === void 0 ? void 0 : matched(choices, own2);
26796
+ const model = mine ?? byDefault ?? choices[0]?.id;
26797
+ if (model === void 0) return void 0;
26798
+ const keep = [model, ...byDefault === void 0 ? [] : [byDefault]];
26799
+ for (const id2 of keep) {
26800
+ if (!choices.some((choice) => choice.id === id2)) choices = [{ id: id2, label: id2.slice(0, LABEL_MAX), efforts: [] }, ...choices];
26801
+ }
26802
+ let others = MODEL_CHOICES_MAX2 - keep.filter((id2, at) => keep.indexOf(id2) === at).length;
26803
+ choices = choices.filter((choice) => keep.includes(choice.id) || others-- > 0);
26672
26804
  return {
26673
- context: (edit) => bridge2().onContextEdit(edit),
26674
- contextWindows: {
26675
- compact: async (ask2) => bridge2().onWindowCompact(ask2),
26676
- change: async (ask2) => bridge2().onWindowChange(ask2)
26677
- }
26805
+ model,
26806
+ own: mine !== void 0,
26807
+ ...byDefault === void 0 ? {} : { default: { label: DEFAULT_LABEL, model: byDefault } },
26808
+ choices,
26809
+ editable: true
26678
26810
  };
26679
26811
  }
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.`;
26812
+
26813
+ // src/profiles.ts
26814
+ import { randomBytes as randomBytes2 } from "node:crypto";
26815
+ import { existsSync as existsSync9, readFileSync as readFileSync9, rmSync as rmSync4 } from "node:fs";
26816
+ import { createConnection } from "node:net";
26817
+ import { join as join9 } from "node:path";
26818
+
26819
+ // src/gateway.ts
26820
+ import { spawn as spawn2 } from "node:child_process";
26821
+ import { createHash as createHash3 } from "node:crypto";
26822
+ 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";
26823
+ import { homedir as homedir5 } from "node:os";
26824
+ import { basename as basename4, delimiter as delimiter2, dirname as dirname5, join as join8, resolve as resolve3 } from "node:path";
26825
+ var TIMING = {
26826
+ stopMs: 15e3,
26827
+ killMs: 5e3,
26828
+ earlyMs: 1e4,
26829
+ upMs: 9e4,
26830
+ serviceMs: 15e4,
26831
+ pollMs: 500
26832
+ };
26833
+ var EXPAT = "/opt/homebrew/opt/expat/lib/libexpat.1.dylib";
26834
+ var EXPAT_SYMBOL = "_XML_SetAllocTrackerActivationThreshold";
26835
+ function gatewaysOf(agents) {
26836
+ return [...new Set(agents.map(({ address }) => address.url.replace(/\/+$/, "")))].map((url2) => ({ url: url2 }));
26682
26837
  }
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.`;
26838
+ function isLocal(url2) {
26839
+ try {
26840
+ return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(new URL(url2).hostname);
26841
+ } catch {
26842
+ return false;
26843
+ }
26687
26844
  }
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;
26845
+ function portOf(url2) {
26846
+ const parsed = new URL(url2);
26847
+ return Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
26848
+ }
26849
+ function line(words4) {
26850
+ const one = words4.replace(/\s+/g, " ").trim();
26851
+ return one.length <= 200 ? one : `${one.slice(0, 199)}\u2026`;
26852
+ }
26853
+ function whyFrom(output2, code) {
26854
+ const lines2 = output2.split("\n").map((each) => each.trim()).filter((each) => each !== "");
26855
+ const why = lines2.findLast((each) => /^[A-Za-z_][\w.]*(Error|Exception)\b/.test(each)) ?? lines2.at(-1);
26856
+ if (why === void 0) return code === null || code === void 0 ? "it was stopped" : `it exited ${code}`;
26857
+ return why.length <= 200 ? why : why.replace(/\/(?:[^\s/(),:'"]+\/)+([^\s/(),:'"]+)/g, "$1");
26858
+ }
26859
+ function gatewayRunArgs(command) {
26860
+ const words4 = command.trim().split(/\s+/);
26861
+ const at = words4.findIndex((word) => basename4(word) === "hermes");
26862
+ if (at === -1) return void 0;
26863
+ const args = words4.slice(at + 1);
26864
+ const plain = [];
26865
+ for (let next = 0; next < args.length; next += 1) {
26866
+ const word = args[next] ?? "";
26867
+ if (word === "-p" || word === "--profile") next += 1;
26868
+ else if (!word.startsWith("-")) plain.push(word);
26693
26869
  }
26870
+ return plain[0] === "gateway" && plain[1] === "run" ? args : void 0;
26694
26871
  }
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, "-")}`;
26872
+ function serviceOf(system, root2, env = {}) {
26873
+ const home2 = resolve3(root2);
26874
+ const native = resolve3(system.home, ".hermes");
26875
+ const suffix = home2 === native ? "" : dirname5(home2) === join8(native, "profiles") ? basename4(home2) : createHash3("sha256").update(home2).digest("hex").slice(0, 8);
26876
+ if (system.platform === "darwin") {
26877
+ const path = join8(system.home, "Library", "LaunchAgents", `ai.hermes.gateway${suffix ? `-${suffix}` : ""}.plist`);
26878
+ return system.exists(path) ? { kind: "launchd", path } : void 0;
26879
+ }
26880
+ if (system.platform === "linux") {
26881
+ const unit = `hermes-gateway${suffix ? `-${suffix}` : ""}.service`;
26882
+ const user = join8(env["XDG_CONFIG_HOME"] || join8(system.home, ".config"), "systemd", "user", unit);
26883
+ if (system.exists(user)) return { kind: "systemd", path: user };
26884
+ const machine = join8("/etc/systemd/system", unit);
26885
+ if (system.exists(machine)) return { kind: "systemd", path: machine, system: true };
26886
+ }
26887
+ return void 0;
26717
26888
  }
26718
- var Bridge = class {
26889
+ function expand(text, env) {
26890
+ let out = "";
26891
+ let at = 0;
26892
+ while (at < text.length) {
26893
+ const char = text[at] ?? "";
26894
+ if (char === "\\" && at + 1 < text.length) {
26895
+ out += text[at + 1];
26896
+ at += 2;
26897
+ } else if (char !== "$") {
26898
+ out += char;
26899
+ at += 1;
26900
+ } else if (text[at + 1] === "{") {
26901
+ let depth = 1;
26902
+ let end2 = at + 2;
26903
+ for (; end2 < text.length; end2 += 1) {
26904
+ if (text[end2] === "{") depth += 1;
26905
+ else if (text[end2] === "}" && --depth === 0) break;
26906
+ }
26907
+ const inner = /^([A-Za-z_][A-Za-z0-9_]*)(?:(:?)-([\s\S]*))?$/.exec(text.slice(at + 2, end2));
26908
+ if (inner !== null) {
26909
+ const value = env[inner[1] ?? ""];
26910
+ const unset = inner[2] === ":" ? value === void 0 || value === "" : value === void 0;
26911
+ out += inner[3] !== void 0 && unset ? expand(inner[3], env) : value ?? "";
26912
+ }
26913
+ at = end2 + 1;
26914
+ } else {
26915
+ const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(at + 1))?.[0];
26916
+ out += name2 === void 0 ? "$" : env[name2] ?? "";
26917
+ at += 1 + (name2?.length ?? 0);
26918
+ }
26919
+ }
26920
+ return out;
26921
+ }
26922
+ function wrapperOf(text, env) {
26923
+ for (const each of text.split("\n")) {
26924
+ const found = /^\s*((?:[A-Za-z_][A-Za-z0-9_]*="(?:[^"\\]|\\.)*"\s+)*)exec\s+"([^"]+)"/.exec(each);
26925
+ if (found === null) continue;
26926
+ const vars = {};
26927
+ for (const set2 of (found[1] ?? "").matchAll(/([A-Za-z_][A-Za-z0-9_]*)="((?:[^"\\]|\\.)*)"/g)) {
26928
+ vars[set2[1] ?? ""] = expand(set2[2] ?? "", env);
26929
+ }
26930
+ return { entry: expand(found[2] ?? "", env), vars };
26931
+ }
26932
+ return void 0;
26933
+ }
26934
+ var NO_HERMES = "The hermes command isn't on this machine's PATH, so Hermes's gateway can't be started.";
26935
+ var messageOf2 = (error62) => error62 instanceof Error ? error62.message : String(error62);
26936
+ function gatewayEnv(env) {
26937
+ return Object.fromEntries(Object.entries(env).filter(([name2]) => !name2.startsWith("CHIPS_")));
26938
+ }
26939
+ async function runHermes(system, env, args, timeoutMs, log2) {
26940
+ const hermes = system.which("hermes", env);
26941
+ if (hermes === void 0) return { code: null, output: "The hermes command isn't on this machine's PATH." };
26942
+ const plain = gatewayEnv(env);
26943
+ const ran = await system.run(hermes, args, plain, timeoutMs);
26944
+ if (ran.code === 0 || !ran.output.includes(EXPAT_SYMBOL) || !system.exists(EXPAT)) return ran;
26945
+ const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
26946
+ if (wrapper === void 0) return ran;
26947
+ const expat = dirname5(EXPAT);
26948
+ log2(
26949
+ `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}.`
26950
+ );
26951
+ const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
26952
+ return system.run(wrapper.entry, args, { ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries }, timeoutMs);
26953
+ }
26954
+ var Gateways = class {
26719
26955
  constructor(options) {
26720
26956
  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
26957
  }
26749
26958
  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 });
26959
+ timing = TIMING;
26960
+ /** Each gateway restarted, one after another: what went wrong, in a line, or nothing. */
26961
+ async restart() {
26962
+ const troubles = [];
26963
+ for (const gateway of this.options.gateways) {
26964
+ let trouble;
26965
+ try {
26966
+ trouble = await this.restartOne(gateway);
26967
+ } catch (error62) {
26968
+ trouble = `Restart failed: ${messageOf2(error62)}`;
26969
+ }
26970
+ if (trouble !== void 0) troubles.push(trouble);
26785
26971
  }
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();
26972
+ return troubles.length === 0 ? void 0 : line(troubles.join(" "));
26803
26973
  }
26804
- /** Hold the health watch's words while Hermes's gateway restarts, and let them go after. */
26805
- hold(held) {
26806
- this.held = held;
26974
+ /** Each local gateway that doesn't answer, started: the bridge is starting. */
26975
+ async startIfDown() {
26976
+ const { system, log: log2 } = this.options;
26977
+ for (const gateway of this.options.gateways) {
26978
+ if (!isLocal(gateway.url)) continue;
26979
+ const port = portOf(gateway.url);
26980
+ try {
26981
+ if (await system.healthy(gateway.url)) continue;
26982
+ let trouble;
26983
+ if (this.serviceFor(gateway) !== void 0) {
26984
+ log2(`Hermes isn't answering on port ${port}: starting its service, hermes gateway start`);
26985
+ trouble = await this.byService(gateway, "start", void 0);
26986
+ } else {
26987
+ const pid = await system.listener(port);
26988
+ if (pid !== void 0) {
26989
+ log2(`Hermes isn't answering on port ${port}, but pid ${pid} listens there: no second gateway started`);
26990
+ continue;
26991
+ }
26992
+ log2(`Hermes isn't answering on port ${port}: starting hermes gateway run`);
26993
+ trouble = await this.start(gateway, ["gateway", "run"], void 0);
26994
+ }
26995
+ if (trouble !== void 0) log2(trouble);
26996
+ } catch (error62) {
26997
+ log2(`Hermes's gateway on port ${port} couldn't be started: ${messageOf2(error62)}`);
26998
+ }
26999
+ }
26807
27000
  }
26808
27001
  /**
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.
27002
+ * The service of Hermes's root, for the Main agent's gateway: a gateway at another address is
27003
+ * another profile's own, which `hermes gateway status` of the root doesn't speak for.
26811
27004
  */
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();
27005
+ serviceFor(gateway) {
27006
+ if (gateway !== this.options.gateways[0]) return void 0;
27007
+ return serviceOf(this.options.system, this.options.root, this.options.env);
26822
27008
  }
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`);
27009
+ async restartOne(gateway) {
27010
+ const { system, log: log2 } = this.options;
27011
+ if (!isLocal(gateway.url)) {
27012
+ return `Hermes's gateway at ${new URL(gateway.url).host} isn't on this machine, so it can't be restarted from here.`;
26846
27013
  }
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);
27014
+ const port = portOf(gateway.url);
27015
+ const pid = await system.listener(port);
27016
+ if (this.serviceFor(gateway) !== void 0) {
27017
+ log2("restarting Hermes's service: hermes gateway restart");
27018
+ return this.byService(gateway, "restart", pid);
26863
27019
  }
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");
27020
+ if (pid === void 0) {
27021
+ if (await system.healthy(gateway.url)) return `Can't tell what answers on port ${port}, so nothing was restarted.`;
27022
+ log2(`nothing listens on port ${port}: starting hermes gateway run`);
27023
+ return this.start(gateway, ["gateway", "run"], void 0);
27024
+ }
27025
+ const info = await system.describe(pid);
27026
+ if (info === void 0) return this.start(gateway, ["gateway", "run"], void 0);
27027
+ const args = this.ours(info);
27028
+ if (args === void 0) {
27029
+ log2(`pid ${pid} on port ${port} isn't this user's hermes gateway run, so it was left alone: ${info.command.slice(0, 160)}`);
27030
+ return `Port ${port} is held by something other than hermes gateway run (pid ${pid}), so nothing was restarted.`;
27031
+ }
27032
+ if (system.which("hermes", this.options.env) === void 0) return NO_HERMES;
27033
+ log2(`stopping Hermes's gateway, pid ${pid}`);
27034
+ if (!await this.stop(pid)) return `Hermes's gateway (pid ${pid}) didn't stop, so it wasn't started again.`;
27035
+ return this.start(gateway, args, info.cwd);
26869
27036
  }
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) };
27037
+ /** A `hermes gateway run` of this user's: the words to start it again with. */
27038
+ ours(info) {
27039
+ return info.uid === this.options.system.uid ? gatewayRunArgs(info.command) : void 0;
26875
27040
  }
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);
27041
+ /** SIGTERM, then SIGKILL once it has had a while, and only while it is still that gateway. */
27042
+ async stop(pid) {
27043
+ const { system, log: log2 } = this.options;
27044
+ system.signal(pid, "SIGTERM");
27045
+ if (await this.gone(pid, this.timing.stopMs)) return true;
27046
+ const still = await system.describe(pid);
27047
+ if (still === void 0) return true;
27048
+ if (this.ours(still) === void 0) return false;
27049
+ log2(`Hermes's gateway, pid ${pid}, is still running ${this.timing.stopMs / 1e3} s after SIGTERM: SIGKILL`);
27050
+ system.signal(pid, "SIGKILL");
27051
+ return this.gone(pid, this.timing.killMs);
26883
27052
  }
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 });
27053
+ async gone(pid, withinMs) {
27054
+ const { system } = this.options;
27055
+ const by = system.now() + withinMs;
27056
+ while (system.alive(pid)) {
27057
+ if (system.now() >= by) return false;
27058
+ await system.sleep(this.timing.pollMs);
26892
27059
  }
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 };
27060
+ return true;
26904
27061
  }
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);
27062
+ /** Hermes's own `hermes gateway restart|start`, then the gateway answering, from a new process. */
27063
+ async byService(gateway, verb, before) {
27064
+ const { system, env } = this.options;
27065
+ const hermes = system.which("hermes", env);
27066
+ if (hermes === void 0) return NO_HERMES;
27067
+ const scope = this.serviceFor(gateway)?.system === true ? ["--system"] : [];
27068
+ const ran = await system.run(hermes, ["gateway", verb, ...scope], gatewayEnv(env), this.timing.serviceMs);
27069
+ if (ran.code !== 0) return line(`hermes gateway ${verb} failed: ${whyFrom(ran.output, ran.code)}`);
27070
+ const by = system.now() + this.timing.serviceMs;
27071
+ for (; ; ) {
27072
+ const old = before !== void 0 && system.alive(before);
27073
+ if (!old && await system.healthy(gateway.url)) return void 0;
27074
+ if (system.now() >= by) {
27075
+ if (await system.healthy(gateway.url)) return void 0;
27076
+ return `Hermes's service ${verb === "start" ? "started" : "restarted"}, but nothing answers on port ${portOf(gateway.url)}.`;
27077
+ }
27078
+ await system.sleep(this.timing.pollMs);
26910
27079
  }
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));
26916
- }
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));
26921
- }
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;
26925
27080
  }
26926
27081
  /**
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.
27082
+ * `hermes <args>` (`gateway run`), on its own, in `cwd` (else the home), then through Homebrew's
27083
+ * expat when it died of macOS's.
26932
27084
  */
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)}`));
27085
+ async start(gateway, args, cwd) {
27086
+ const { system, env, log: log2 } = this.options;
27087
+ const port = portOf(gateway.url);
27088
+ const hermes = system.which("hermes", env);
27089
+ if (hermes === void 0) return NO_HERMES;
27090
+ const folder2 = cwd ?? system.home;
27091
+ const plain = gatewayEnv(env);
27092
+ let outcome = await this.launch(gateway, hermes, args, plain, folder2, `hermes ${args.join(" ")}`);
27093
+ if (outcome.state === "ended" && outcome.early && outcome.output.includes(EXPAT_SYMBOL) && system.exists(EXPAT)) {
27094
+ const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
27095
+ if (wrapper !== void 0) {
27096
+ const expat = dirname5(EXPAT);
27097
+ log2(
27098
+ `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}.`
27099
+ );
27100
+ const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
27101
+ outcome = await this.launch(
27102
+ gateway,
27103
+ wrapper.entry,
27104
+ args,
27105
+ { ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries },
27106
+ folder2,
27107
+ `${wrapper.entry} ${args.join(" ")} with DYLD_LIBRARY_PATH=${expat}`
27108
+ );
27109
+ }
26945
27110
  }
26946
- return main2;
27111
+ if (outcome.state === "up") {
27112
+ log2(`Hermes's gateway${outcome.pid === void 0 ? "" : `, pid ${outcome.pid},`} answers on port ${port}. It logs to ${this.options.logFile}.`);
27113
+ return void 0;
27114
+ }
27115
+ if (outcome.state === "silent") return `Hermes's gateway started, but nothing answers on port ${port}.`;
27116
+ return line(`Hermes's gateway stopped as it started: ${whyFrom(outcome.output, outcome.code)}`);
26947
27117
  }
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)}`));
27118
+ /** A program started, and watched until it answers, ends, or has had its time. */
27119
+ async launch(gateway, command, args, env, cwd, what) {
27120
+ const { system, logFile } = this.options;
27121
+ system.note(logFile, `--- ${new Date(system.now()).toISOString()} chips-hermes started ${what}`);
27122
+ const from = system.size(logFile);
27123
+ const startedAt = system.now();
27124
+ const child = system.launch({ command, args, env, cwd, log: logFile });
27125
+ let ended;
27126
+ void child.exited.then(({ code }) => {
27127
+ ended = { code, at: system.now() };
27128
+ });
27129
+ for (; ; ) {
27130
+ await Promise.resolve();
27131
+ if (ended !== void 0) {
27132
+ return { state: "ended", early: ended.at - startedAt <= this.timing.earlyMs, output: system.since(logFile, from), code: ended.code };
27133
+ }
27134
+ if (await system.healthy(gateway.url)) return { state: "up", pid: child.pid };
27135
+ if (system.now() - startedAt >= this.timing.upMs) return { state: "silent" };
27136
+ await system.sleep(this.timing.pollMs);
27137
+ }
26954
27138
  }
26955
- /**
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.
26959
- */
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
27139
+ };
27140
+ function restarter(deps) {
27141
+ let running;
27142
+ return (ask2) => {
27143
+ if (running !== void 0) {
27144
+ deps.log(`restart asked by ${ask2.askedBy} while one runs: it is the same restart`);
27145
+ return running;
27146
+ }
27147
+ running = (async () => {
27148
+ deps.log(`restart asked by ${ask2.askedBy}: restarting Hermes's gateway`);
27149
+ deps.hold(true);
27150
+ let trouble;
27151
+ try {
27152
+ trouble = await deps.restart();
27153
+ } catch (error62) {
27154
+ trouble = line(`Restart failed: ${error62 instanceof Error ? error62.message : String(error62)}`);
27155
+ }
27156
+ deps.log(trouble === void 0 ? "restarted, and Hermes answers" : `restart: ${trouble}`);
27157
+ try {
27158
+ await deps.say(trouble);
27159
+ } catch (error62) {
27160
+ deps.log(`could not tell Chips how the restart went: ${String(error62)}`);
27161
+ } finally {
27162
+ deps.hold(false);
27163
+ }
27164
+ })().finally(() => {
27165
+ running = void 0;
27166
+ });
27167
+ return running;
27168
+ };
27169
+ }
27170
+ function gatewayLog(configFolder, platform = process.platform) {
27171
+ return platform === "darwin" ? join8(homedir5(), "Library", "Logs", "chips-hermes-gateway.log") : join8(configFolder, "gateway.log");
27172
+ }
27173
+ function runToEnd(command, args, options) {
27174
+ return new Promise((done) => {
27175
+ const child = spawn2(command, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"] });
27176
+ let output2 = "";
27177
+ const keep = (data) => {
27178
+ output2 = (output2 + String(data)).slice(-8e3);
27179
+ };
27180
+ child.stdout.on("data", keep);
27181
+ child.stderr.on("data", keep);
27182
+ const limit = setTimeout(() => child.kill("SIGTERM"), options.timeoutMs);
27183
+ child.on("error", (error62) => {
27184
+ clearTimeout(limit);
27185
+ done({ code: null, output: error62.message });
27186
+ });
27187
+ child.on("close", (code) => {
27188
+ clearTimeout(limit);
27189
+ done({ code, output: output2 });
27190
+ });
27191
+ });
27192
+ }
27193
+ function thisMachine() {
27194
+ return {
27195
+ platform: process.platform,
27196
+ uid: process.getuid?.() ?? -1,
27197
+ home: homedir5(),
27198
+ exists: existsSync8,
27199
+ read(path) {
27200
+ try {
27201
+ return readFileSync8(path, "utf8");
27202
+ } catch {
27203
+ return void 0;
27204
+ }
27205
+ },
27206
+ which(command, env) {
27207
+ for (const folder2 of (env["PATH"] ?? "").split(delimiter2)) {
27208
+ if (folder2 === "") continue;
27209
+ const path = join8(folder2, command);
27210
+ try {
27211
+ accessSync2(path, constants2.X_OK);
27212
+ return path;
27213
+ } catch {
27214
+ }
27215
+ }
27216
+ return void 0;
27217
+ },
27218
+ async listener(port) {
27219
+ const { output: output2 } = await runToEnd("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], { timeoutMs: 5e3 });
27220
+ const pid = Number(/^p(\d+)$/m.exec(output2)?.[1]);
27221
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
27222
+ },
27223
+ async describe(pid) {
27224
+ const { code, output: output2 } = await runToEnd("ps", ["-ww", "-o", "uid=", "-o", "command=", "-p", String(pid)], { timeoutMs: 5e3 });
27225
+ const found = /^\s*(\d+)\s+(.*)$/m.exec(output2);
27226
+ if (code !== 0 || found === null) return void 0;
27227
+ return { uid: Number(found[1]), command: found[2] ?? "", cwd: processFolder(pid) };
27228
+ },
27229
+ alive(pid) {
27230
+ try {
27231
+ process.kill(pid, 0);
27232
+ return true;
27233
+ } catch (error62) {
27234
+ return error62.code === "EPERM";
27235
+ }
27236
+ },
27237
+ signal(pid, signal) {
27238
+ try {
27239
+ process.kill(pid, signal);
27240
+ } catch {
27241
+ }
27242
+ },
27243
+ run: (command, args, env, timeoutMs) => runToEnd(command, args, { env, timeoutMs }),
27244
+ launch({ command, args, env, cwd, log: log2 }) {
27245
+ const out = openSync2(log2, "a");
27246
+ try {
27247
+ const child = spawn2(command, args, { cwd, env, detached: true, stdio: ["ignore", out, out] });
27248
+ const exited = new Promise((done) => {
27249
+ child.once("exit", (code, signal) => done({ code, signal }));
27250
+ child.once("error", (error62) => {
27251
+ appendFileSync2(log2, `${error62.message}
27252
+ `);
27253
+ done({ code: null, signal: null });
27254
+ });
27255
+ });
27256
+ child.unref();
27257
+ return { pid: child.pid, exited };
27258
+ } finally {
27259
+ closeSync2(out);
27260
+ }
27261
+ },
27262
+ note(log2, words4) {
27263
+ appendFileSync2(log2, `${words4}
27264
+ `);
27265
+ },
27266
+ size(log2) {
27267
+ try {
27268
+ return statSync3(log2).size;
27269
+ } catch {
27270
+ return 0;
27271
+ }
27272
+ },
27273
+ since(log2, from) {
27274
+ try {
27275
+ const to = statSync3(log2).size;
27276
+ const start = Math.max(from, to - 64e3);
27277
+ const buffer = Buffer.alloc(Math.max(0, to - start));
27278
+ const fd = openSync2(log2, "r");
27279
+ try {
27280
+ readSync(fd, buffer, 0, buffer.length, start);
27281
+ } finally {
27282
+ closeSync2(fd);
27283
+ }
27284
+ return buffer.toString("utf8");
27285
+ } catch {
27286
+ return "";
27287
+ }
27288
+ },
27289
+ healthy: (url2) => new HermesClient({ url: url2, key: "" }, (input2, init) => fetch(input2, { ...init, signal: AbortSignal.timeout(5e3) })).healthy(),
27290
+ sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
27291
+ now: () => Date.now()
27292
+ };
27293
+ }
27294
+
27295
+ // src/profiles.ts
27296
+ var KEY_ENV = "API_SERVER_KEY";
27297
+ var RESERVED = /* @__PURE__ */ new Set(["hermes", "default", "test", "tmp", "root", "sudo"]);
27298
+ var CREATE_MS = 12e4;
27299
+ function freeProfile(root2, name2, taken) {
27300
+ const slug = name2.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 56).replace(/^-+|-+$/g, "") || "agent";
27301
+ for (let count = 1; ; count += 1) {
27302
+ if (count === 1 && RESERVED.has(slug)) continue;
27303
+ const profile = count === 1 ? slug : `${slug}-${count}`;
27304
+ if (!taken.includes(profile) && !existsSync9(join9(root2, "profiles", profile))) return profile;
27305
+ }
27306
+ }
27307
+ function readyProfile(home2, names, hook) {
27308
+ for (const file3 of ["MEMORY.md", "USER.md"]) rmSync4(join9(home2, "memories", file3), { force: true });
27309
+ if (envValue(home2, KEY_ENV) === void 0) appendEnv(home2, KEY_ENV, randomBytes2(32).toString("hex"));
27310
+ if (hook === void 0) return void 0;
27311
+ const path = join9(home2, "config.yaml");
27312
+ if (existsSync9(path)) {
27313
+ const theirs = hookUrl(hook.port, names.from).replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
27314
+ const entry = new RegExp(`^(\\s*(?:-\\s*)?url:\\s*["']?)${theirs}(["']?\\s*(?:#.*)?)$`, "m");
27315
+ const text = readFileSync9(path, "utf8");
27316
+ if (entry.test(text)) writeWhole(path, text.replace(entry, `$1${hookUrl(hook.port, names.profile)}$2`));
27317
+ }
27318
+ return addHook(home2, hook.port, names.profile, hook.secret);
27319
+ }
27320
+ function hermesProfiles(options) {
27321
+ return {
27322
+ async create(name2, from) {
27323
+ const args = ["profile", "create", name2, "--clone-from", from, "--no-alias"];
27324
+ const ran = await runHermes(options.system, options.env, args, CREATE_MS, options.log);
27325
+ if (ran.code !== 0) throw new Error(whyFrom(ran.output, ran.code).replace(/^Error:\s*/, ""));
27326
+ },
27327
+ async serve() {
27328
+ const result = await askGateway(options.root, "rescan-profiles");
27329
+ return typeof result?.["multiplex"] === "boolean" ? result["multiplex"] : void 0;
27330
+ }
27331
+ };
27332
+ }
27333
+ function controlSocketOf(root2) {
27334
+ const direct = join9(root2, "gateway.sock");
27335
+ if (existsSync9(direct)) return direct;
27336
+ try {
27337
+ const pointed = readFileSync9(join9(root2, "gateway.sock.path"), "utf8").trim();
27338
+ return pointed !== "" && existsSync9(pointed) ? pointed : void 0;
27339
+ } catch {
27340
+ return void 0;
27341
+ }
27342
+ }
27343
+ function askGateway(root2, verb, timeoutMs = 8e3) {
27344
+ const path = controlSocketOf(root2);
27345
+ if (path === void 0) return Promise.resolve(void 0);
27346
+ return new Promise((done) => {
27347
+ let heard = "";
27348
+ let over = false;
27349
+ const socket = createConnection(path);
27350
+ const finish = (line2) => {
27351
+ if (over) return;
27352
+ over = true;
27353
+ clearTimeout(timer);
27354
+ socket.destroy();
27355
+ done(line2 === void 0 ? void 0 : resultOf(line2));
27356
+ };
27357
+ const timer = setTimeout(() => finish(void 0), timeoutMs);
27358
+ socket.on("connect", () => socket.write(`${JSON.stringify({ verb, id: 1, protocol: 1 })}
27359
+ `));
27360
+ socket.on("data", (chunk) => {
27361
+ heard += String(chunk);
27362
+ const end2 = heard.indexOf("\n");
27363
+ if (end2 >= 0) finish(heard.slice(0, end2));
27364
+ });
27365
+ socket.on("end", () => finish(heard));
27366
+ socket.on("error", () => finish(void 0));
27367
+ });
27368
+ }
27369
+ function resultOf(line2) {
27370
+ try {
27371
+ const answer = JSON.parse(line2);
27372
+ return answer.ok === true && typeof answer.result === "object" && answer.result !== null ? answer.result : void 0;
27373
+ } catch {
27374
+ return void 0;
27375
+ }
27376
+ }
27377
+
27378
+ // src/bridge.ts
27379
+ var WANTS = [
27380
+ "agents",
27381
+ "new_spaces",
27382
+ "new_agents",
27383
+ "forks",
27384
+ "context_files",
27385
+ "models",
27386
+ "context_windows",
27387
+ "restarts"
27388
+ ];
27389
+ function settingsFor(active) {
27390
+ const bridge2 = () => {
27391
+ const there = active();
27392
+ if (there === void 0) throw new Refuse("The bridge is starting. Try again in a moment.");
27393
+ return there;
27394
+ };
27395
+ return {
27396
+ context: (edit) => bridge2().onContextEdit(edit),
27397
+ models: (change) => bridge2().onModelChange(change),
27398
+ contextWindows: {
27399
+ compact: async (ask2) => bridge2().onWindowCompact(ask2),
27400
+ change: async (ask2) => bridge2().onWindowChange(ask2)
27401
+ }
27402
+ };
27403
+ }
27404
+ function filesHere(project = false) {
27405
+ 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.`;
27406
+ }
27407
+ var FILES_HERE = filesHere();
27408
+ var MARKDOWN_HERE = "Hermes's note that this API server's replies are plain text does not hold for these runs either: Chips shows your answer as Markdown, so use headings, lists, bold, tables and code blocks where they help a reader.";
27409
+ function instructionsFor(folder2, project = false, rules = "") {
27410
+ const files = filesHere(project);
27411
+ const here2 = folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
27412
+ return [here2, MARKDOWN_HERE, rules].filter((part) => part !== "").join("\n\n");
27413
+ }
27414
+ function freeFolder(root2, name2, taken) {
27415
+ const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
27416
+ for (let count = 1; ; count += 1) {
27417
+ const folder2 = join10(root2, count === 1 ? slug : `${slug}-${count}`);
27418
+ if (!taken.includes(folder2)) return folder2;
27419
+ }
27420
+ }
27421
+ var ids = createIdGenerator();
27422
+ var QUIET_MS = 1500;
27423
+ var LOST_STREAM_MS = 12e4;
27424
+ var KEPT_FEEDBACK = 500;
27425
+ var KINDS = {
27426
+ terminal: "shell",
27427
+ process: "shell",
27428
+ execute_code: "shell",
27429
+ web_search: "search",
27430
+ search_files: "search",
27431
+ session_search: "search",
27432
+ web_extract: "web",
27433
+ read_file: "file",
27434
+ write_file: "edit",
27435
+ patch: "edit",
27436
+ vision_analyze: "image",
27437
+ image_generate: "image",
27438
+ memory: "memory",
27439
+ delegate_task: "handoff"
27440
+ };
27441
+ function keyOf(agent) {
27442
+ return `hermes:${(agent.address.profile ?? "default").toLowerCase()}:${agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
27443
+ }
27444
+ var Bridge = class {
27445
+ constructor(options) {
27446
+ this.options = options;
27447
+ this.log = options.log ?? (() => void 0);
27448
+ const { chips: chips2, hermesRoot: hermesRoot2 } = options;
27449
+ if (hermesRoot2 === void 0) return;
27450
+ this.contexts = new Contexts(
27451
+ () => this.roster(),
27452
+ {
27453
+ agent: (agentId2, copy) => chips2.settings.context(agentId2, copy),
27454
+ harness: (copy) => chips2.settings.harnessContext(copy)
27455
+ },
27456
+ (line2) => this.log(`context: ${line2}`),
27457
+ options.env ?? process.env
27458
+ );
27459
+ if (options.fills === void 0) return;
27460
+ const kept = options.state.fills ??= {};
27461
+ this.fills = new Fills({
27462
+ whose: (sessionId) => this.whoseSession(sessionId),
27463
+ profileOf: (agentId2) => {
27464
+ const profile = this.profileFor(agentId2);
27465
+ return profile === void 0 ? void 0 : { profile: profile.name, home: profile.home };
27466
+ },
27467
+ secretFor: (profile) => envValue(profileHome(hermesRoot2, profile), SECRET_ENV) ?? kept.secret ?? options.fills?.secret,
27468
+ send: (agentId2, copy) => chips2.settings.contextWindow(agentId2, copy),
27469
+ kept,
27470
+ save: () => options.saveState(options.state),
27471
+ log: (line2) => this.log(`context window: ${line2}`),
27472
+ sendEveryMs: options.fillsEveryMs
26968
27473
  });
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();
26973
27474
  }
27475
+ options;
27476
+ agents = [];
27477
+ rooms = /* @__PURE__ */ new Map();
27478
+ /** Every Chips agent of the harness's, configured here or not. */
27479
+ ours = /* @__PURE__ */ new Set();
27480
+ /** The harness's agents' names, by Chips id, for the folders named after them. */
27481
+ names = /* @__PURE__ */ new Map();
27482
+ /** The agents made in Chips with a profile of their own, by Chips id (Chips ADR 0241). */
27483
+ own = /* @__PURE__ */ new Map();
27484
+ /** The feedback events answered with a turn, the newest `KEPT_FEEDBACK`. */
27485
+ lookedAt = /* @__PURE__ */ new Set();
26974
27486
  /**
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.
27487
+ * The agents made, the spaces claimed and the copies made, one after another: a message for an
27488
+ * agent not settled yet waits for them (see `runnerOf`).
26984
27489
  */
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;
27490
+ making = Promise.resolve();
27491
+ log;
27492
+ health;
27493
+ /** The harness's agents in Chips as last listed: whose context and window copies are sent. */
27494
+ listed = [];
27495
+ /** The agents' context files (Chips ADR 0134), when the bridge knows where Hermes keeps them. */
27496
+ contexts;
27497
+ contextTimer;
27498
+ /** The agents' context windows (Chips ADR 0146), when Hermes's webhook has somewhere to reach. */
27499
+ fills;
27500
+ /** The port Hermes's webhook reaches the bridge on, once it listens. */
27501
+ fillsPort;
27502
+ toldNoReadings = false;
27503
+ /** Whether the log has said once that a model copy wasn't sent. */
27504
+ toldModels = false;
27505
+ /** While Hermes's gateway restarts (Chips ADR 0094), the health watch says nothing: the restart says how it went. */
27506
+ held = false;
27507
+ /** What went wrong in the last restart, said in place of the watch's own words while Hermes doesn't answer. */
27508
+ why;
27509
+ /** Register the agents, start hearing what is said to them, and say how their gateways are. */
27510
+ async start() {
27511
+ const { chips: chips2 } = this.options;
27512
+ const known = await chips2.agents.list();
27513
+ for (const [index, config3] of this.options.agents.entries()) {
27514
+ const key = keyOf(config3);
27515
+ const registered = index === 0 ? await this.seatMain(config3, known) : await chips2.agents.register({ key, name: config3.name });
27516
+ const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
27517
+ this.agents.push({ config: config3, key, client, agentId: registered.agentId, up: true });
26993
27518
  }
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;
27519
+ for (const [agentId2, profile] of Object.entries(this.options.state.made ?? {})) {
27520
+ const running = this.runningOn(agentId2, profile, known.find((agent) => agent.agentId === agentId2)?.name ?? profile);
27521
+ if (running !== void 0) this.own.set(agentId2, running);
26998
27522
  }
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();
27523
+ this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
27524
+ chips2.on("message.new", (message) => void this.hear(message));
27525
+ chips2.on("agent.asked", (asked) => {
27526
+ this.making = this.making.then(() => this.make(asked)).catch((error62) => this.log(`a new agent went wrong: ${messageOf3(error62)}`));
27527
+ });
27528
+ chips2.on("agent.fork", (fork) => {
27529
+ this.making = this.making.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf3(error62)}`));
27530
+ });
27531
+ chips2.onSpaceAsked((asked) => {
27532
+ this.making = this.making.then(() => this.claim(asked)).catch((error62) => this.log(`a space from the + went wrong: ${messageOf3(error62)}`));
27533
+ });
27534
+ chips2.on("feedback", (feedback2) => {
27535
+ this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf3(error62)}`));
27536
+ });
27537
+ chips2.on("agent.attached", () => void this.agentsMoved());
27538
+ chips2.on("agent.released", () => void this.agentsMoved());
27539
+ await this.sayHowItIs();
27540
+ this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
27541
+ this.health.unref();
27542
+ await this.startSettings();
27018
27543
  }
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;
27544
+ /** Hold the health watch's words while Hermes's gateway restarts, and let them go after. */
27545
+ hold(held) {
27546
+ this.held = held;
27026
27547
  }
27027
27548
  /**
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).
27549
+ * How a restart went (Chips ADR 0094): fine as the gateways now are, or its words, which stand
27550
+ * while Hermes doesn't answer. The health watch speaks for itself again after.
27033
27551
  */
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;
27552
+ async restarted(trouble) {
27553
+ this.held = false;
27554
+ this.why = trouble;
27555
+ if (trouble === void 0) return this.sayHowItIs();
27556
+ await this.options.chips.reportTrouble(trouble).catch(() => void 0);
27557
+ }
27558
+ stop() {
27559
+ if (this.health !== void 0) clearInterval(this.health);
27560
+ if (this.contextTimer !== void 0) clearInterval(this.contextTimer);
27561
+ void this.fills?.close();
27046
27562
  }
27563
+ // ------------------------------------------------------------------ an agent's page
27047
27564
  /**
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.
27565
+ * The agents' models, context and context windows, sent at start and kept current: the models
27566
+ * again when an agent comes or goes, the files read again every so often and after each turn, a
27567
+ * window copy on each reading Hermes sends.
27051
27568
  */
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;
27569
+ async startSettings() {
27570
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
27571
+ await this.sendModels();
27572
+ if (this.contexts === void 0) return;
27573
+ await this.contexts.refresh();
27574
+ this.contextTimer = setInterval(() => void this.contexts?.refresh(), this.options.contextEveryMs ?? 15e3);
27575
+ this.contextTimer.unref();
27576
+ const fills2 = this.fills;
27577
+ const asked = this.options.fills;
27578
+ if (fills2 === void 0 || asked === void 0) return;
27579
+ const kept = this.options.state.fills ??= {};
27056
27580
  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;
27581
+ this.fillsPort = await fills2.listen(asked.port);
27582
+ if (kept.port !== this.fillsPort) {
27583
+ kept.port = this.fillsPort;
27584
+ this.options.saveState(this.options.state);
27585
+ }
27061
27586
  } catch (error62) {
27062
- this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf2(error62)}`);
27063
- return void 0;
27587
+ 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
27588
  }
27589
+ await fills2.sendAll();
27065
27590
  }
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 };
27591
+ /** The address Hermes's webhook POSTs a profile's readings to, once the bridge listens. */
27592
+ hookUrl(profile = "default") {
27593
+ return this.fillsPort === void 0 ? void 0 : hookUrl(this.fillsPort, profileName(profile));
27594
+ }
27595
+ /** The harness's agents listed again, the copies of any gone forgotten, and the context and models sent. */
27596
+ async agentsMoved() {
27597
+ const before = this.listed;
27598
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
27599
+ const now = new Set(this.active().map((agent) => agent.agentId));
27600
+ for (const gone of before) {
27601
+ if (now.has(gone.agentId)) continue;
27602
+ this.options.chips.settings.forget(gone.agentId);
27603
+ this.fills?.forget(gone.agentId);
27084
27604
  }
27605
+ await this.contexts?.refresh();
27606
+ await this.sendModels();
27607
+ }
27608
+ /** The harness's agents that read anything now: the active ones listed. */
27609
+ active() {
27610
+ return this.listed.filter((agent) => agent.state === "active");
27611
+ }
27612
+ /** A profile as the context needs it: its name and its home under Hermes's root. */
27613
+ profileOfRunning(agent) {
27614
+ const root2 = this.options.hermesRoot;
27615
+ if (root2 === void 0) return void 0;
27616
+ return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
27617
+ }
27618
+ /** The profile an agent runs on: its own, a copy's original's, else the first (as `profileOf` picks). */
27619
+ profileFor(agentId2) {
27620
+ const running = this.runningFor(agentId2);
27621
+ return running === void 0 ? void 0 : this.profileOfRunning(running);
27085
27622
  }
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);
27623
+ /** What answers for an agent: its own profile, a copy's original's, else the first. */
27624
+ runningFor(agentId2) {
27625
+ const own2 = this.agents.find((agent) => agent.agentId === agentId2) ?? this.own.get(agentId2);
27626
+ const copied = this.options.state.profiles?.[agentId2];
27627
+ return own2 ?? (copied === void 0 ? void 0 : this.byKey(copied)) ?? this.agents[0];
27096
27628
  }
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
- }
27629
+ /** A profile the bridge speaks to, by its key: a configured agent's, or a made agent's Chips id. */
27630
+ byKey(key) {
27631
+ return this.agents.find((agent) => agent.key === key) ?? this.own.get(key);
27110
27632
  }
27111
27633
  /**
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.
27634
+ * Who reads what: each active agent with its profile, and the first profile, whose SOUL.md each
27635
+ * new agent starts from (and whose every file each new agent reads, where none gets a profile of
27636
+ * its own).
27116
27637
  */
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);
27638
+ roster() {
27639
+ const readers = [];
27640
+ const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
27641
+ for (const agentId2 of ids2) {
27642
+ if (this.listed.some((agent) => agent.agentId === agentId2 && agent.state !== "active")) continue;
27643
+ const profile = this.profileFor(agentId2);
27644
+ if (profile !== void 0) readers.push({ agentId: agentId2, profile });
27126
27645
  }
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] }
27646
+ const first = this.agents[0];
27647
+ return {
27648
+ readers,
27649
+ first: first === void 0 ? void 0 : this.profileOfRunning(first),
27650
+ ownProfiles: this.options.profiles !== void 0
27137
27651
  };
27138
- await this.hear(message, { goal: lookingAgain(note), sayWho: false });
27139
27652
  }
27653
+ /** The agent a Hermes session the bridge keeps is, with its profile: nothing for any other. */
27654
+ whoseSession(sessionId) {
27655
+ const entry = Object.entries(this.options.state.sessions).find(([, session]) => session === sessionId);
27656
+ if (entry === void 0) return void 0;
27657
+ const who = entry[0].slice(0, entry[0].lastIndexOf("|"));
27658
+ const agentId2 = this.agents.find((agent) => agent.key === who)?.agentId ?? who;
27659
+ const profile = this.profileFor(agentId2);
27660
+ return profile === void 0 ? void 0 : { agentId: agentId2, profile: profile.name, home: profile.home };
27661
+ }
27662
+ /** A person's edit of a context file from Chips (Chips ADR 0134), saved and answered. */
27663
+ async onContextEdit(frame) {
27664
+ if (this.contexts === void 0) throw new Refuse("This bridge reads no context files.");
27665
+ if (frame.agent_id !== void 0 && !this.listed.some((agent) => agent.agentId === frame.agent_id)) {
27666
+ this.listed = await this.options.chips.agents.list().catch(() => this.listed);
27667
+ }
27668
+ return this.contexts.edit(frame);
27669
+ }
27670
+ // ------------------------------------------------------------------ an agent's model
27140
27671
  /**
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.
27672
+ * The model a person picked for an agent, as its copy names it: a choice's id, or the model its
27673
+ * config says each of its profile's runs asks for; nothing for Hermes's default.
27143
27674
  */
27144
- async runnerOf(taker) {
27145
- if (taker !== this.options.chips.harnessId && !this.ours.has(taker)) await this.copying;
27146
- return this.profileOf(taker);
27675
+ ownModel(agentId2, running) {
27676
+ const picked = this.options.state.models?.[agentId2];
27677
+ return picked === null ? void 0 : picked ?? running.config.model;
27678
+ }
27679
+ /** What a turn's runs ask Hermes for: the agent's own model, else nothing, and the profile's runs. */
27680
+ runModel(turn) {
27681
+ const picked = this.options.state.models?.[turn.who];
27682
+ if (picked === null) return {};
27683
+ if (picked !== void 0) return runModelOf(picked);
27684
+ return turn.agent.config.model === void 0 ? {} : { model: turn.agent.config.model };
27147
27685
  }
27148
27686
  /**
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.
27687
+ * Each agent's model, sent to Chips (Chips ADR 0141): the models Hermes has for its profile, read
27688
+ * once a profile, with the one it runs on. One whose Hermes doesn't answer is sent none, and the
27689
+ * log says so once.
27152
27690
  */
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);
27691
+ async sendModels() {
27692
+ const { chips: chips2 } = this.options;
27693
+ const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
27694
+ const read = /* @__PURE__ */ new Map();
27695
+ for (const agentId2 of ids2) {
27696
+ const running = this.runningFor(agentId2);
27697
+ if (running === void 0) continue;
27698
+ try {
27699
+ let options = read.get(running);
27700
+ if (options === void 0) {
27701
+ options = running.client.modelOptions();
27702
+ read.set(running, options);
27703
+ }
27704
+ const copy = copyOf(await options, this.ownModel(agentId2, running));
27705
+ if (copy !== void 0) await chips2.settings.model(agentId2, copy);
27706
+ } catch (error62) {
27707
+ if (!this.toldModels) this.log(`models: none sent for ${running.config.name}: ${messageOf3(error62)}`);
27708
+ this.toldModels = true;
27164
27709
  }
27165
27710
  }
27166
- return taker === this.options.chips.harnessId || this.ours.has(taker) ? this.agents[0] : void 0;
27167
27711
  }
27168
27712
  /**
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.
27713
+ * A person picked an agent's model (Chips ADR 0141): one Hermes has for its profile, kept as the
27714
+ * model each of its runs asks for from now on, or Hermes's default (`null`), the profile's own.
27715
+ * The model it runs on already changes nothing. Hermes says no levels of effort, so none is taken.
27171
27716
  */
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;
27717
+ async onModelChange(frame) {
27718
+ const { state: state2 } = this.options;
27719
+ const agentId2 = frame.agent_id;
27720
+ const ours2 = this.ours.has(agentId2) || this.agents.some((agent) => agent.agentId === agentId2) || this.own.has(agentId2);
27721
+ const running = ours2 ? this.runningFor(agentId2) : void 0;
27722
+ if (running === void 0) throw new Refuse(`${agentId2} isn't one of this harness's agents.`);
27723
+ let options;
27225
27724
  try {
27226
- session = await this.sessionFor(room);
27725
+ options = await running.client.modelOptions();
27227
27726
  } catch (error62) {
27228
- await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf2(error62)}`, true);
27229
- return;
27727
+ throw new Refuse(`Hermes didn't say which models it has: ${messageOf3(error62)}`);
27230
27728
  }
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);
27729
+ const now = copyOf(options, this.ownModel(agentId2, running));
27730
+ if (now === void 0) throw new Refuse("Hermes has no model for this agent's profile.");
27731
+ if (frame.model === now.model) return now;
27732
+ if (frame.model !== null && !now.choices.some((choice) => choice.id === frame.model)) {
27733
+ throw new Refuse(`Hermes has no ${frame.model} for this agent's profile.`, now);
27249
27734
  }
27735
+ state2.models = { ...state2.models, [agentId2]: frame.model };
27736
+ this.options.saveState(state2);
27737
+ const name2 = await this.nameOf(agentId2, running.config.name);
27738
+ this.log(`${name2} runs on ${frame.model ?? `Hermes's default (${now.default?.model ?? "its profile's"})`}, from Chips`);
27739
+ return copyOf(options, this.ownModel(agentId2, running)) ?? now;
27250
27740
  }
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;
27741
+ /** A person's Compact now (Chips ADR 0146): Hermes can't be asked to compact from outside a run. */
27742
+ onWindowCompact(frame) {
27743
+ throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
27744
+ }
27745
+ /** A person's limit (Chips ADR 0146): Hermes sets it in its profile's config, for all its agents. */
27746
+ onWindowChange(frame) {
27747
+ const profile = this.profileFor(frame.agent_id);
27748
+ throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
27749
+ }
27750
+ /** The Main agent, the first registered, once started: who says the harness's first words. */
27751
+ get mainAgentId() {
27752
+ return this.agents[0]?.agentId;
27753
+ }
27754
+ /**
27755
+ * The Main agent: the one Chips made for the harness when it joined, keyed by the harness's own
27756
+ * id, registered as Main (Chips ADR 0183), so it is called what the config says and has no
27757
+ * second beside it. It goes home, and a Main of ours from before that, out of its room now, is
27758
+ * archived, which gives its name up. With no such agent (a harness from before them), the Main
27759
+ * Chips has is kept: a harness has one at most.
27760
+ */
27761
+ async seatMain(config3, known) {
27762
+ const { chips: chips2 } = this.options;
27763
+ const before = known.find((agent) => agent.lead);
27764
+ const made = known.some((agent) => agent.key === chips2.harnessId);
27765
+ if (!made && before !== void 0) {
27766
+ await this.goHome(before, known);
27767
+ return before;
27259
27768
  }
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);
27769
+ const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
27770
+ await this.goHome(main2, known);
27771
+ if (before !== void 0 && before.agentId !== main2.agentId) {
27772
+ await chips2.agents.register({ key: before.key, name: before.name, state: "archived" }).catch((error62) => this.log(`${before.name} stays: ${String(error62)}`));
27267
27773
  }
27774
+ return main2;
27268
27775
  }
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;
27776
+ /** The Main agent into the harness's own room, in the place of whichever of ours is there. */
27777
+ async goHome(main2, known) {
27778
+ const { chips: chips2, home: home2 } = this.options;
27779
+ if (home2 === void 0 || main2.spaces.includes(home2)) return;
27780
+ const there = known.find((agent) => agent.agentId !== main2.agentId && agent.spaces.includes(home2));
27781
+ 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
27782
  }
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;
27783
+ /**
27784
+ * A person asked for a new agent in a room (Chips ADR 0108): one of the harness's, keyed by the
27785
+ * ask so an ask heard twice makes one, registered for the room, which Chips puts it in while the
27786
+ * room waits. It answers through a Hermes profile of its own (`ownProfile`).
27787
+ */
27788
+ async make(asked) {
27789
+ const { chips: chips2 } = this.options;
27790
+ if (asked.harness_id !== chips2.harnessId || asked.space_id === void 0) return;
27791
+ const agent = await chips2.agents.register({
27792
+ key: `hermes:asked:${asked.id}`,
27793
+ name: asked.name,
27794
+ madeFor: asked.id,
27795
+ spaceId: asked.space_id
27796
+ });
27797
+ this.names.set(agent.agentId, agent.name);
27798
+ this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
27799
+ await this.ownProfile(agent.agentId, agent.name);
27800
+ this.ours.add(agent.agentId);
27801
+ void this.agentsMoved();
27292
27802
  }
27293
27803
  /**
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.
27804
+ * A Hermes profile of its own for an agent made in Chips (Chips ADR 0241), so it remembers only
27805
+ * its own conversations: named after the agent, cloned from the Main agent's profile so it starts
27806
+ * with its model, keys, tools, skills and SOUL.md, its memory empty (`profiles.ts`). The gateway
27807
+ * is asked to serve it now; one that serves only the profile it started with, as Hermes's does
27808
+ * when it started with one, is restarted once. Where Hermes doesn't make one, the agent runs on
27809
+ * the first profile, in a session of its own, and the log says why.
27297
27810
  */
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);
27811
+ async ownProfile(agentId2, name2) {
27812
+ const { profiles, hermesRoot: root2, state: state2 } = this.options;
27813
+ const main2 = this.agents[0];
27814
+ if (profiles === void 0 || root2 === void 0 || main2 === void 0 || this.own.has(agentId2)) return;
27815
+ const from = profileName(main2.config.address.profile);
27816
+ const profile = freeProfile(root2, name2, Object.values(state2.made ?? {}));
27817
+ const home2 = profileHome(root2, profile);
27818
+ const secret = state2.fills?.secret ?? this.options.fills?.secret;
27819
+ try {
27820
+ await profiles.create(profile, from);
27821
+ const port = this.fillsPort;
27822
+ const hook = readyProfile(home2, { from, profile }, port === void 0 || secret === void 0 ? void 0 : { port, secret });
27823
+ if (hook?.state === "by_hand") this.log(`context window: ${hook.path} was left as it is: ${hook.why}. Add these lines to it:
27824
+ ${hook.lines}`);
27825
+ } catch (error62) {
27826
+ this.log(`${name2} runs on the ${from} profile, in a session of its own: Hermes didn't make it one (${messageOf3(error62)})`);
27303
27827
  return;
27304
27828
  }
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
- }
27829
+ state2.made = { ...state2.made, [agentId2]: profile };
27830
+ this.options.saveState(state2);
27831
+ const running = this.runningOn(agentId2, profile, name2);
27832
+ if (running !== void 0) this.own.set(agentId2, running);
27833
+ this.log(`${name2} has a Hermes profile of its own, ${profile}, cloned from ${from}`);
27834
+ const served = await profiles.serve().catch(() => void 0);
27835
+ if (served === false) {
27836
+ 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`);
27837
+ await this.options.restartGateway?.();
27838
+ } else if (served === void 0) {
27839
+ this.log(`Hermes's gateway didn't answer on its control socket: it serves ${profile} once it looks again, or starts again`);
27312
27840
  }
27313
- turn.next.push(text);
27314
27841
  }
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
- }
27842
+ /**
27843
+ * A profile made for an agent, as the bridge reaches it: under `/p/<profile>` of the gateway the
27844
+ * Main agent is reached at, with the key in the profile's own `.env`.
27845
+ */
27846
+ runningOn(agentId2, profile, name2) {
27847
+ const main2 = this.agents[0];
27848
+ const root2 = this.options.hermesRoot;
27849
+ if (main2 === void 0 || root2 === void 0) return void 0;
27850
+ const home2 = profileHome(root2, profile);
27851
+ const config3 = {
27852
+ name: name2,
27853
+ address: { url: main2.config.address.url, profile, key: envValue(home2, KEY_ENV) ?? "" },
27854
+ keyEnv: KEY_ENV,
27855
+ keyFrom: join10(home2, ".env")
27856
+ };
27857
+ const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
27858
+ return { config: config3, key: agentId2, client, agentId: agentId2, up: true };
27343
27859
  }
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);
27860
+ /**
27861
+ * A space a person asked the harness for from the + (Chips ADR 0241), as the ACP bridge takes
27862
+ * one: claimed under a key made from the space, so a space handed over twice is claimed once,
27863
+ * and named from its first words. Chips makes the agent of that key and puts it in the space; it
27864
+ * gets a Hermes profile of its own (`ownProfile`). It answers the first words, which are the
27865
+ * ask's and aren't heard again.
27866
+ */
27867
+ async claim(asked) {
27868
+ const { chips: chips2 } = this.options;
27869
+ const { spaceId, firstMessage } = asked;
27870
+ const key = `hermes:space:${spaceId}`;
27871
+ await chips2.claimSpace(spaceId, key, titleFrom(firstMessage.text));
27872
+ const agent = (await chips2.agents.list()).find((each) => each.key === key);
27873
+ if (agent === void 0) {
27874
+ this.log(`claimed ${spaceId}, and Chips put no agent there`);
27875
+ return;
27876
+ }
27877
+ this.names.set(agent.agentId, agent.name);
27878
+ this.log(`${agent.name} claimed ${spaceId}`);
27879
+ await this.ownProfile(agent.agentId, agent.name);
27880
+ this.ours.add(agent.agentId);
27881
+ void this.agentsMoved();
27882
+ const message = {
27883
+ type: "message.new",
27884
+ space_id: spaceId,
27885
+ message_id: firstMessage.messageId,
27886
+ text: firstMessage.text,
27887
+ from: { participant_id: asked.askedBy, kind: "person", display_name: await this.whoSaid(asked), role: "member" },
27888
+ for: [agent.agentId]
27889
+ };
27890
+ void this.hear(message);
27350
27891
  }
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);
27892
+ /** Who said a space's first words, from its lines. */
27893
+ async whoSaid(asked) {
27894
+ const context = await this.options.chips.roomContext(asked.spaceId, { limit: 20 }).catch(() => void 0);
27895
+ return context?.lines.find((line2) => line2.message_id === asked.firstMessage.messageId)?.from.name ?? "Someone";
27367
27896
  }
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);
27897
+ /**
27898
+ * A person asked for a copy of one of the harness's agents in another room (ADR 0087, Fork): the
27899
+ * agent's session in the room it is in, else the last it had, forked in Hermes, is the copy's
27900
+ * session in the room asked. The copy answers through its original's profile, whose Hermes has
27901
+ * that session (Hermes forks a session only within its profile, so a copy gets no profile of its
27902
+ * own: Chips ADR 0241), and works in its original's folder. It is registered with `forkedFrom` for the
27903
+ * room, which Chips puts it in while the room waits. The fork and the copy are keyed by the ask,
27904
+ * so an ask heard twice forks once (Hermes refuses the id again) and registers the same copy. An
27905
+ * agent that never spoke is copied with nothing to remember. A turn still going is not in the
27906
+ * copy: Hermes copies what the session holds. Nothing is made for an ask the room has stopped
27907
+ * waiting for (`FORK_WAIT_MS`), nor for an agent that isn't the harness's.
27908
+ */
27909
+ async copy(fork) {
27910
+ const { chips: chips2, state: state2 } = this.options;
27911
+ const spaceId = fork.space_id;
27912
+ if (spaceId === void 0) return;
27913
+ const late = Date.now() - Date.parse(fork.ts);
27914
+ if (late > FORK_WAIT_MS) {
27915
+ this.log(`no copy of ${fork.agent_id}: asked ${Math.round(late / 1e3)} s ago, and the room waits a minute`);
27916
+ return;
27380
27917
  }
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);
27918
+ const original = await this.profileOf(fork.agent_id);
27919
+ if (original === void 0) {
27920
+ this.log(`no copy of ${fork.agent_id}: it isn't one of this harness's agents`);
27921
+ return;
27394
27922
  }
27923
+ const name2 = await this.nameOf(fork.agent_id, original.config.name);
27924
+ const key = `hermes:fork:${fork.id}`;
27925
+ const who = fork.agent_id === original.agentId ? original.key : fork.agent_id;
27926
+ const listed = await chips2.agents.list().catch(() => []);
27927
+ const room = listed.find((agent) => agent.agentId === fork.agent_id)?.room ?? void 0;
27928
+ const from = (room === void 0 ? void 0 : state2.sessions[`${who}|${room}`]) ?? Object.entries(state2.sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
27929
+ if (from === void 0) this.log(`${name2} never spoke: its copy remembers nothing`);
27930
+ const session = from === void 0 ? void 0 : await original.client.forkSession(from, { id: `chips-fork-${fork.id}`, title: `Chips space ${spaceId} \xB7 ${key}` });
27931
+ const folder2 = this.folderOf(original, fork.agent_id, name2);
27932
+ const copy = await chips2.agents.register({ key, name: forkName(name2), forkedFrom: fork.agent_id, spaceId });
27933
+ if (session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] ??= session;
27934
+ state2.profiles = { ...state2.profiles, [copy.agentId]: original.key };
27935
+ if (folder2 !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: folder2 };
27936
+ this.options.saveState(state2);
27937
+ this.ours.add(copy.agentId);
27938
+ this.names.set(copy.agentId, copy.name);
27939
+ const away = copy.attached === false && !copy.spaces.includes(spaceId);
27940
+ this.log(`${copy.name}, a copy of ${name2}, made${away ? `, not in the room: ${copy.why ?? "too late"}` : ""}`);
27941
+ void this.agentsMoved();
27395
27942
  }
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 });
27943
+ /** An agent's name, from Chips's list when it isn't known yet. */
27944
+ async nameOf(agentId2, otherwise) {
27945
+ if (!this.names.has(agentId2)) {
27946
+ const listed = await this.options.chips.agents.list().catch(() => []);
27947
+ for (const agent of listed) this.names.set(agent.agentId, agent.name);
27473
27948
  }
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();
27949
+ return this.names.get(agentId2) ?? otherwise;
27950
+ }
27951
+ /**
27952
+ * The folder an agent works in and sends its files from: the one the config names for them all
27953
+ * (`folder`, as it was), else one of its own under `folders`, named after it the first time it
27954
+ * takes a turn and kept, so a rename keeps it. Every agent once shared one folder, and a team's
27955
+ * files overwrote a one-agent room's game (Chips TT-16); and the one folder had to be Hermes's
27956
+ * own working folder, which a plain Hermes doesn't set (the setup audit, 17 October 2026).
27957
+ */
27958
+ folderOf(agent, agentId2, name2) {
27959
+ if (agent.config.folder !== void 0) return agent.config.folder;
27960
+ const { folders: root2, state: state2 } = this.options;
27961
+ if (root2 === void 0) return void 0;
27962
+ const kept = state2.folders?.[agentId2];
27963
+ const folder2 = kept ?? freeFolder(root2, name2, Object.values(state2.folders ?? {}));
27964
+ mkdirSync5(folder2, { recursive: true });
27965
+ if (kept === void 0) {
27966
+ state2.folders = { ...state2.folders, [agentId2]: folder2 };
27967
+ this.options.saveState(state2);
27968
+ }
27969
+ return folder2;
27479
27970
  }
27480
27971
  /**
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.
27972
+ * The room's project for a turn (Chips ADR 0221): the agent's copy, in its folder, brought up to
27973
+ * date, and what the turn is told of it. None for an agent a person pointed at a folder of their
27974
+ * own, nor where this machine has no git for it.
27484
27975
  */
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);
27976
+ async projectFor(turn, spaceId, agentId2, name2) {
27977
+ if (this.options.projects !== true || turn.agent.config.folder !== void 0) return void 0;
27978
+ const folder2 = turn.folder;
27979
+ if (folder2 === void 0) return void 0;
27980
+ try {
27981
+ const project = this.options.chips.project({ spaceId, agentId: agentId2, name: name2, folder: folder2, log: (line2) => this.log(line2) });
27982
+ const facts = await project.pull();
27983
+ turn.project = { copy: project, name: name2 };
27984
+ return facts;
27985
+ } catch (error62) {
27986
+ this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf3(error62)}`);
27987
+ return void 0;
27507
27988
  }
27508
27989
  }
27509
27990
  /**
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.
27991
+ * What the turn changed in its copy, sent to the room's project before its words are posted: the
27992
+ * commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one; else, when
27993
+ * the change didn't go, the line the reply says so in (Chips AP-6).
27514
27994
  */
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);
27995
+ async sendProject(turn, words4) {
27996
+ if (turn.project === void 0) return {};
27997
+ const { copy, name: name2 } = turn.project;
27998
+ try {
27999
+ const pushed = await copy.push({ words: words4 });
28000
+ this.log(`${name2}'s project: ${pushedInWords(pushed)}`);
28001
+ if (pushed.state === "sent") return { change: pushed.commit };
28002
+ const note = heldNote(pushed, name2);
28003
+ return note === void 0 ? {} : { note };
28004
+ } catch (error62) {
28005
+ this.log(`${name2}'s project couldn't be sent: ${messageOf3(error62)}`);
28006
+ const note = heldNote({ state: "failed", why: messageOf3(error62) }, name2);
28007
+ return note === void 0 ? {} : { note };
27535
28008
  }
27536
- turn.open.clear();
27537
28009
  }
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();
28010
+ /** Trouble while a profile's gateway doesn't answer, and fine once all do (ADR 0083); nothing while a restart runs. */
28011
+ async sayHowItIs() {
28012
+ if (this.held) return;
28013
+ for (const agent of this.agents) agent.up = await agent.client.healthy();
28014
+ if (this.held) return;
28015
+ const down = this.agents.filter((agent) => !agent.up);
28016
+ if (down.length === 0) this.why = void 0;
28017
+ const chips2 = this.options.chips;
28018
+ const said2 = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(this.why ?? `Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
28019
+ await said2.catch(() => void 0);
27541
28020
  }
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;
28021
+ // ------------------------------------------------------------------ what is said to it
28022
+ /** A message for some of its agents: each takes a turn in its room, one after another there. */
28023
+ async hear(message, how = {}) {
28024
+ const spaceId = message.space_id;
28025
+ if (spaceId === void 0) return;
28026
+ const harnessId = this.options.chips.harnessId;
28027
+ for (const taker of message.for ?? [harnessId]) {
28028
+ const agent = await this.runnerOf(taker);
28029
+ if (agent === void 0) continue;
28030
+ const room = this.roomOf(agent, taker, spaceId);
28031
+ room.queue.push({ message, speakerId: taker === harnessId ? void 0 : taker, ...how });
28032
+ void this.pump(room);
28033
+ }
27593
28034
  }
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;
28035
+ /**
28036
+ * "Not right?" on one of its agents' replies (Chips ADR 0200): a person's note on it, or a bare
28037
+ * 👎 from an older phone, is a turn of the agent that said it, in its session in that room and
28038
+ * in line with the room's other turns, to look again and answer in its place. Any other
28039
+ * reaction is the person's own. Once per event, however often it is heard.
28040
+ */
28041
+ async notRight(feedback2) {
28042
+ const { space_id: spaceId, message_id: messageId } = feedback2;
28043
+ const note = feedback2.kind === "correction" ? (feedback2.text ?? "").trim() : void 0;
28044
+ const flagged = note === void 0 ? feedback2.kind === "reaction" && feedback2.reaction === "\u{1F44E}" : note !== "";
28045
+ if (spaceId === void 0 || !messageId || !flagged || this.lookedAt.has(feedback2.id)) return;
28046
+ this.lookedAt.add(feedback2.id);
28047
+ for (const old of this.lookedAt) {
28048
+ if (this.lookedAt.size <= KEPT_FEEDBACK) break;
28049
+ this.lookedAt.delete(old);
28050
+ }
28051
+ const context = await this.options.chips.roomContext(spaceId, { limit: 200 }).catch(() => void 0);
28052
+ const line2 = context?.lines.find((each) => each.message_id === messageId);
28053
+ const message = {
28054
+ type: "message.new",
28055
+ space_id: spaceId,
28056
+ // An id of its own, so nothing of the turn is taken for the reply's.
28057
+ message_id: messageIdOf(feedback2.id),
28058
+ text: lookAgain(feedback2.from.display_name, line2?.text, note),
28059
+ from: feedback2.from,
28060
+ ...line2 === void 0 ? {} : { for: [line2.from.id] }
28061
+ };
28062
+ await this.hear(message, { goal: lookingAgain(note), sayWho: false });
27631
28063
  }
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);
28064
+ /**
28065
+ * Which profile answers for a Chips agent, as `profileOf` says. An agent not settled may be one
28066
+ * being made, or a copy, which is waited for, so that its first message is answered on its own
28067
+ * profile, or in the session it was forked.
28068
+ */
28069
+ async runnerOf(taker) {
28070
+ const settled = taker === this.options.chips.harnessId || this.own.has(taker) || this.agents.some((agent) => agent.agentId === taker);
28071
+ if (!settled) await this.making;
28072
+ return this.profileOf(taker);
28073
+ }
28074
+ /**
28075
+ * Which profile answers for a Chips agent: its own, configured or made for it; a copy's
28076
+ * original's; or the first for the harness itself and for any other of the harness's — its
28077
+ * default agent, one made before profiles were — read afresh once when a name is new.
28078
+ */
28079
+ async profileOf(taker) {
28080
+ const own2 = this.agents.find((agent) => agent.agentId === taker) ?? this.own.get(taker);
28081
+ if (own2 !== void 0) return own2;
28082
+ const copied = this.options.state.profiles?.[taker];
28083
+ const original = copied === void 0 ? void 0 : this.byKey(copied);
28084
+ if (original !== void 0) return original;
28085
+ if (taker !== this.options.chips.harnessId && !this.ours.has(taker)) {
28086
+ const listed = await this.options.chips.agents.list().catch(() => []);
28087
+ for (const agent of listed) {
28088
+ this.ours.add(agent.agentId);
28089
+ this.names.set(agent.agentId, agent.name);
28090
+ }
28091
+ }
28092
+ return taker === this.options.chips.harnessId || this.ours.has(taker) ? this.agents[0] : void 0;
27657
28093
  }
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;
28094
+ /**
28095
+ * The session a turn is taken in: the profile's own in the room, for its agent or the harness
28096
+ * itself, and one of its own for any other of the harness's, so two in a room are two.
28097
+ */
28098
+ roomOf(agent, taker, spaceId) {
28099
+ const own2 = taker === agent.agentId || taker === this.options.chips.harnessId;
28100
+ const id2 = `${own2 ? agent.key : taker}|${spaceId}`;
28101
+ let room = this.rooms.get(id2);
28102
+ if (room === void 0) {
28103
+ room = { id: id2, agent, spaceId, queue: [], busy: false };
28104
+ this.rooms.set(id2, room);
28105
+ }
28106
+ return room;
27667
28107
  }
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 };
28108
+ async pump(room) {
28109
+ if (room.busy) return;
28110
+ const asked = room.queue.shift();
28111
+ if (asked === void 0) return;
28112
+ room.busy = true;
28113
+ try {
28114
+ await this.turn(room, asked);
28115
+ } catch (error62) {
28116
+ this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf3(error62)}`);
28117
+ } finally {
28118
+ room.busy = false;
28119
+ void this.contexts?.refresh();
28120
+ void this.pump(room);
28121
+ }
27674
28122
  }
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;
28123
+ async turn(room, { message, speakerId, goal, sayWho }) {
28124
+ const { agent, spaceId } = room;
28125
+ const speaker = speakerId === void 0 ? this.options.chips : this.options.chips.as(speakerId);
28126
+ const run2 = await speaker.runs.start({ spaceId, goal: goal ?? goalOf(message, agent.config.name) });
28127
+ const turn = {
28128
+ run: run2,
28129
+ speaker,
28130
+ agent,
28131
+ who: speakerId ?? agent.agentId,
28132
+ hermesRun: "",
28133
+ reply: "",
28134
+ saidAt: 0,
28135
+ cancelled: false,
28136
+ next: [],
28137
+ open: /* @__PURE__ */ new Map(),
28138
+ counted: 0,
28139
+ approvals: /* @__PURE__ */ new Map(),
28140
+ allowed: [],
28141
+ interrupted: false
28142
+ };
28143
+ run2.on("cancel", () => {
28144
+ turn.cancelled = true;
28145
+ if (turn.hermesRun !== "") void agent.client.stop(turn.hermesRun).catch(() => void 0);
28146
+ });
28147
+ run2.on("steer", (steer) => {
28148
+ const words4 = `${steer.from?.display_name ?? "The person"} adds: ${steer.text}`;
28149
+ void this.steered(turn, words4, steer.mode);
28150
+ });
28151
+ let session;
28152
+ try {
28153
+ session = await this.sessionFor(room);
28154
+ } catch (error62) {
28155
+ await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf3(error62)}`, true);
28156
+ return;
28157
+ }
28158
+ const name2 = speakerId === void 0 ? agent.config.name : await this.nameOf(speakerId, agent.config.name);
28159
+ turn.folder = this.folderOf(agent, speakerId ?? agent.agentId, name2);
28160
+ const project = await this.projectFor(turn, room.spaceId, speakerId ?? agent.agentId, name2);
28161
+ turn.rules = rulesFor(message, { taker: speakerId, project });
28162
+ let text = promptFor(message, { taker: speakerId, sayWho, project, rulesHeld: true });
28163
+ for (let round = 0; text !== void 0 && !turn.cancelled && !run2.over; round += 1) {
28164
+ const last = await this.once(turn, session, text, `${run2.id}:${round}`);
28165
+ if (last === void 0) return;
28166
+ if (last.event === "run.cancelled" && turn.interrupted && turn.next.length > 0) {
28167
+ turn.interrupted = false;
28168
+ text = turn.next.shift();
28169
+ continue;
27694
28170
  }
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 ?? "";
28171
+ if (last.event !== "run.completed") {
28172
+ await this.ended(turn, last);
28173
+ return;
27700
28174
  }
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);
28175
+ text = turn.next.shift();
28176
+ if (text === void 0) await this.finish(turn, last);
27706
28177
  }
27707
28178
  }
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);
28179
+ /** One Hermes run of the turn: started, its events heard to the end. The end, or nothing on failure. */
28180
+ async once(turn, session, text, key) {
28181
+ const { agent } = turn;
28182
+ try {
28183
+ turn.hermesRun = await agent.client.startRun({
28184
+ text,
28185
+ sessionId: session,
28186
+ ...this.runModel(turn),
28187
+ key,
28188
+ instructions: instructionsFor(turn.folder, turn.project !== void 0, turn.rules)
28189
+ });
28190
+ } catch (error62) {
28191
+ await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf3(error62)}`, true);
28192
+ return void 0;
28193
+ }
28194
+ if (turn.cancelled) await agent.client.stop(turn.hermesRun).catch(() => void 0);
28195
+ try {
28196
+ const last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event));
28197
+ return last ?? await this.endOf(turn);
28198
+ } catch (error62) {
28199
+ this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf3(error62)}`);
28200
+ return await this.endOf(turn);
27717
28201
  }
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
28202
  }
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)}`;
28203
+ /** The end of a run whose stream was lost: its state read until it is over, as a terminal event. */
28204
+ async endOf(turn) {
28205
+ const by = Date.now() + LOST_STREAM_MS;
28206
+ while (Date.now() < by) {
28207
+ const state2 = await turn.agent.client.run(turn.hermesRun).catch(() => void 0);
28208
+ const status = typeof state2?.["status"] === "string" ? state2["status"] : void 0;
28209
+ if (status !== void 0 && TERMINAL.has(`run.${status}`)) {
28210
+ return { ...state2, event: `run.${status}` };
27742
28211
  }
27743
- if (trouble !== void 0) troubles.push(trouble);
28212
+ await new Promise((resolve7) => setTimeout(resolve7, 1e3));
27744
28213
  }
27745
- return troubles.length === 0 ? void 0 : line(troubles.join(" "));
28214
+ await this.failed(turn, `${turn.agent.config.name}'s run in Hermes stopped answering.`, true);
28215
+ return void 0;
27746
28216
  }
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);
28217
+ /** The room's session: the one it had, or a new one Hermes makes for it. */
28218
+ async sessionFor({ id: id2, agent, spaceId }) {
28219
+ const kept = this.options.state.sessions[id2];
28220
+ if (kept !== void 0) return kept;
28221
+ const who = id2.slice(0, id2.lastIndexOf("|"));
28222
+ const made = await agent.client.newSession(`Chips space ${spaceId}${who === agent.key ? "" : ` \xB7 ${who}`}`);
28223
+ this.options.state.sessions[id2] = made;
28224
+ this.options.saveState(this.options.state);
28225
+ return made;
28226
+ }
28227
+ /**
28228
+ * A person's words for a run that is going. A steer is said to the Hermes run, or to the next one
28229
+ * of the turn when Hermes won't take it now. A follow-up waits for this one to end. An
28230
+ * interrupt stops it, and the turn goes on with the words.
28231
+ */
28232
+ async steered(turn, text, mode) {
28233
+ if (mode === "interrupt" && turn.hermesRun !== "") {
28234
+ turn.next.unshift(text);
28235
+ turn.interrupted = true;
28236
+ await turn.agent.client.stop(turn.hermesRun).catch(() => void 0);
28237
+ return;
28238
+ }
28239
+ if (mode === "steer" && turn.hermesRun !== "") {
27753
28240
  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);
28241
+ await turn.agent.client.steer(turn.hermesRun, text);
28242
+ return;
27769
28243
  } catch (error62) {
27770
- log2(`Hermes's gateway on port ${port} couldn't be started: ${messageOf3(error62)}`);
28244
+ this.log(`Hermes didn't take a steer mid-run (${messageOf3(error62)}); saying it next`);
27771
28245
  }
27772
28246
  }
28247
+ turn.next.push(text);
28248
+ }
28249
+ // ------------------------------------------------------------------ what Hermes streams
28250
+ async heard(turn, event) {
28251
+ if (turn.run.over) return;
28252
+ switch (event.event) {
28253
+ case "message.delta":
28254
+ turn.reply += typeof event["delta"] === "string" ? event["delta"] : "";
28255
+ return;
28256
+ case "message.interim":
28257
+ case "reasoning.available":
28258
+ this.sayThought(turn, String(event["text"] ?? ""), event.event === "message.interim" ? "writing" : "thinking");
28259
+ return;
28260
+ case "tool.started":
28261
+ case "subagent.start":
28262
+ await this.started(turn, event);
28263
+ return;
28264
+ case "tool.completed":
28265
+ case "subagent.complete":
28266
+ await this.completed(turn, event);
28267
+ return;
28268
+ case "approval.request":
28269
+ void this.ask(turn, event);
28270
+ return;
28271
+ case "approval.responded":
28272
+ await this.settled(turn, event);
28273
+ return;
28274
+ default:
28275
+ return;
28276
+ }
28277
+ }
28278
+ sayThought(turn, text, phase) {
28279
+ const now = Date.now();
28280
+ const line2 = lastLine(text);
28281
+ if (line2 === "" || now - turn.saidAt < QUIET_MS) return;
28282
+ turn.saidAt = now;
28283
+ void turn.run.status({ phase, verbPhrase: line2.slice(0, 200) }).catch(() => void 0);
27773
28284
  }
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);
28285
+ async started(turn, event) {
28286
+ const sub = event.event === "subagent.start";
28287
+ const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
28288
+ turn.counted += 1;
28289
+ const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
28290
+ const stepId = callId ?? `${tool}-${turn.counted}`;
28291
+ const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
28292
+ turn.open.set(lane, [...turn.open.get(lane) ?? [], stepId]);
28293
+ const said2 = sub ? event["goal"] : event["preview"];
28294
+ await turn.run.step({
28295
+ stepId,
28296
+ status: "started",
28297
+ title: `${tool}${typeof said2 === "string" && said2 !== "" ? ` \xB7 ${said2}` : ""}`.slice(0, 300),
28298
+ tool,
28299
+ ...KINDS[tool] === void 0 ? {} : { kind: KINDS[tool] }
28300
+ }).catch(() => void 0);
27781
28301
  }
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);
28302
+ async completed(turn, event) {
28303
+ const sub = event.event === "subagent.complete";
28304
+ const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
28305
+ const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
28306
+ const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
28307
+ const waiting = turn.open.get(lane) ?? [];
28308
+ const stepId = callId ?? waiting.shift() ?? `${tool}-${turn.counted += 1}`;
28309
+ turn.open.set(lane, waiting.filter((each) => each !== stepId));
28310
+ const failed = event["error"] === true || sub && event["status"] === "failed";
28311
+ const approvalId = sub ? void 0 : turn.allowed.shift();
28312
+ if (approvalId !== void 0) {
28313
+ await turn.run.executed(approvalId, failed ? "failed" : "executed").catch(() => void 0);
27797
28314
  }
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.`;
28315
+ const summary = sub ? event["summary"] : resultWords(event["preview"]);
28316
+ const seconds = typeof event["duration"] === "number" ? event["duration"] : void 0;
28317
+ await turn.run.step({
28318
+ stepId,
28319
+ status: failed ? "failed" : "done",
28320
+ tool,
28321
+ ...typeof summary === "string" && summary !== "" ? { summary: summary.slice(0, 2e3) } : {},
28322
+ ...seconds === void 0 ? {} : { durationMs: Math.round(seconds * 1e3) }
28323
+ }).catch(() => void 0);
28324
+ const cost3 = event["cost_usd"];
28325
+ if (sub && typeof cost3 === "number" && cost3 > 0) {
28326
+ const model = typeof event["model"] === "string" ? event["model"] : "subagent";
28327
+ await turn.run.usage({ scope: "step", model, costUsd: cost3 }).catch(() => void 0);
27804
28328
  }
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
28329
  }
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;
28330
+ // ------------------------------------------------------------------ what Hermes asks
28331
+ /** An approval Hermes wants before it acts, as a card; the answer is Hermes's choice. */
28332
+ async ask(turn, event) {
28333
+ const requestId = idOf(event);
28334
+ const command = typeof event["command"] === "string" ? event["command"] : void 0;
28335
+ const description = typeof event["description"] === "string" ? event["description"] : void 0;
28336
+ const choices = Array.isArray(event["choices"]) ? event["choices"].map(String) : ["once", "deny"];
28337
+ const approvalId = ids.next("approval");
28338
+ turn.approvals.set(requestId ?? approvalId, { approvalId, answered: false });
28339
+ const asked = turn.run.approval({
28340
+ approvalId,
28341
+ action: {
28342
+ kind: command === void 0 ? "hermes.approval" : "shell.exec",
28343
+ input: { ...command === void 0 ? {} : { command }, ...description === void 0 ? {} : { description } }
28344
+ },
28345
+ question: (description === void 0 ? "Run this command?" : `Allow it to ${lowerFirst(description)}?`).slice(0, 300),
28346
+ lasts: "once",
28347
+ risk: [],
28348
+ ...command === void 0 ? {} : { preview: { format: "markdown", title: "Run a command", body: `\`\`\`sh
28349
+ ${command}
28350
+ \`\`\`` } }
28351
+ });
28352
+ const stopped2 = new Promise((resolve7) => turn.run.on("cancel", () => resolve7(void 0)));
28353
+ const decision = await Promise.race([asked, stopped2]);
28354
+ const pending = turn.approvals.get(requestId ?? approvalId);
28355
+ if (decision === void 0 || pending === void 0 || pending.answered) return;
28356
+ pending.answered = true;
28357
+ if (decision.allowed) turn.allowed.push(approvalId);
28358
+ const generation = event["execution_generation"];
28359
+ await turn.agent.client.approve(turn.hermesRun, {
28360
+ choice: choiceOf(decision, choices, command !== void 0),
28361
+ ...requestId === void 0 ? {} : { request_id: requestId },
28362
+ ...typeof generation === "number" ? { execution_generation: generation } : {}
28363
+ }).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf3(error62)}`));
27813
28364
  }
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);
28365
+ /** An approval answered in Hermes's own tools: settled in Chips, once, so the card cools (ADR 0079). */
28366
+ async settled(turn, event) {
28367
+ const requestId = idOf(event);
28368
+ const pending = requestId === void 0 ? void 0 : turn.approvals.get(requestId);
28369
+ if (pending === void 0 || pending.answered) return;
28370
+ pending.answered = true;
28371
+ const denied = event["choice"] === "deny";
28372
+ await this.options.chips.send(
28373
+ "approval.resolved",
28374
+ { run_id: turn.run.id },
28375
+ {
28376
+ approval_id: pending.approvalId,
28377
+ decision: denied ? "denied" : "approved",
28378
+ scope: "once",
28379
+ reason: "Answered in Hermes."
28380
+ },
28381
+ `hermes-approval:${turn.hermesRun}:${requestId}:settled`
28382
+ ).catch(() => void 0);
27825
28383
  }
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);
28384
+ // ------------------------------------------------------------------ the end
28385
+ /** The turn is over: its words in the room, its tokens, and the run ended. */
28386
+ async finish(turn, last) {
28387
+ const { run: run2, agent } = turn;
28388
+ if (run2.over) return;
28389
+ await this.closeSteps(turn, "done");
28390
+ for (const approvalId of turn.allowed.splice(0)) {
28391
+ await run2.executed(approvalId, "executed", "Hermes didn't say how it went.").catch(() => void 0);
27832
28392
  }
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);
28393
+ await this.counted(turn, last);
28394
+ const output2 = typeof last["output"] === "string" ? last["output"].trim() : "";
28395
+ const answer = output2 !== "" ? output2 : turn.reply.trim();
28396
+ const { change, note } = await this.sendProject(turn, answer);
28397
+ const { folder: folder2 } = turn;
28398
+ const { chips: chips2 } = this.options;
28399
+ const words4 = [answer, note ?? ""].filter((part) => part !== "").join("\n\n");
28400
+ const post = await sendAnswer(words4, {
28401
+ folders: folder2 === void 0 ? [] : [folder2],
28402
+ upload: (input2) => chips2.upload(input2)
28403
+ });
28404
+ const reply = post.text;
28405
+ if (reply !== "" || post.uploadIds.length > 0) {
28406
+ await turn.speaker.post({ spaceId: run2.spaceId, runId: run2.id, text: reply, uploadIds: post.uploadIds, change });
27852
28407
  }
28408
+ const said2 = post.summary;
28409
+ const summary = said2 === "" ? "Done." : said2.length <= 2e3 ? said2 : `${said2.slice(0, 1990)}\u2026`;
28410
+ await run2.complete({ summary });
28411
+ this.log(`${agent.config.name} answered in ${run2.spaceId}`);
28412
+ this.missingReadings();
27853
28413
  }
27854
28414
  /**
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.
28415
+ * A turn ended and Hermes sent no reading at all since the bridge started: its gateway most
28416
+ * likely started before Chips's webhook was in its config. Said once, a little after, since a
28417
+ * reading comes a moment after its model call.
27857
28418
  */
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 };
28419
+ missingReadings() {
28420
+ if (this.fills === void 0 || this.fillsPort === void 0 || this.fills.anyHeard || this.toldNoReadings) return;
28421
+ this.toldNoReadings = true;
28422
+ setTimeout(() => {
28423
+ if (this.fills?.anyHeard === false) {
28424
+ this.log(
28425
+ "context window: Hermes sent no reading after a turn. Start hermes gateway again once, so it takes Chips's webhook from its config."
28426
+ );
27906
28427
  }
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);
28428
+ }, 1e4).unref();
28429
+ }
28430
+ /** A run that failed, was stopped or was cut short in Hermes. */
28431
+ async ended(turn, last) {
28432
+ await this.closeSteps(turn, "failed");
28433
+ await this.counted(turn, last);
28434
+ if (turn.cancelled || turn.run.over) return;
28435
+ const name2 = turn.agent.config.name;
28436
+ if (last.event === "run.cancelled") await this.failed(turn, `${name2}'s run was stopped in Hermes.`, false);
28437
+ else if (last.event === "run.interrupted") await this.failed(turn, `Hermes restarted during ${name2}'s run.`, true);
28438
+ else {
28439
+ const why = typeof last["error"] === "string" && last["error"] !== "" ? last["error"] : "it failed in Hermes";
28440
+ await this.failed(turn, `${name2}: ${why}`, true);
27910
28441
  }
27911
28442
  }
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;
28443
+ /**
28444
+ * The tokens Hermes counted, as a step's share, so they add up over the turn's runs. Hermes's
28445
+ * `input_tokens` is already every token in, its cache's reads and writes among them (its
28446
+ * `CanonicalUsage.prompt_tokens`), which is what Chips counts (Chips ADR 0214); the cache's two
28447
+ * counts go with it, and are left out where Hermes doesn't say them.
28448
+ */
28449
+ async counted(turn, last) {
28450
+ const usage2 = last["usage"] ?? {};
28451
+ const count = (key) => typeof usage2[key] === "number" ? usage2[key] : void 0;
28452
+ const input2 = count("input_tokens") ?? 0;
28453
+ const output2 = count("output_tokens") ?? 0;
28454
+ if (input2 + output2 === 0) return;
28455
+ const runtime = last["runtime"] ?? {};
28456
+ const model = typeof runtime["model"] === "string" ? runtime["model"] : this.runModel(turn).model ?? "hermes";
28457
+ await turn.run.usage({
28458
+ scope: "step",
28459
+ model,
28460
+ inputTokens: input2,
28461
+ cacheReadTokens: count("cache_read_tokens"),
28462
+ cacheWriteTokens: count("cache_write_tokens"),
28463
+ outputTokens: output2
28464
+ }).catch(() => void 0);
28465
+ }
28466
+ async closeSteps(turn, status) {
28467
+ for (const [, ids2] of turn.open) {
28468
+ for (const stepId of ids2) await turn.run.step({ stepId, status }).catch(() => void 0);
27919
28469
  }
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
- };
28470
+ turn.open.clear();
28471
+ }
28472
+ async failed(turn, error62, retryable) {
28473
+ if (!turn.run.over) await turn.run.fail({ error: error62, retryable }).catch(() => void 0);
28474
+ if (retryable) void this.sayHowItIs();
28475
+ }
28476
+ };
28477
+ function choiceOf(decision, choices, command = false) {
28478
+ if (!decision.allowed) return "deny";
28479
+ if (command) return "once";
28480
+ if (decision.scope === "always" && choices.includes("always")) return "always";
28481
+ if (decision.scope !== "once" && choices.includes("session")) return "session";
28482
+ return "once";
27942
28483
  }
27943
- function gatewayLog(configFolder, platform = process.platform) {
27944
- return platform === "darwin" ? join9(homedir5(), "Library", "Logs", "chips-hermes-gateway.log") : join9(configFolder, "gateway.log");
28484
+ function idOf(event) {
28485
+ const id2 = event["request_id"] ?? event["prompt_id"];
28486
+ return typeof id2 === "string" && id2 !== "" ? id2 : void 0;
27945
28487
  }
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
- });
28488
+ function messageOf3(error62) {
28489
+ if (error62 instanceof HermesError) return `${error62.message} (${error62.status})`;
28490
+ return error62 instanceof Error ? error62.message : String(error62);
27965
28491
  }
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
- };
28492
+ function titleFrom(text) {
28493
+ const line2 = text.trim().split("\n")[0] ?? "";
28494
+ if (line2.length <= 48) return line2;
28495
+ const cut2 = line2.slice(0, 48);
28496
+ return `${cut2.slice(0, Math.max(cut2.lastIndexOf(" "), 24))}\u2026`;
28497
+ }
28498
+ function forkName(original) {
28499
+ return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
28500
+ }
28501
+ function goalOf(message, name2) {
28502
+ const first = message.text.split("\n").find((line2) => line2.trim() !== "")?.trim() ?? "";
28503
+ const named = first.toLowerCase().startsWith(`@${name2.toLowerCase()}`);
28504
+ const goal = named ? first.slice(name2.length + 1).trim() : first.replace(/^@\S+\s*/, "");
28505
+ return (goal === "" ? `Answer ${message.from.display_name}` : goal).slice(0, 300);
28506
+ }
28507
+ var REPLY_SHOWN = 1500;
28508
+ var GOAL_SHOWN = 120;
28509
+ function lookAgain(who, reply, note) {
28510
+ const said2 = reply?.trim() ?? "";
28511
+ return [
28512
+ `${who} says your reply in this space wasn't right.`,
28513
+ ...said2 === "" ? [] : [`Your reply:
28514
+ \xAB${cut(said2, REPLY_SHOWN)}\xBB`],
28515
+ 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`,
28516
+ "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."
28517
+ ].join("\n\n");
28518
+ }
28519
+ function lookingAgain(note) {
28520
+ const first = note?.split("\n").find((line2) => line2.trim() !== "")?.trim();
28521
+ return first === void 0 ? "Looking again at its reply" : cut(`Looking again: ${first}`, GOAL_SHOWN);
28522
+ }
28523
+ function cut(text, most) {
28524
+ return text.length <= most ? text : `${text.slice(0, most - 1)}\u2026`;
28525
+ }
28526
+ function resultWords(preview2) {
28527
+ if (typeof preview2 !== "string" || !preview2.trimStart().startsWith("{")) return preview2;
28528
+ let result;
28529
+ try {
28530
+ result = JSON.parse(preview2);
28531
+ } catch {
28532
+ return preview2;
28533
+ }
28534
+ if (typeof result !== "object" || result === null || !("output" in result)) return preview2;
28535
+ const { output: output2, error: error62 } = result;
28536
+ const said2 = [output2, error62].find((each) => typeof each === "string" && each.trim() !== "");
28537
+ return typeof said2 === "string" ? said2.trim() : "";
28538
+ }
28539
+ function lastLine(text) {
28540
+ const lines2 = text.split(/(?<=[.!?])\s+|\n+/).map((line2) => line2.trim()).filter((line2) => line2 !== "");
28541
+ return lines2.at(-1) ?? "";
28542
+ }
28543
+ function lowerFirst(text) {
28544
+ return text.length === 0 ? text : `${text[0]?.toLowerCase() ?? ""}${text.slice(1)}`;
28066
28545
  }
28067
28546
 
28068
28547
  // src/keep.ts
28069
28548
  import { homedir as homedir6 } from "node:os";
28070
- import { basename as basename5, join as join10, resolve as resolve4 } from "node:path";
28549
+ import { basename as basename5, join as join11, resolve as resolve4 } from "node:path";
28071
28550
  import { fileURLToPath as fileURLToPath2 } from "node:url";
28072
28551
  var flags = process.argv.slice(2);
28073
28552
  var words3 = flags.filter((word) => !word.startsWith("--"));
28074
- var whole = (word) => resolve4(word.startsWith("~/") ? join10(homedir6(), word.slice(2)) : word);
28553
+ var whole = (word) => resolve4(word.startsWith("~/") ? join11(homedir6(), word.slice(2)) : word);
28075
28554
  var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
28076
28555
  var self = fileURLToPath2(import.meta.url);
28077
28556
  var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
@@ -28117,9 +28596,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
28117
28596
  }
28118
28597
 
28119
28598
  // src/setup.ts
28120
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
28599
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
28121
28600
  import { homedir as homedir7 } from "node:os";
28122
- import { join as join11, resolve as resolve5 } from "node:path";
28601
+ import { join as join12, resolve as resolve5 } from "node:path";
28123
28602
  var SetupError = class extends Error {
28124
28603
  constructor(message, words4 = {}) {
28125
28604
  super(message);
@@ -28144,16 +28623,16 @@ function readyHooks(config3, options) {
28144
28623
  const lines2 = [];
28145
28624
  let restart2 = false;
28146
28625
  const done = /* @__PURE__ */ new Set();
28147
- for (const agent of config3.agents) {
28148
- const profile = profileName(agent.address.profile);
28626
+ for (const named of [...config3.agents.map((agent) => agent.address.profile), ...options.made ?? []]) {
28627
+ const profile = profileName(named);
28149
28628
  if (done.has(profile)) continue;
28150
28629
  done.add(profile);
28151
- const home2 = profileHome(options.root, agent.address.profile);
28630
+ const home2 = profileHome(options.root, named);
28152
28631
  let outcome;
28153
28632
  try {
28154
28633
  outcome = addHook(home2, options.port, profile, options.secret);
28155
28634
  } catch (error62) {
28156
- lines2.push(`Chips's webhook wasn't added to ${shown(join11(home2, "config.yaml"))}: ${error62 instanceof Error ? error62.message : String(error62)}`);
28635
+ lines2.push(`Chips's webhook wasn't added to ${shown(join12(home2, "config.yaml"))}: ${error62 instanceof Error ? error62.message : String(error62)}`);
28157
28636
  continue;
28158
28637
  }
28159
28638
  if (outcome.state === "added") {
@@ -28179,8 +28658,8 @@ function hermesEnvPath(env = process.env) {
28179
28658
  }
28180
28659
  function fromHermes(name2, env) {
28181
28660
  const path = hermesEnvPath(env);
28182
- if (!existsSync9(path)) return void 0;
28183
- for (const line2 of readFileSync9(path, "utf8").split("\n")) {
28661
+ if (!existsSync10(path)) return void 0;
28662
+ for (const line2 of readFileSync10(path, "utf8").split("\n")) {
28184
28663
  const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line2);
28185
28664
  if (match?.[1] === name2) return match[2]?.replace(/^(["'])(.*)\1$/, "$2");
28186
28665
  }
@@ -28191,7 +28670,7 @@ function folderOf(said2) {
28191
28670
  return resolve5(said2 === "~" || said2.startsWith("~/") ? homedir7() + said2.slice(1) : said2);
28192
28671
  }
28193
28672
  function readConfig(path, env = process.env) {
28194
- const raw = existsSync9(path) ? JSON.parse(readFileSync9(path, "utf8")) : PLAIN;
28673
+ const raw = existsSync10(path) ? JSON.parse(readFileSync10(path, "utf8")) : PLAIN;
28195
28674
  const url2 = typeof raw.url === "string" ? raw.url : "http://127.0.0.1:8642";
28196
28675
  const folder2 = folderOf(raw.folder);
28197
28676
  if (!Array.isArray(raw.agents) || raw.agents.length === 0) {
@@ -28261,15 +28740,19 @@ async function checkHermes(config3, clientFor = (agent) => new HermesClient(agen
28261
28740
  }
28262
28741
  }
28263
28742
 
28743
+ // src/version.ts
28744
+ import { readFileSync as readFileSync11 } from "node:fs";
28745
+ var VERSION = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf8")).version;
28746
+
28264
28747
  // src/main.ts
28265
- var home = (path) => path.startsWith("~/") ? join12(homedir8(), path.slice(2)) : path;
28748
+ var home = (path) => path.startsWith("~/") ? join13(homedir8(), path.slice(2)) : path;
28266
28749
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
28267
28750
  var LABEL = "chat.chips.hermes";
28268
28751
  stopIfAsked(LABEL);
28269
28752
  var configPath = resolve6(home(words3[0] ?? "~/.chips-hermes/config.json"));
28270
28753
  var folder = dirname6(configPath);
28271
- var statePath = join12(folder, "sessions.json");
28272
- var state = existsSync10(statePath) ? JSON.parse(readFileSync10(statePath, "utf8")) : { sessions: {} };
28754
+ var statePath = join13(folder, "sessions.json");
28755
+ var state = existsSync11(statePath) ? JSON.parse(readFileSync12(statePath, "utf8")) : { sessions: {} };
28273
28756
  var saveState = (next) => {
28274
28757
  mkdirSync6(folder, { recursive: true, mode: 448 });
28275
28758
  writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
@@ -28294,12 +28777,13 @@ try {
28294
28777
  reportStep("checking", { detail: "Looking for its API server" });
28295
28778
  await checkHermes(config2);
28296
28779
  }
28297
- fills.secret ??= randomBytes2(32).toString("hex");
28780
+ fills.secret ??= randomBytes3(32).toString("hex");
28298
28781
  fills.port = config2.hookPort ?? fills.port ?? hookPortIn(profileHome(root, config2.agents[0]?.address.profile)) ?? await freePort();
28299
28782
  saveState(state);
28300
28783
  const first = config2.agents[0];
28301
28784
  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 });
28785
+ const made = Object.values(state.made ?? {});
28786
+ const hooks = readyHooks(config2, { root, port: fills.port, secret: fills.secret, gatewayUp, made });
28303
28787
  for (const line2 of hooks.lines) log(line2);
28304
28788
  if (link !== void 0) reportStep("checked", { detail: checkedWords(config2, hooks.restart) });
28305
28789
  } catch (error62) {
@@ -28329,7 +28813,7 @@ var joined = await join3({
28329
28813
  link,
28330
28814
  url: process.env["CHIPS_URL"],
28331
28815
  name,
28332
- software: "chips-hermes 1.0.0",
28816
+ software: `chips-hermes ${VERSION}`,
28333
28817
  wants: WANTS,
28334
28818
  onCode: (code) => log(`Allow this harness in Chips: ${code}`)
28335
28819
  }).catch(refused2);
@@ -28347,8 +28831,8 @@ var restart = restarter({
28347
28831
  var chips = await connect({
28348
28832
  url: joined.url,
28349
28833
  token: joined.token,
28350
- client: { name: "chips-hermes", version: "1.0.0" },
28351
- store: fileStore(join12(folder, "chips-state.json")),
28834
+ client: { name: "chips-hermes", version: VERSION },
28835
+ store: fileStore(join13(folder, "chips-state.json")),
28352
28836
  onRestart: restart,
28353
28837
  settings: settingsFor(() => bridge)
28354
28838
  });
@@ -28364,7 +28848,10 @@ bridge = new Bridge({
28364
28848
  saveState,
28365
28849
  log,
28366
28850
  hermesRoot: root,
28367
- fills: { port: fills.port, secret: fills.secret }
28851
+ fills: { port: fills.port, secret: fills.secret },
28852
+ // Each agent made in Chips is a Hermes profile of its own (Chips ADR 0241).
28853
+ profiles: hermesProfiles({ root, env: process.env, system: thisMachine(), log: (line2) => log(`profiles: ${line2}`) }),
28854
+ restartGateway: () => restart({ askedBy: "chips-hermes, for a new agent's profile", askedAt: (/* @__PURE__ */ new Date()).toISOString() })
28368
28855
  });
28369
28856
  await bridge.start();
28370
28857
  log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through Hermes`);