@chipschat/opencode 1.0.6 → 1.0.7

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,9 +8,9 @@ var __export = (target, all) => {
8
8
  };
9
9
 
10
10
  // src/main.ts
11
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
12
- import { homedir as homedir6 } from "node:os";
13
- import { dirname as dirname4, join as join9, resolve as resolve4 } from "node:path";
11
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
12
+ import { homedir as homedir7 } from "node:os";
13
+ import { dirname as dirname5, join as join10, resolve as resolve4 } from "node:path";
14
14
 
15
15
  // node_modules/zod/v4/classic/external.js
16
16
  var external_exports = {};
@@ -4871,10 +4871,10 @@ function partPattern(schema) {
4871
4871
  const own2 = schema._zod.pattern?.source;
4872
4872
  const inner = def.innerType ?? schema._zod.innerType;
4873
4873
  if (inner) {
4874
- const before = inner._zod.pattern?.source;
4874
+ const before2 = inner._zod.pattern?.source;
4875
4875
  const after = partPattern(inner);
4876
- if (own2 && before && after && after !== before) {
4877
- return own2.replace(cleanRegex(before), () => cleanRegex(after));
4876
+ if (own2 && before2 && after && after !== before2) {
4877
+ return own2.replace(cleanRegex(before2), () => cleanRegex(after));
4878
4878
  }
4879
4879
  return own2;
4880
4880
  }
@@ -25467,7 +25467,270 @@ function fileStore(path) {
25467
25467
 
25468
25468
  // src/bridge.ts
25469
25469
  import { mkdirSync as mkdirSync4 } from "node:fs";
25470
- import { join as join5 } from "node:path";
25470
+ import { homedir as homedir4 } from "node:os";
25471
+ import { join as join6 } from "node:path";
25472
+
25473
+ // src/context.ts
25474
+ import { createHash as createHash2 } from "node:crypto";
25475
+ import { globSync, readFileSync as readFileSync5, statSync } from "node:fs";
25476
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
25477
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, relative, sep as sep2 } from "node:path";
25478
+ var AGENTS_MD = "AGENTS.md";
25479
+ function placesOf(env, home3) {
25480
+ const config3 = env["OPENCODE_CONFIG_DIR"] || join5(env["XDG_CONFIG_HOME"] || join5(home3, ".config"), "opencode");
25481
+ return { home: home3, config: config3 };
25482
+ }
25483
+ function hashOf(text2) {
25484
+ return createHash2("sha256").update(text2, "utf8").digest("hex");
25485
+ }
25486
+ function isFile(path) {
25487
+ try {
25488
+ return statSync(path).isFile();
25489
+ } catch {
25490
+ return false;
25491
+ }
25492
+ }
25493
+ function textOf(path) {
25494
+ try {
25495
+ return readFileSync5(path, "utf8");
25496
+ } catch {
25497
+ return void 0;
25498
+ }
25499
+ }
25500
+ var within = (path, dir) => path === dir || path.startsWith(`${dir}${sep2}`);
25501
+ function upTo(dir, stop2) {
25502
+ const dirs = [dir];
25503
+ for (let at = dir; within(at, stop2) && at !== stop2 && dirname4(at) !== at; ) {
25504
+ at = dirname4(at);
25505
+ dirs.push(at);
25506
+ }
25507
+ return dirs;
25508
+ }
25509
+ function namedFiles(sources, folder2, root, home3) {
25510
+ const found = [];
25511
+ const glob = (pattern, cwd) => {
25512
+ try {
25513
+ return globSync(pattern, { cwd }).map((match) => join5(cwd, match));
25514
+ } catch {
25515
+ return [];
25516
+ }
25517
+ };
25518
+ for (const source2 of sources) {
25519
+ if (/^https?:\/\//i.test(source2)) continue;
25520
+ const expanded = source2.startsWith("~/") ? join5(home3, source2.slice(2)) : source2;
25521
+ const matches = isAbsolute2(expanded) ? glob(expanded.slice(dirname4(expanded).length + 1), dirname4(expanded)) : upTo(folder2, within(folder2, root) ? root : folder2).flatMap((dir) => glob(expanded, dir));
25522
+ for (const path of matches.sort()) if (isFile(path) && !found.includes(path)) found.push(path);
25523
+ }
25524
+ return found;
25525
+ }
25526
+ function before(version3, than) {
25527
+ const parts = (each) => each.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
25528
+ const [a, b] = [parts(version3), parts(than)];
25529
+ for (let at = 0; at < 3; at += 1) if ((a[at] ?? 0) !== (b[at] ?? 0)) return (a[at] ?? 0) < (b[at] ?? 0);
25530
+ return false;
25531
+ }
25532
+ var READS_NAMED = "2.0.30";
25533
+ var ContextFiles = class {
25534
+ constructor(places, folders) {
25535
+ this.places = places;
25536
+ this.folders = folders;
25537
+ }
25538
+ places;
25539
+ folders;
25540
+ /** `~/chips-opencode/agents/AGENTS.md`, or the whole path where it isn't in the home. */
25541
+ tilde(path) {
25542
+ const { home: home3 } = this.places;
25543
+ return within(path, home3) ? `~${path.slice(home3.length)}` : path;
25544
+ }
25545
+ /** A shared file's name: where it is, a step at a time (`~ › .config › opencode › AGENTS.md`), with no slash. */
25546
+ crumbs(path) {
25547
+ const { home: home3 } = this.places;
25548
+ const steps = within(path, home3) ? ["~", ...relative(home3, path).split(sep2)] : ["/", ...path.split(sep2)];
25549
+ return steps.filter((step) => step !== "").join(" \u203A ").slice(0, 255);
25550
+ }
25551
+ /**
25552
+ * The harness's own set, for agents whose folders read as `readings` says: every agent's
25553
+ * `AGENTS.md`, those above it, the person's own, and the named files not in an agent's folder.
25554
+ */
25555
+ sharedSpots(readings) {
25556
+ const { places, folders } = this;
25557
+ const spots = [];
25558
+ const add = (spot) => {
25559
+ if (!spots.some((each) => each.path === spot.path)) spots.push(spot);
25560
+ };
25561
+ const reaches = (dir) => (folder2) => within(folder2, dir) && within(dir, readings.get(folder2)?.stop ?? folder2);
25562
+ if (folders !== void 0 && within(folders, places.home)) {
25563
+ const path = join5(folders, AGENTS_MD);
25564
+ add({
25565
+ path,
25566
+ name: this.crumbs(path),
25567
+ title: "For every OpenCode agent",
25568
+ about: this.tilde(path),
25569
+ editable: true,
25570
+ id: "every-agent",
25571
+ over: reaches(folders)
25572
+ });
25573
+ }
25574
+ const yours = join5(places.config, AGENTS_MD);
25575
+ for (const [folder2, reading] of readings) {
25576
+ for (const dir of upTo(folder2, reading.stop).slice(1)) {
25577
+ const path = join5(dir, AGENTS_MD);
25578
+ if (path === yours || !isFile(path)) continue;
25579
+ add({
25580
+ path,
25581
+ name: this.crumbs(path),
25582
+ title: `From ${this.tilde(dir)}`.slice(0, 80),
25583
+ about: this.tilde(path),
25584
+ editable: false,
25585
+ whyReadOnly: "It sits above the agents' folders.",
25586
+ id: `above-${hashOf(path).slice(0, 12)}`,
25587
+ over: reaches(dir)
25588
+ });
25589
+ }
25590
+ }
25591
+ if (isFile(yours)) {
25592
+ add({
25593
+ path: yours,
25594
+ name: this.crumbs(yours),
25595
+ title: "For all your OpenCode",
25596
+ about: this.tilde(yours),
25597
+ editable: false,
25598
+ whyReadOnly: "Your own OpenCode reads it too.",
25599
+ id: "yours"
25600
+ });
25601
+ }
25602
+ for (const [folder2, reading] of readings) {
25603
+ for (const path of reading.named) {
25604
+ if (within(path, folder2)) continue;
25605
+ add({
25606
+ path,
25607
+ name: this.crumbs(path),
25608
+ title: "Named in OpenCode's config",
25609
+ about: this.tilde(path),
25610
+ editable: false,
25611
+ whyReadOnly: "OpenCode's config names it for every session.",
25612
+ id: `named-${hashOf(path).slice(0, 12)}`,
25613
+ over: (other) => readings.get(other)?.named.includes(path) === true
25614
+ });
25615
+ }
25616
+ }
25617
+ return spots;
25618
+ }
25619
+ /** An agent's own files: its folder's `AGENTS.md`, and the named files in its folder. */
25620
+ ownSpots(folder2, reading) {
25621
+ const named = reading.named.filter((path) => within(path, folder2) && path !== join5(folder2, AGENTS_MD));
25622
+ return [
25623
+ { path: join5(folder2, AGENTS_MD), name: AGENTS_MD, title: "How it works", editable: true },
25624
+ ...named.map((path) => ({
25625
+ path,
25626
+ name: relative(folder2, path).split(sep2).join(" \u203A ").slice(0, 255),
25627
+ title: "Named in OpenCode's config",
25628
+ editable: true
25629
+ }))
25630
+ ];
25631
+ }
25632
+ /** The shared files an agent in `folder` reads. */
25633
+ readBy(folder2, reading) {
25634
+ return this.sharedSpots(/* @__PURE__ */ new Map([[folder2, reading]])).filter((spot) => spot.over === void 0 || spot.over(folder2));
25635
+ }
25636
+ /** An agent's copy: its own files and the shared ones it reads, eight at most. */
25637
+ agentCopy(folder2, reading) {
25638
+ const rows = [];
25639
+ const notShown = [];
25640
+ const add = (row, text2) => {
25641
+ const named = { name: row["name"], title: row["title"] };
25642
+ if (text2 !== void 0 && contextBytes(text2) > CONTEXT_FILE_BYTES) notShown.push({ ...named, reason: "too_large" });
25643
+ else if (rows.length >= CONTEXT_FILES_SHOWN) notShown.push({ ...named, reason: "count" });
25644
+ else rows.push(row);
25645
+ };
25646
+ for (const spot of this.ownSpots(folder2, reading)) {
25647
+ const text2 = textOf(spot.path) ?? "";
25648
+ add({ name: spot.name, title: spot.title, role: "instructions", text: text2, hash: hashOf(text2), editable: true }, text2);
25649
+ }
25650
+ for (const spot of this.readBy(folder2, reading)) {
25651
+ add({
25652
+ name: spot.name,
25653
+ title: spot.title,
25654
+ role: "instructions",
25655
+ ...spot.about === void 0 ? {} : { about: spot.about },
25656
+ shared: spot.id,
25657
+ replaceable: false,
25658
+ editable: false
25659
+ });
25660
+ }
25661
+ return { files: rows, ...notShown.length === 0 ? {} : { not_shown: notShown } };
25662
+ }
25663
+ /** The harness's own set, each file with how many of its agents (by their folders' readings) read it. */
25664
+ harnessCopy(readings, agentFolders) {
25665
+ const shared = [];
25666
+ const notShown = [];
25667
+ for (const spot of this.sharedSpots(readings)) {
25668
+ const text2 = textOf(spot.path) ?? "";
25669
+ const named = { name: spot.name, title: spot.title };
25670
+ if (contextBytes(text2) > CONTEXT_FILE_BYTES) {
25671
+ notShown.push({ ...named, reason: "too_large" });
25672
+ continue;
25673
+ }
25674
+ if (shared.length >= CONTEXT_FILES_SHOWN) {
25675
+ notShown.push({ ...named, reason: "count" });
25676
+ continue;
25677
+ }
25678
+ const over = spot.over;
25679
+ shared.push({
25680
+ id: spot.id,
25681
+ name: spot.name,
25682
+ title: spot.title,
25683
+ role: "instructions",
25684
+ ...spot.about === void 0 ? {} : { about: spot.about },
25685
+ text: text2,
25686
+ hash: hashOf(text2),
25687
+ editable: spot.editable,
25688
+ ...spot.whyReadOnly === void 0 ? {} : { why_read_only: spot.whyReadOnly },
25689
+ used_by: over === void 0 ? agentFolders.length : agentFolders.filter(over).length
25690
+ });
25691
+ }
25692
+ return { shared, ...notShown.length === 0 ? {} : { not_shown: notShown } };
25693
+ }
25694
+ /**
25695
+ * A person's edit of one of an agent's files, saved where OpenCode reads it, only if it is still
25696
+ * the version they changed. Refused, with the copy as it is, for a file that isn't the agent's own
25697
+ * to change, and as `changed` for one changed since.
25698
+ */
25699
+ async saveAgent(folder2, reading, edit) {
25700
+ const now = () => this.agentCopy(folder2, reading);
25701
+ const spot = this.ownSpots(folder2, reading).find((each) => each.name === edit.name);
25702
+ if (spot === void 0) {
25703
+ const shared = this.readBy(folder2, reading).some((each) => each.name === edit.name);
25704
+ throw new Refuse(
25705
+ shared ? "Every agent reads this one. Change it on OpenCode's page." : `There is no file called ${edit.name} here.`,
25706
+ now(),
25707
+ "failed"
25708
+ );
25709
+ }
25710
+ if (edit.base_hash === null) throw new Refuse("This file is the agent's own already.", now(), "failed");
25711
+ await this.write(spot, edit, now);
25712
+ }
25713
+ /** A person's edit of one of the harness's own files, read by its agents beside their own. */
25714
+ async saveShared(readings, agentFolders, edit) {
25715
+ const now = () => this.harnessCopy(readings, agentFolders);
25716
+ const spot = this.sharedSpots(readings).find((each) => each.name === edit.name);
25717
+ if (spot === void 0) throw new Refuse(`There is no file called ${edit.name} here.`, now(), "failed");
25718
+ if (!spot.editable) throw new Refuse(spot.whyReadOnly ?? "It can't be changed from Chips.", now(), "failed");
25719
+ if (edit.base_hash === null) {
25720
+ throw new Refuse("Every agent reads this file beside its own, so none can have its own in its place.", now(), "failed");
25721
+ }
25722
+ await this.write(spot, edit, now);
25723
+ }
25724
+ async write(spot, edit, now) {
25725
+ if (hashOf(textOf(spot.path) ?? "") !== edit.base_hash) throw new Refuse("", now(), "changed");
25726
+ try {
25727
+ await mkdir2(dirname4(spot.path), { recursive: true });
25728
+ await writeFile2(spot.path, edit.text, "utf8");
25729
+ } catch (error62) {
25730
+ throw new Refuse(`It couldn't be saved: ${error62 instanceof Error ? error62.message : String(error62)}`.slice(0, 300), now(), "failed");
25731
+ }
25732
+ }
25733
+ };
25471
25734
 
25472
25735
  // src/opencode.ts
25473
25736
  var OpenCodeError = class extends Error {
@@ -25507,12 +25770,47 @@ var OpenCodeClient = class {
25507
25770
  return false;
25508
25771
  }
25509
25772
  }
25773
+ /** Which OpenCode answers: `GET /api/info`'s `version`, `2.0.25`. */
25774
+ async version() {
25775
+ const info = await this.call("GET", "/api/info");
25776
+ return typeof info["version"] === "string" ? info["version"] : "";
25777
+ }
25778
+ /** The root of the project a folder is in, as OpenCode has it: the folder itself, outside any repository. */
25779
+ async projectRoot(directory) {
25780
+ const read = await this.call("GET", `/api/location${whereIs(directory)}`);
25781
+ const project = read["project"] ?? {};
25782
+ return typeof project["directory"] === "string" ? project["directory"] : directory;
25783
+ }
25784
+ /**
25785
+ * The instruction files the config documents for a folder name (`instructions`), each document's
25786
+ * in turn, each named once: a path, a glob or a URL, as written.
25787
+ */
25788
+ async instructions(directory) {
25789
+ const answer = await this.fetcher(`${this.root}/api/config${whereIs(directory)}`, { headers: { authorization: this.auth } });
25790
+ if (!answer.ok) throw await failureOf(answer);
25791
+ const entries = await answer.json();
25792
+ const named = (Array.isArray(entries) ? entries : []).filter((entry) => entry["type"] === "document").flatMap((entry) => (entry["info"] ?? {})["instructions"]).filter((each) => typeof each === "string");
25793
+ return [...new Set(named)];
25794
+ }
25510
25795
  async agents() {
25511
25796
  return await this.catalogue("agent");
25512
25797
  }
25513
25798
  async models() {
25514
25799
  return await this.catalogue("model");
25515
25800
  }
25801
+ /** The model a session with none of its own runs on, when the server has one. */
25802
+ async defaultModel() {
25803
+ const read = await this.call("GET", `/api/model/default${this.where()}`);
25804
+ const data = read["data"];
25805
+ return typeof data === "object" && data !== null ? data : void 0;
25806
+ }
25807
+ /**
25808
+ * The model a session runs on from its next model call. OpenCode takes any model and level
25809
+ * here, one it can't run among them, so the bridge asks only for those of its catalogue.
25810
+ */
25811
+ async setModel(sessionId, model) {
25812
+ await this.call("POST", `/api/session/${encodeURIComponent(sessionId)}/model`, { model });
25813
+ }
25516
25814
  /**
25517
25815
  * A project's catalogue. OpenCode 2.0 answers the first ask for a project it hasn't loaded with
25518
25816
  * an empty list, and the next with the whole one: a server always has its `build` agent, so an
@@ -25577,6 +25875,13 @@ var OpenCodeClient = class {
25577
25875
  if (typeof id2 !== "string") throw new OpenCodeError("OpenCode took the words and named no inbox item", 502);
25578
25876
  return id2;
25579
25877
  }
25878
+ /**
25879
+ * Compact a session: its inbox takes the ask, which starts at once when it is idle and at its next
25880
+ * step when it is working. `session.compaction.started` and `.ended` or `.failed` say how it goes.
25881
+ */
25882
+ async compact(sessionId) {
25883
+ await this.call("POST", `/api/session/${encodeURIComponent(sessionId)}/compact`, {});
25884
+ }
25580
25885
  /** Words the session hasn't taken yet, taken back. */
25581
25886
  async withdraw(sessionId, item) {
25582
25887
  await this.call("DELETE", `/api/session/${encodeURIComponent(sessionId)}/inbox/${encodeURIComponent(item)}`);
@@ -25626,6 +25931,22 @@ var OpenCodeClient = class {
25626
25931
  const content = Array.isArray(newest?.["content"]) ? newest["content"] : [];
25627
25932
  return content.filter((part) => part["type"] === "text" && typeof part["text"] === "string").map((part) => String(part["text"])).join("\n\n");
25628
25933
  }
25934
+ /** The tokens and model of a session's newest model call, and when: nothing for a session with none. */
25935
+ async lastCall(sessionId) {
25936
+ const read = await this.call(
25937
+ "GET",
25938
+ `/api/session/${encodeURIComponent(sessionId)}/message?type=assistant&order=desc&limit=1`
25939
+ );
25940
+ const newest = (Array.isArray(read["data"]) ? read["data"] : [])[0];
25941
+ const tokens2 = newest?.["tokens"];
25942
+ if (typeof tokens2 !== "object" || tokens2 === null) return void 0;
25943
+ const time3 = newest?.["time"] ?? {};
25944
+ return {
25945
+ tokens: tokens2,
25946
+ ...typeof newest?.["model"] === "object" && newest["model"] !== null ? { model: newest["model"] } : {},
25947
+ at: typeof time3["created"] === "number" ? time3["created"] : 0
25948
+ };
25949
+ }
25629
25950
  /** Every event the server sends, each handed to `hear` as it comes, until the stream ends. */
25630
25951
  async events(hear, signal) {
25631
25952
  const answer = await this.fetcher(`${this.root}/api/event`, {
@@ -25649,7 +25970,7 @@ var OpenCodeClient = class {
25649
25970
  /** `?location[directory]=…` for the catalogues, which are per project. */
25650
25971
  where() {
25651
25972
  const directory = this.address.directory;
25652
- return directory === void 0 ? "" : `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
25973
+ return directory === void 0 ? "" : whereIs(directory);
25653
25974
  }
25654
25975
  async call(method, path, body) {
25655
25976
  const answer = await this.fetcher(`${this.root}${path}`, {
@@ -25665,6 +25986,9 @@ var OpenCodeClient = class {
25665
25986
  return text2 === "" ? {} : JSON.parse(text2);
25666
25987
  }
25667
25988
  };
25989
+ function whereIs(directory) {
25990
+ return `?location%5Bdirectory%5D=${encodeURIComponent(directory)}`;
25991
+ }
25668
25992
  function eventOf(block) {
25669
25993
  const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
25670
25994
  if (data === "") return void 0;
@@ -25696,15 +26020,131 @@ async function failureOf(answer) {
25696
26020
  return new OpenCodeError(message, answer.status, tag, kind);
25697
26021
  }
25698
26022
 
26023
+ // src/models.ts
26024
+ var MODEL_CHOICES_MAX2 = 40;
26025
+ var EFFORT_LEVELS_MAX2 = 8;
26026
+ var LEVEL_WORDS = {
26027
+ none: "None",
26028
+ minimal: "Minimal",
26029
+ low: "Low",
26030
+ medium: "Medium",
26031
+ high: "High",
26032
+ xhigh: "Extra high",
26033
+ max: "Max",
26034
+ thinking: "Thinking"
26035
+ };
26036
+ function writtenOf(model) {
26037
+ return `${model.providerID}/${model.id}`;
26038
+ }
26039
+ function refOf(pick2) {
26040
+ return { ...modelRefOf(pick2.model), ...pick2.effort === void 0 ? {} : { variant: pick2.effort } };
26041
+ }
26042
+ function levelOf(model) {
26043
+ return model?.variant === void 0 || model.variant === "default" ? void 0 : model.variant;
26044
+ }
26045
+ function sameModel(a, b) {
26046
+ return a?.providerID === b?.providerID && a?.id === b?.id && levelOf(a) === levelOf(b);
26047
+ }
26048
+ function levelWords(id2) {
26049
+ return (LEVEL_WORDS[id2] ?? `${id2.charAt(0).toUpperCase()}${id2.slice(1)}`).slice(0, 40);
26050
+ }
26051
+ function runnable(catalogue) {
26052
+ return catalogue.filter((model) => model.enabled !== false && model.status !== "deprecated");
26053
+ }
26054
+ function levelsOf(model) {
26055
+ const seen = /* @__PURE__ */ new Set();
26056
+ const each = (model?.variants ?? []).filter((variant) => !seen.has(variant.id) && seen.add(variant.id));
26057
+ return each.slice(-EFFORT_LEVELS_MAX2).map((variant) => ({ id: variant.id, label: levelWords(variant.id) }));
26058
+ }
26059
+ var fitting = (choice, effort) => choice?.efforts.some((level) => level.id === effort) === true ? effort : void 0;
26060
+ function modelCopy(input2) {
26061
+ const { pick: pick2, fallback } = input2;
26062
+ const runsOn = pick2 === void 0 ? fallback?.model : refOf(pick2);
26063
+ if (runsOn === void 0) return void 0;
26064
+ const offered2 = runnable(input2.catalogue);
26065
+ const names = /* @__PURE__ */ new Map();
26066
+ for (const model of offered2) names.set(model.name, (names.get(model.name) ?? 0) + 1);
26067
+ const choices = offered2.map((model) => ({
26068
+ id: writtenOf(model),
26069
+ label: ((names.get(model.name) ?? 0) > 1 ? `${model.name} \xB7 ${model.providerID}` : model.name || model.id).slice(0, 80),
26070
+ efforts: levelsOf(model)
26071
+ }));
26072
+ const named = (ref) => {
26073
+ const id2 = writtenOf(ref);
26074
+ const known = choices.find((choice2) => choice2.id === id2);
26075
+ if (known !== void 0) return known;
26076
+ const level = levelOf(ref);
26077
+ const choice = { id: id2, label: ref.id.slice(0, 80), efforts: level === void 0 ? [] : [{ id: level, label: levelWords(level) }] };
26078
+ choices.push(choice);
26079
+ return choice;
26080
+ };
26081
+ const now = named(runsOn);
26082
+ const otherwise = fallback === void 0 ? void 0 : named(fallback.model);
26083
+ const effort = fitting(now, levelOf(runsOn));
26084
+ const fallbackEffort = fitting(otherwise, levelOf(fallback?.model));
26085
+ const keep = /* @__PURE__ */ new Set([now.id, ...otherwise === void 0 ? [] : [otherwise.id]]);
26086
+ let others = MODEL_CHOICES_MAX2 - keep.size;
26087
+ return {
26088
+ model: now.id,
26089
+ ...effort === void 0 ? {} : { effort },
26090
+ own: pick2 !== void 0,
26091
+ ...fallback === void 0 || otherwise === void 0 ? {} : {
26092
+ default: {
26093
+ label: fallback.label.slice(0, 80),
26094
+ model: otherwise.id,
26095
+ ...fallbackEffort === void 0 ? {} : { effort: fallbackEffort }
26096
+ }
26097
+ },
26098
+ choices: choices.filter((choice) => keep.has(choice.id) || others-- > 0),
26099
+ editable: true
26100
+ };
26101
+ }
26102
+ function pickOf(change, catalogue) {
26103
+ if (change.model === null) return null;
26104
+ const model = runnable(catalogue).find((each) => writtenOf(each) === change.model);
26105
+ if (model === void 0) throw new Error(`OpenCode doesn't run ${change.model} here.`);
26106
+ if (change.effort === null) return { model: change.model };
26107
+ if (!(model.variants ?? []).some((variant) => variant.id === change.effort)) {
26108
+ throw new Error(`${model.name || model.id} has no ${levelWords(change.effort)} level.`);
26109
+ }
26110
+ return { model: change.model, effort: change.effort };
26111
+ }
26112
+
26113
+ // src/windows.ts
26114
+ var OWN_LIMIT = "OpenCode compacts it itself when it is nearly full, as its config says.";
26115
+ function usedOf(tokens2) {
26116
+ return tokens2.input + tokens2.cache.read + tokens2.cache.write + tokens2.output + tokens2.reasoning;
26117
+ }
26118
+ function compactedTo(data) {
26119
+ const tokens2 = data["tokens"];
26120
+ const summary = typeof tokens2?.output === "number" ? tokens2.output : 0;
26121
+ const recent = typeof data["recent"] === "string" ? data["recent"] : "";
26122
+ return Math.round(summary + recent.length / 4);
26123
+ }
26124
+ function windowCopy(kept, compacting) {
26125
+ return {
26126
+ used: Math.max(0, Math.round(kept.used)),
26127
+ window: Math.max(1, Math.round(kept.window)),
26128
+ ...kept.model === void 0 ? {} : { model: kept.model.slice(0, 120) },
26129
+ as_of: kept.asOf,
26130
+ ...compacting === void 0 ? {} : { compacting },
26131
+ ...kept.last === void 0 ? {} : { last: kept.last },
26132
+ ...kept.longTurn === void 0 ? {} : { long_turn: { ...kept.longTurn, folds: Math.min(kept.longTurn.folds, LONG_TURN_FOLDS_MAX) } },
26133
+ compacts: true,
26134
+ editable: false,
26135
+ why_read_only: OWN_LIMIT
26136
+ };
26137
+ }
26138
+
25699
26139
  // src/bridge.ts
25700
- var WANTS = ["agents", "new_agents", "forks"];
26140
+ var WANTS = ["agents", "new_spaces", "new_agents", "forks", "models", "context_files", "context_windows"];
25701
26141
  function wantsFor(ownServer2) {
25702
26142
  return ownServer2 ? [...WANTS, "restarts"] : WANTS;
25703
26143
  }
25704
26144
  function freeFolder(root, name, taken) {
25705
26145
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
25706
26146
  for (let count = 1; ; count += 1) {
25707
- const folder2 = join5(root, count === 1 ? slug : `${slug}-${count}`);
26147
+ const folder2 = join6(root, count === 1 ? slug : `${slug}-${count}`);
25708
26148
  if (!taken.includes(folder2)) return folder2;
25709
26149
  }
25710
26150
  }
@@ -25735,6 +26175,9 @@ var Bridge = class {
25735
26175
  constructor(options) {
25736
26176
  this.options = options;
25737
26177
  this.log = options.log ?? (() => void 0);
26178
+ this.places = options.places ?? placesOf(process.env, homedir4());
26179
+ const known = options.folders !== void 0 || options.directory !== void 0;
26180
+ this.files = known ? new ContextFiles(this.places, options.folders) : void 0;
25738
26181
  }
25739
26182
  options;
25740
26183
  agents = [];
@@ -25770,6 +26213,15 @@ var Bridge = class {
25770
26213
  restarting;
25771
26214
  /** Why OpenCode's server isn't there, where the bridge knows more than that it doesn't answer. */
25772
26215
  why;
26216
+ /** The files OpenCode builds each agent from, where the bridge knows each agent's folder. */
26217
+ files;
26218
+ places;
26219
+ /** Each folder's project root, as OpenCode said it. */
26220
+ roots = /* @__PURE__ */ new Map();
26221
+ /** Whether the server reads the files its config's `instructions` names, asked once a start or a restart. */
26222
+ readsNamed;
26223
+ /** Agents compacting now, by whose sessions they are: waiting for their session's next step, or under way, and the ask from Chips. */
26224
+ compacting = /* @__PURE__ */ new Map();
25773
26225
  /** Register the agents, start hearing OpenCode and what is said in Chips, and say how it is. */
25774
26226
  async start() {
25775
26227
  const { chips: chips2 } = this.options;
@@ -25789,9 +26241,17 @@ var Bridge = class {
25789
26241
  chips2.on("agent.fork", (fork) => {
25790
26242
  this.copy(fork).catch((error62) => this.log(`a copy went wrong: ${messageOf(error62)}`));
25791
26243
  });
26244
+ chips2.onSpaceAsked((asked) => {
26245
+ this.claim(asked).catch((error62) => this.log(`a space from the + went wrong: ${messageOf(error62)}`));
26246
+ });
25792
26247
  chips2.on("feedback", (feedback2) => {
25793
26248
  this.notRight(feedback2).catch((error62) => this.log(`a second look went wrong: ${messageOf(error62)}`));
25794
26249
  });
26250
+ for (const agent of await chips2.agents.list().catch(() => [])) {
26251
+ if (agent.state !== "archived") await this.sendModel(agent.agentId);
26252
+ }
26253
+ await this.sendContexts();
26254
+ await this.sendWindows();
25795
26255
  await this.sayHowItIs();
25796
26256
  this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
25797
26257
  this.health.unref();
@@ -25823,6 +26283,7 @@ var Bridge = class {
25823
26283
  try {
25824
26284
  await again2();
25825
26285
  this.why = void 0;
26286
+ this.readsNamed = void 0;
25826
26287
  this.wake?.();
25827
26288
  } catch (error62) {
25828
26289
  const why = messageOf(error62).replace(/\s+/g, " ").trim();
@@ -25844,16 +26305,16 @@ var Bridge = class {
25844
26305
  */
25845
26306
  async seatMain(config3, known) {
25846
26307
  const { chips: chips2 } = this.options;
25847
- const before = known.find((agent) => agent.lead);
26308
+ const before2 = known.find((agent) => agent.lead);
25848
26309
  const made = known.some((agent) => agent.key === chips2.harnessId);
25849
- if (!made && before !== void 0) {
25850
- await this.goHome(before, known);
25851
- return before;
26310
+ if (!made && before2 !== void 0) {
26311
+ await this.goHome(before2, known);
26312
+ return before2;
25852
26313
  }
25853
26314
  const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
25854
26315
  await this.goHome(main2, known);
25855
- if (before !== void 0 && before.agentId !== main2.agentId) {
25856
- await chips2.agents.register({ key: before.key, name: before.name, state: "archived" }).catch((error62) => this.log(`${before.name} stays: ${String(error62)}`));
26316
+ if (before2 !== void 0 && before2.agentId !== main2.agentId) {
26317
+ await chips2.agents.register({ key: before2.key, name: before2.name, state: "archived" }).catch((error62) => this.log(`${before2.name} stays: ${String(error62)}`));
25857
26318
  }
25858
26319
  return main2;
25859
26320
  }
@@ -25915,6 +26376,47 @@ var Bridge = class {
25915
26376
  });
25916
26377
  this.ours.add(agent.agentId);
25917
26378
  this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
26379
+ await this.sendModel(agent.agentId);
26380
+ await this.sendContexts();
26381
+ }
26382
+ /**
26383
+ * A space a person asked for from Chips's + (Chips ADR 0241), or beside another harness: claimed
26384
+ * under a key of its own, named from its first words, and Chips makes the new agent the key is
26385
+ * for and puts it in the space. That agent answers the first words, which are the ask's and are
26386
+ * not heard again as a message, as the first OpenCode agent, in a session and a folder of its
26387
+ * own. A claim heard again finds the space claimed already, and the agent the same.
26388
+ */
26389
+ async claim(asked) {
26390
+ const { chips: chips2 } = this.options;
26391
+ const { spaceId, firstMessage } = asked;
26392
+ const key = `opencode:space:${spaceId}`;
26393
+ await chips2.claimSpace(spaceId, key, titleFrom(firstMessage.text));
26394
+ const agent = (await chips2.agents.list()).find((each) => each.key === key);
26395
+ if (agent === void 0) {
26396
+ this.log(`claimed ${spaceId}, and Chips put no agent there`);
26397
+ return;
26398
+ }
26399
+ this.ours.add(agent.agentId);
26400
+ this.log(`${agent.name} claimed ${spaceId}`);
26401
+ await this.sendModel(agent.agentId);
26402
+ await this.sendContexts();
26403
+ const context = await chips2.roomContext(spaceId, { limit: 20 }).catch(() => void 0);
26404
+ const said = context?.lines.find((line) => line.message_id === firstMessage.messageId)?.from;
26405
+ const message = {
26406
+ type: "message.new",
26407
+ space_id: spaceId,
26408
+ message_id: firstMessage.messageId,
26409
+ text: firstMessage.text,
26410
+ from: {
26411
+ participant_id: asked.askedBy,
26412
+ kind: "person",
26413
+ display_name: said?.name ?? "Someone",
26414
+ role: said?.role ?? "member",
26415
+ ...said?.workspace_role === void 0 ? {} : { workspace_role: said.workspace_role }
26416
+ },
26417
+ for: [agent.agentId]
26418
+ };
26419
+ await this.hear(message);
25918
26420
  }
25919
26421
  /** A person's Fork, made once however often it is heard at a time. */
25920
26422
  copy(fork) {
@@ -25976,6 +26478,8 @@ var Bridge = class {
25976
26478
  this.ours.add(copy.agentId);
25977
26479
  if (kept.session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] = kept.session;
25978
26480
  if (kept.folder !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: kept.folder };
26481
+ const pick2 = state2.models?.[who];
26482
+ if (pick2 !== void 0) state2.models = { ...state2.models, [copy.agentId]: pick2 };
25979
26483
  saveState(state2);
25980
26484
  if (copied !== void 0) {
25981
26485
  await opencode2.update(copied.id, {
@@ -25986,6 +26490,7 @@ var Bridge = class {
25986
26490
  this.log(
25987
26491
  `${copy.name}, a copy of ${name}, made${kept.session === void 0 ? " new" : ` from ${kept.session}`}${copy.attached === false && copy.why !== void 0 ? `; not in the room: ${copy.why}` : ""}`
25988
26492
  );
26493
+ void this.sendModel(copy.agentId).then(() => this.sendContexts());
25989
26494
  }
25990
26495
  /** The session of an agent's to copy: its room's, or where it is in none, the last it had. */
25991
26496
  sessionToCopy(who, room) {
@@ -25993,6 +26498,304 @@ var Bridge = class {
25993
26498
  if (room !== void 0 && room !== null) return sessions[`${who}|${room}`];
25994
26499
  return Object.entries(sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
25995
26500
  }
26501
+ // ------------------------------------------------------------------ what a person changes from an agent's page
26502
+ /**
26503
+ * Which OpenCode agent a Chips agent of the harness's runs as, and whose its sessions and its
26504
+ * pick are, as `roomOf` says: the key of one configured here, else its Chips id. Nothing for an
26505
+ * agent not the harness's.
26506
+ */
26507
+ async whoseIs(agentId2) {
26508
+ const agent = await this.runnerOf(agentId2);
26509
+ if (agent === void 0 || agentId2 === this.options.chips.harnessId) return void 0;
26510
+ return { agent, who: agentId2 === agent.agentId ? agent.key : agentId2 };
26511
+ }
26512
+ /**
26513
+ * What an agent runs on with no pick of its own: its model in the bridge's config, else its
26514
+ * OpenCode agent's, else the server's default.
26515
+ */
26516
+ async fallbackOf(agent) {
26517
+ if (agent.model !== void 0) return { model: agent.model, label: "As in config.json" };
26518
+ const { opencode: opencode2 } = this.options;
26519
+ const own2 = (await opencode2.agents().catch(() => [])).find((each) => each.id === agent.config.agent)?.model;
26520
+ if (own2 !== void 0) return { model: own2, label: "OpenCode's default" };
26521
+ const model = await opencode2.defaultModel();
26522
+ return model === void 0 ? void 0 : { model: { providerID: model.providerID, id: model.id }, label: "OpenCode's default" };
26523
+ }
26524
+ /** An agent's model, as OpenCode and the bridge have it now: nothing for an agent not the harness's. */
26525
+ async modelCopyOf(agentId2) {
26526
+ const whose = await this.whoseIs(agentId2);
26527
+ if (whose === void 0) return void 0;
26528
+ const [catalogue, fallback] = await Promise.all([this.options.opencode.models(), this.fallbackOf(whose.agent)]);
26529
+ return modelCopy({ catalogue, pick: this.options.state.models?.[whose.who], fallback });
26530
+ }
26531
+ /** An agent's model sent, where it changed since it last was (Chips takes the same copy once). */
26532
+ async sendModel(agentId2) {
26533
+ try {
26534
+ const copy = await this.modelCopyOf(agentId2);
26535
+ if (copy !== void 0) await this.options.chips.settings.model(agentId2, copy);
26536
+ } catch (error62) {
26537
+ this.log(`the model of ${agentId2} not sent: ${messageOf(error62)}`);
26538
+ }
26539
+ }
26540
+ /**
26541
+ * A person's pick of an agent's model and level from its page (Chips ADR 0241): kept with the
26542
+ * bridge's state, and given to each of its sessions before its next message there. Only a model
26543
+ * OpenCode runs, at one of its levels; `null` is its default again. Answered with its copy.
26544
+ */
26545
+ async modelChanged(change) {
26546
+ const whose = await this.whoseIs(change.agent_id);
26547
+ if (whose === void 0) throw new Refuse("This harness has no such agent.");
26548
+ const { opencode: opencode2, state: state2, saveState } = this.options;
26549
+ let pick2;
26550
+ try {
26551
+ pick2 = pickOf(change, await opencode2.models());
26552
+ } catch (error62) {
26553
+ throw new Refuse(messageOf(error62), await this.modelCopyOf(change.agent_id).catch(() => void 0));
26554
+ }
26555
+ const models = { ...state2.models };
26556
+ if (pick2 === null) delete models[whose.who];
26557
+ else models[whose.who] = pick2;
26558
+ state2.models = models;
26559
+ saveState(state2);
26560
+ const name = await this.nameOf(whose);
26561
+ this.log(`${name} runs on ${pick2 === null ? "its default" : `${pick2.model}${pick2.effort === void 0 ? "" : ` at ${pick2.effort}`}`}, from Chips`);
26562
+ const copy = await this.modelCopyOf(change.agent_id);
26563
+ if (copy === void 0) throw new Refuse("OpenCode names no model for it.");
26564
+ return copy;
26565
+ }
26566
+ /**
26567
+ * The session a message goes to, put on the model its agent runs on, where it isn't on it
26568
+ * already (Chips ADR 0241): its pick, else its model in the bridge's config. A session that was
26569
+ * put on another before, with neither now, goes back to its OpenCode agent's or the server's.
26570
+ */
26571
+ async modelled(sessionId, { who, agent }) {
26572
+ const { opencode: opencode2, state: state2 } = this.options;
26573
+ const pick2 = state2.models?.[who];
26574
+ try {
26575
+ const has = (await opencode2.session(sessionId))["model"];
26576
+ const want = pick2 !== void 0 ? refOf(pick2) : agent.model ?? (has === void 0 ? void 0 : (await this.fallbackOf(agent))?.model);
26577
+ if (want === void 0 || sameModel(want, has)) return;
26578
+ await opencode2.setModel(sessionId, want);
26579
+ this.log(`${sessionId} runs on ${writtenOf(want)}${want.variant === void 0 ? "" : ` at ${want.variant}`} from now`);
26580
+ } catch (error62) {
26581
+ if (!(error62 instanceof OpenCodeError && error62.tag === "SessionNotFoundError")) {
26582
+ this.log(`${sessionId} stays on its model: ${messageOf(error62)}`);
26583
+ }
26584
+ }
26585
+ }
26586
+ /** The folder an agent's sessions work in, its own or the one project's; none where the bridge knows neither. */
26587
+ async agentFolder(whose) {
26588
+ return await this.folderFor(whose) ?? this.options.directory;
26589
+ }
26590
+ /**
26591
+ * What OpenCode reads for a folder beside its `AGENTS.md` files: where its walk up stops, and
26592
+ * from 2.0.30 the files its config's `instructions` names. A folder whose project OpenCode can't
26593
+ * say is its own project.
26594
+ */
26595
+ async readingOf(folder2) {
26596
+ const { opencode: opencode2 } = this.options;
26597
+ const { home: home3 } = this.places;
26598
+ let root = this.roots.get(folder2);
26599
+ if (root === void 0) {
26600
+ root = await opencode2.projectRoot(folder2).catch(() => folder2);
26601
+ this.roots.set(folder2, root);
26602
+ }
26603
+ this.readsNamed ??= opencode2.version().then(
26604
+ (version3) => !before(version3, READS_NAMED),
26605
+ () => false
26606
+ );
26607
+ const sources = await this.readsNamed ? await opencode2.instructions(folder2).catch(() => []) : [];
26608
+ return { stop: within(folder2, home3) ? home3 : root, named: namedFiles(sources, folder2, root, home3) };
26609
+ }
26610
+ /** Each agent of the harness's, with the folder its sessions work in and what OpenCode reads there. */
26611
+ async agentReadings() {
26612
+ const read = [];
26613
+ for (const agent of await this.options.chips.agents.list().catch(() => [])) {
26614
+ if (agent.state === "archived") continue;
26615
+ const whose = await this.whoseIs(agent.agentId);
26616
+ const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
26617
+ if (folder2 !== void 0) read.push({ agentId: agent.agentId, folder: folder2, reading: await this.readingOf(folder2) });
26618
+ }
26619
+ return read;
26620
+ }
26621
+ /** The harness's own set and each agent's files, as they are on disk now: each sent where it changed. */
26622
+ async sendContexts() {
26623
+ const files = this.files;
26624
+ if (files === void 0) return;
26625
+ try {
26626
+ const read = await this.agentReadings();
26627
+ const readings = new Map(read.map((each) => [each.folder, each.reading]));
26628
+ await this.options.chips.settings.harnessContext(files.harnessCopy(readings, read.map((each) => each.folder)));
26629
+ for (const { agentId: agentId2, folder: folder2, reading } of read) {
26630
+ await this.options.chips.settings.context(agentId2, files.agentCopy(folder2, reading));
26631
+ }
26632
+ } catch (error62) {
26633
+ this.log(`the agents' files not sent: ${messageOf(error62)}`);
26634
+ }
26635
+ }
26636
+ /** One agent's files sent, where they changed. */
26637
+ async sendContext(agentId2) {
26638
+ const whose = await this.whoseIs(agentId2);
26639
+ const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
26640
+ if (this.files === void 0 || folder2 === void 0) return;
26641
+ try {
26642
+ await this.options.chips.settings.context(agentId2, this.files.agentCopy(folder2, await this.readingOf(folder2)));
26643
+ } catch (error62) {
26644
+ this.log(`the files of ${agentId2} not sent: ${messageOf(error62)}`);
26645
+ }
26646
+ }
26647
+ /**
26648
+ * A person's edit of a file from Chips (Chips ADR 0134): saved where OpenCode reads it, only if
26649
+ * it is still the version they changed, and answered with the copy as it is now. OpenCode reads
26650
+ * it again before the agent's next model call, and tells the agent what changed: every agent
26651
+ * working in that folder, or for the harness's own set every agent that reads the file.
26652
+ */
26653
+ async contextEdited(edit) {
26654
+ const files = this.files;
26655
+ if (files === void 0) throw new Refuse("This bridge doesn't know where its agents' files are.");
26656
+ if (edit.agent_id === void 0) {
26657
+ const read = await this.agentReadings();
26658
+ const readings = new Map(read.map((each) => [each.folder, each.reading]));
26659
+ const folders = read.map((each) => each.folder);
26660
+ await files.saveShared(readings, folders, edit);
26661
+ this.log(`OpenCode's ${edit.name} saved from Chips`);
26662
+ return files.harnessCopy(readings, folders);
26663
+ }
26664
+ const whose = await this.whoseIs(edit.agent_id);
26665
+ const folder2 = whose === void 0 ? void 0 : await this.agentFolder(whose);
26666
+ if (whose === void 0 || folder2 === void 0) throw new Refuse("This harness has no such agent.");
26667
+ const reading = await this.readingOf(folder2);
26668
+ await files.saveAgent(folder2, reading, edit);
26669
+ this.log(`${await this.nameOf(whose)}'s ${edit.name} saved from Chips`);
26670
+ for (const other of await this.agentReadings()) {
26671
+ if (other.agentId !== edit.agent_id && other.folder === folder2) void this.sendContext(other.agentId);
26672
+ }
26673
+ return files.agentCopy(folder2, reading);
26674
+ }
26675
+ // ------------------------------------------------------------------ how full each agent is (Chips ADRs 0146, 0241)
26676
+ /** The Chips agent whose sessions are `who`'s: a configured agent's, or the agent itself. */
26677
+ agentIdOf(who) {
26678
+ return this.agents.find((agent) => agent.key === who)?.agentId ?? who;
26679
+ }
26680
+ /** Whose a session is, by the rooms the bridge keeps: nothing for one it didn't make. */
26681
+ whoseSession(sessionId) {
26682
+ const id2 = Object.entries(this.options.state.sessions).find(([, each]) => each === sessionId)?.[0];
26683
+ return id2?.slice(0, id2.lastIndexOf("|"));
26684
+ }
26685
+ /** An agent's window copy sent, and kept with the bridge's state: nothing until it has said how full it is. */
26686
+ async sendWindow(who) {
26687
+ const kept = this.options.state.windows?.[who];
26688
+ if (kept === void 0) return;
26689
+ this.options.saveState(this.options.state);
26690
+ await this.options.chips.settings.contextWindow(this.agentIdOf(who), windowCopy(kept, this.compacting.get(who)?.state)).catch((error62) => this.log(`the context window of ${who} not sent: ${messageOf(error62)}`));
26691
+ }
26692
+ /**
26693
+ * Each agent's window as it last said, at start. One the bridge kept nothing of, from before it
26694
+ * kept them, is read from the newest model call of its sessions.
26695
+ */
26696
+ async sendWindows() {
26697
+ const { opencode: opencode2, state: state2 } = this.options;
26698
+ const whose = new Set(Object.keys(state2.sessions).map((id2) => id2.slice(0, id2.lastIndexOf("|"))));
26699
+ for (const who of whose) {
26700
+ if (state2.windows?.[who] === void 0) {
26701
+ let newest;
26702
+ for (const [id2, sessionId] of Object.entries(state2.sessions)) {
26703
+ if (!id2.startsWith(`${who}|`)) continue;
26704
+ const call = await opencode2.lastCall(sessionId).catch(() => void 0);
26705
+ if (call !== void 0 && (newest === void 0 || call.at > newest.call.at)) newest = { sessionId, call };
26706
+ }
26707
+ const model = newest?.call.model === void 0 ? void 0 : writtenOf(newest.call.model);
26708
+ const window = model === void 0 ? void 0 : await this.windowOf(model);
26709
+ if (newest === void 0 || model === void 0 || window === void 0) continue;
26710
+ const asOf = new Date(newest.call.at > 0 ? newest.call.at : Date.now()).toISOString();
26711
+ state2.windows = { ...state2.windows, [who]: { used: usedOf(newest.call.tokens), window, model, asOf, session: newest.sessionId } };
26712
+ }
26713
+ await this.sendWindow(who);
26714
+ }
26715
+ }
26716
+ /** A turn ended: how full it got, where that passed half the window, and the copy as it is now. */
26717
+ async turnHeld(turn) {
26718
+ const kept = this.options.state.windows?.[turn.who];
26719
+ if (kept === void 0) return;
26720
+ if (turn.peak > kept.window / 2) {
26721
+ kept.longTurn = { at: (/* @__PURE__ */ new Date()).toISOString(), peak: Math.round(turn.peak), folds: turn.folds };
26722
+ }
26723
+ await this.sendWindow(turn.who);
26724
+ }
26725
+ /**
26726
+ * What OpenCode says of a compaction of one of the agents' sessions: under way, done, with how
26727
+ * much it held before and after, or failed. A Compact now asked from Chips that failed is
26728
+ * refused after all, in OpenCode's words.
26729
+ */
26730
+ compactionHeard(sessionId, event) {
26731
+ const who = this.whoseSession(sessionId);
26732
+ const kept = who === void 0 ? void 0 : this.options.state.windows?.[who];
26733
+ if (who === void 0 || kept === void 0) return;
26734
+ const asked = this.compacting.get(who);
26735
+ if (event.type === "session.compaction.started") {
26736
+ if (asked === void 0) this.compacting.set(who, { state: "running" });
26737
+ else asked.state = "running";
26738
+ void this.sendWindow(who);
26739
+ return;
26740
+ }
26741
+ if (event.type === "session.compaction.ended") {
26742
+ this.compacting.delete(who);
26743
+ const to = compactedTo(event.data);
26744
+ const by = event.data["reason"] === "manual" ? "asked" : "itself";
26745
+ kept.last = { at: (/* @__PURE__ */ new Date()).toISOString(), from: Math.round(kept.used), to, by };
26746
+ kept.used = to;
26747
+ kept.asOf = kept.last.at;
26748
+ kept.session = sessionId;
26749
+ const turn = this.turns.get(sessionId);
26750
+ if (turn !== void 0 && by === "itself") turn.folds += 1;
26751
+ this.log(`${who} compacted (${by}), ${kept.last.from} to ${to} tokens`);
26752
+ void this.sendWindow(who);
26753
+ return;
26754
+ }
26755
+ if (event.type === "session.compaction.failed") {
26756
+ this.compacting.delete(who);
26757
+ const error62 = event.data["error"] ?? {};
26758
+ const why = text(error62["message"]) ?? "OpenCode didn't compact it.";
26759
+ this.log(`${who} not compacted: ${why}`);
26760
+ if (asked?.changeId === void 0) {
26761
+ void this.sendWindow(who);
26762
+ return;
26763
+ }
26764
+ void this.options.chips.settings.contextWindowFailed(this.agentIdOf(who), asked.changeId, why, windowCopy(kept)).catch((failed) => this.log(`the context window of ${who} not sent: ${messageOf(failed)}`));
26765
+ }
26766
+ }
26767
+ /**
26768
+ * A person's Compact now (Chips ADR 0146): asked of OpenCode for the session the agent said it
26769
+ * was in last, which takes it at once when idle and at its next step when working, and answered
26770
+ * with the copy saying so. How it goes comes in OpenCode's own events.
26771
+ */
26772
+ async windowCompact(ask2) {
26773
+ const whose = await this.whoseIs(ask2.agent_id);
26774
+ if (whose === void 0) throw new Refuse("This harness has no such agent.");
26775
+ const { who } = whose;
26776
+ const kept = this.options.state.windows?.[who];
26777
+ if (kept === void 0) throw new Refuse(`${await this.nameOf(whose)} hasn't said how full it is yet.`);
26778
+ if (this.compacting.has(who)) throw new Refuse("It is compacting already.", windowCopy(kept, this.compacting.get(who)?.state));
26779
+ const { opencode: opencode2 } = this.options;
26780
+ const busy = this.turns.has(kept.session) || (await opencode2.active().catch(() => /* @__PURE__ */ new Set())).has(kept.session);
26781
+ this.compacting.set(who, { state: busy ? "queued" : "running", changeId: ask2.change_id });
26782
+ try {
26783
+ await opencode2.compact(kept.session);
26784
+ } catch (error62) {
26785
+ this.compacting.delete(who);
26786
+ throw new Refuse(`OpenCode didn't take it: ${messageOf(error62)}`, windowCopy(kept));
26787
+ }
26788
+ this.log(`${await this.nameOf(whose)} compacts ${busy ? "at its session's next step" : "now"}, from Chips`);
26789
+ return windowCopy(kept, this.compacting.get(who)?.state);
26790
+ }
26791
+ /** A person's limit: OpenCode keeps its own, set in its config, and the copy says so. */
26792
+ async windowChange(ask2) {
26793
+ const whose = await this.whoseIs(ask2.agent_id);
26794
+ const kept = whose === void 0 ? void 0 : this.options.state.windows?.[whose.who];
26795
+ if (whose === void 0) throw new Refuse("This harness has no such agent.");
26796
+ const copy = kept === void 0 ? void 0 : windowCopy(kept, this.compacting.get(whose.who)?.state);
26797
+ throw new Refuse(copy?.why_read_only ?? "OpenCode keeps its own limit.", copy);
26798
+ }
25996
26799
  // ------------------------------------------------------------------ what is said to it
25997
26800
  /** A message for some of its agents: each takes a turn in its room, one after another there. */
25998
26801
  async hear(message, own2 = {}) {
@@ -26104,6 +26907,10 @@ var Bridge = class {
26104
26907
  run: run2,
26105
26908
  speaker,
26106
26909
  agent,
26910
+ who: room.who,
26911
+ agentId: speakerId ?? agent.agentId,
26912
+ peak: 0,
26913
+ folds: 0,
26107
26914
  sessionId,
26108
26915
  waiting: /* @__PURE__ */ new Set(),
26109
26916
  taken: /* @__PURE__ */ new Set(),
@@ -26135,6 +26942,9 @@ var Bridge = class {
26135
26942
  if (this.turns.get(sessionId) === turn) this.turns.delete(sessionId);
26136
26943
  if (last.outcome === "succeeded") await this.finish(turn);
26137
26944
  else await this.ended(turn, last);
26945
+ await this.sendModel(turn.agentId);
26946
+ await this.sendContext(turn.agentId);
26947
+ await this.turnHeld(turn);
26138
26948
  }
26139
26949
  /** The words into the room's session: the one it had, or a new one when OpenCode no longer has it. */
26140
26950
  async give({ id: id2, who, agent, spaceId }, text2) {
@@ -26146,16 +26956,18 @@ var Bridge = class {
26146
26956
  }
26147
26957
  if (kept !== void 0) {
26148
26958
  try {
26959
+ await this.modelled(kept, { who, agent });
26149
26960
  return { sessionId: kept, given: await this.prompted(kept, text2) };
26150
26961
  } catch (error62) {
26151
26962
  if (!(error62 instanceof OpenCodeError && error62.tag === "SessionNotFoundError")) throw error62;
26152
26963
  this.log(`${agent.config.name}'s session for ${spaceId} is gone from OpenCode: a new one`);
26153
26964
  }
26154
26965
  }
26966
+ const pick2 = state2.models?.[who];
26155
26967
  const made = await opencode2.newSession({
26156
26968
  title: `Chips space ${spaceId}`,
26157
26969
  agent: agent.config.agent,
26158
- model: agent.model,
26970
+ model: pick2 === void 0 ? agent.model : refOf(pick2),
26159
26971
  directory: await this.folderFor({ who, agent }),
26160
26972
  metadata: { chips_space: spaceId, chips_agent: who }
26161
26973
  });
@@ -26292,6 +27104,11 @@ var Bridge = class {
26292
27104
  return;
26293
27105
  }
26294
27106
  if (event.type === "session.forked") return;
27107
+ if (event.type.startsWith("session.compaction.")) {
27108
+ const going = this.turns.get(sessionId);
27109
+ if (going === void 0) this.compactionHeard(sessionId, event);
27110
+ else going.chain = going.chain.then(() => this.compactionHeard(sessionId, event));
27111
+ }
26295
27112
  const own2 = this.turns.get(sessionId);
26296
27113
  if (own2 === void 0 && this.early.has(sessionId)) {
26297
27114
  this.early.get(sessionId)?.push(event);
@@ -26463,6 +27280,9 @@ var Bridge = class {
26463
27280
  const limit = await this.windowOf(model);
26464
27281
  if (limit === void 0) return;
26465
27282
  await turn.run.context({ usedTokens: prompt + written, limitTokens: limit, model }).catch(() => void 0);
27283
+ const windows = this.options.state.windows ??= {};
27284
+ windows[turn.who] = { ...windows[turn.who], used: prompt + written, window: limit, model, asOf: (/* @__PURE__ */ new Date()).toISOString(), session: turn.sessionId };
27285
+ turn.peak = Math.max(turn.peak, prompt + written);
26466
27286
  }
26467
27287
  /** A model's context window, from OpenCode's catalogue, read once (and once more for each it lacked). */
26468
27288
  async windowOf(model) {
@@ -26639,6 +27459,21 @@ var Bridge = class {
26639
27459
  if (retryable) void this.sayHowItIs();
26640
27460
  }
26641
27461
  };
27462
+ function settingsFor(bridge2) {
27463
+ const active = () => {
27464
+ const running = bridge2();
27465
+ if (running === void 0) throw new Refuse("The harness is still starting.");
27466
+ return running;
27467
+ };
27468
+ return {
27469
+ models: (change) => active().modelChanged(change),
27470
+ context: (edit) => active().contextEdited(edit),
27471
+ contextWindows: {
27472
+ compact: (ask2) => active().windowCompact(ask2),
27473
+ change: (ask2) => active().windowChange(ask2)
27474
+ }
27475
+ };
27476
+ }
26642
27477
  function decisionOf(decision, command = false) {
26643
27478
  if (!decision.allowed) return "reject";
26644
27479
  return decision.scope === "always" && !command ? "always" : "once";
@@ -26727,6 +27562,12 @@ function spoken(words4) {
26727
27562
  return end2 === -1 ? words4 : words4.slice(end2 + THOUGHT_ENDS.length);
26728
27563
  }
26729
27564
  var THOUGHT_ENDS = "</think>";
27565
+ function titleFrom(text2) {
27566
+ const line = text2.trim().split("\n")[0] ?? "";
27567
+ if (line.length <= 48) return line;
27568
+ const cut2 = line.slice(0, 48);
27569
+ return `${cut2.slice(0, Math.max(cut2.lastIndexOf(" "), 24))}\u2026`;
27570
+ }
26730
27571
  function forkName(original) {
26731
27572
  return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
26732
27573
  }
@@ -26791,12 +27632,12 @@ function lastLine(said) {
26791
27632
  }
26792
27633
 
26793
27634
  // src/keep.ts
26794
- import { homedir as homedir4 } from "node:os";
26795
- import { basename as basename3, join as join6, resolve as resolve2 } from "node:path";
27635
+ import { homedir as homedir5 } from "node:os";
27636
+ import { basename as basename3, join as join7, resolve as resolve2 } from "node:path";
26796
27637
  import { fileURLToPath as fileURLToPath2 } from "node:url";
26797
27638
  var flags = process.argv.slice(2);
26798
27639
  var words3 = flags.filter((word) => !word.startsWith("--"));
26799
- var whole = (word) => resolve2(word.startsWith("~/") ? join6(homedir4(), word.slice(2)) : word);
27640
+ var whole = (word) => resolve2(word.startsWith("~/") ? join7(homedir5(), word.slice(2)) : word);
26800
27641
  var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
26801
27642
  var self = fileURLToPath2(import.meta.url);
26802
27643
  var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
@@ -26814,7 +27655,7 @@ function stopIfAsked(label2) {
26814
27655
  if (stopped2.state === "theirs") end(`${stopped2.path} wasn't written by this bridge: it was left as it is.`, 1);
26815
27656
  end(`Nothing called ${label2} was kept running here.`, 0);
26816
27657
  }
26817
- function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
27658
+ function keepIfAsked(label2, url2, names, log2, before2 = () => void 0) {
26818
27659
  if (!flags.includes("--keep")) return;
26819
27660
  const node2 = keptNode();
26820
27661
  const kept = keepRunning({
@@ -26824,7 +27665,7 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
26824
27665
  environment: keepEnvironment(url2, process.env, names, node2)
26825
27666
  });
26826
27667
  if (kept.state === "kept") {
26827
- before();
27668
+ before2();
26828
27669
  end(
26829
27670
  `Kept running as ${label2}: it starts at login, and again whenever it stops. What it says goes to ${kept.logFile}. \`${stopLine}\` stops it.`,
26830
27671
  0
@@ -26844,9 +27685,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
26844
27685
  // src/server.ts
26845
27686
  import { spawn } from "node:child_process";
26846
27687
  import { randomBytes } from "node:crypto";
26847
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
27688
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
26848
27689
  import { createServer } from "node:net";
26849
- import { delimiter as delimiter2, join as join7 } from "node:path";
27690
+ import { delimiter as delimiter2, join as join8 } from "node:path";
26850
27691
  import { createInterface } from "node:readline";
26851
27692
  var ServerError = class extends Error {
26852
27693
  constructor(message, missing = false) {
@@ -26860,7 +27701,7 @@ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
26860
27701
  var onPath = (command, env) => command.includes("/") ? existsSync5(command) : (env["PATH"] ?? "").split(delimiter2).some((folder2) => {
26861
27702
  if (folder2 === "") return false;
26862
27703
  try {
26863
- accessSync2(join7(folder2, command), constants2.X_OK);
27704
+ accessSync2(join8(folder2, command), constants2.X_OK);
26864
27705
  return true;
26865
27706
  } catch {
26866
27707
  return false;
@@ -26896,7 +27737,7 @@ async function answers2(url2, password) {
26896
27737
  function keptIn(path) {
26897
27738
  if (!existsSync5(path)) return void 0;
26898
27739
  try {
26899
- const kept = JSON.parse(readFileSync5(path, "utf8"));
27740
+ const kept = JSON.parse(readFileSync6(path, "utf8"));
26900
27741
  return typeof kept.port === "number" && typeof kept.password === "string" ? kept : void 0;
26901
27742
  } catch {
26902
27743
  return void 0;
@@ -26925,15 +27766,15 @@ async function ownServer(options) {
26925
27766
  const env = options.env ?? process.env;
26926
27767
  const command = options.command ?? "opencode";
26927
27768
  const log2 = options.log ?? (() => void 0);
26928
- const path = join7(options.folder, "server.json");
27769
+ const path = join8(options.folder, "server.json");
26929
27770
  let kept = keptIn(path);
26930
- let before = false;
27771
+ let before2 = false;
26931
27772
  if (kept !== void 0) {
26932
27773
  const now = await answers2(`http://127.0.0.1:${kept.port}`, kept.password);
26933
- before = now === "ok";
27774
+ before2 = now === "ok";
26934
27775
  if (now === "refused") kept = { port: await freePort(), password: kept.password };
26935
27776
  }
26936
- if (!before && !onPath(command, env)) {
27777
+ if (!before2 && !onPath(command, env)) {
26937
27778
  throw new ServerError("OpenCode isn't installed on this machine: install it (opencode.ai), then run this again.", true);
26938
27779
  }
26939
27780
  kept ??= { port: await freePort(), password: randomBytes(24).toString("base64url") };
@@ -27016,7 +27857,7 @@ async function ownServer(options) {
27016
27857
  }
27017
27858
  await ended(info["pid"], stopMs);
27018
27859
  };
27019
- if (before) log2(`OpenCode's server from before is still answering at ${url2}`);
27860
+ if (before2) log2(`OpenCode's server from before is still answering at ${url2}`);
27020
27861
  else {
27021
27862
  await up(false);
27022
27863
  log2(`OpenCode's own server answering at ${url2}`);
@@ -27049,9 +27890,9 @@ async function ownServer(options) {
27049
27890
  }
27050
27891
 
27051
27892
  // src/setup.ts
27052
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
27053
- import { homedir as homedir5 } from "node:os";
27054
- import { join as join8, resolve as resolve3 } from "node:path";
27893
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
27894
+ import { homedir as homedir6 } from "node:os";
27895
+ import { join as join9, resolve as resolve3 } from "node:path";
27055
27896
  var SetupError = class extends Error {
27056
27897
  constructor(message, words4 = {}) {
27057
27898
  super(message);
@@ -27068,9 +27909,9 @@ var hostOf = (url2) => {
27068
27909
  }
27069
27910
  };
27070
27911
  var STILL_GOOD = "The link is still good: run the same command again once that's done.";
27071
- var home = (path) => path.startsWith("~/") ? join8(homedir5(), path.slice(2)) : path;
27912
+ var home = (path) => path.startsWith("~/") ? join9(homedir6(), path.slice(2)) : path;
27072
27913
  function readConfig(path, env = process.env) {
27073
- const raw = existsSync6(path) ? JSON.parse(readFileSync6(path, "utf8")) : {};
27914
+ const raw = existsSync6(path) ? JSON.parse(readFileSync7(path, "utf8")) : {};
27074
27915
  const passwordEnv = typeof raw["passwordEnv"] === "string" ? raw["passwordEnv"] : "OPENCODE_PASSWORD";
27075
27916
  const password = filledIn(env[passwordEnv]) ? env[passwordEnv] : void 0;
27076
27917
  const ownServer2 = password === void 0 && typeof raw["url"] !== "string";
@@ -27126,7 +27967,7 @@ async function checkOpenCode(config3, client) {
27126
27967
  }
27127
27968
 
27128
27969
  // src/main.ts
27129
- var home2 = (path) => path.startsWith("~/") ? join9(homedir6(), path.slice(2)) : path;
27970
+ var home2 = (path) => path.startsWith("~/") ? join10(homedir7(), path.slice(2)) : path;
27130
27971
  var link = process.env["CHIPS_JOIN_LINK"] || void 0;
27131
27972
  var LABEL = "chat.chips.opencode";
27132
27973
  stopIfAsked(LABEL);
@@ -27146,13 +27987,13 @@ try {
27146
27987
  } catch (error62) {
27147
27988
  config2 = stop(error62);
27148
27989
  }
27149
- var folder = dirname4(configPath);
27990
+ var folder = dirname5(configPath);
27150
27991
  mkdirSync5(folder, { recursive: true, mode: 448 });
27151
- var statePath = join9(folder, "sessions.json");
27992
+ var statePath = join10(folder, "sessions.json");
27152
27993
  var server;
27153
27994
  if (link !== void 0) reportStep("checking", { detail: "Looking for OpenCode" });
27154
27995
  if (config2.ownServer) {
27155
- const cwd = config2.directory ?? dirname4(config2.folders);
27996
+ const cwd = config2.directory ?? dirname5(config2.folders);
27156
27997
  mkdirSync5(cwd, { recursive: true });
27157
27998
  try {
27158
27999
  server = await ownServer({ folder, cwd, log });
@@ -27197,16 +28038,18 @@ var joined = await join3({
27197
28038
  onCode: (code) => log(`Allow this harness in Chips: ${code}`)
27198
28039
  }).catch(refused2);
27199
28040
  keepIfAsked(LABEL, joined.url, [config2.passwordEnv], log, () => server?.stop());
27200
- var state = existsSync7(statePath) ? JSON.parse(readFileSync7(statePath, "utf8")) : { sessions: {} };
28041
+ var state = existsSync7(statePath) ? JSON.parse(readFileSync8(statePath, "utf8")) : { sessions: {} };
27201
28042
  var bridge;
27202
28043
  var chips = await connect({
27203
28044
  url: joined.url,
27204
28045
  token: joined.token,
27205
28046
  client: { name: "chips-opencode", version: "1.0.0" },
27206
- store: fileStore(join9(folder, "chips-state.json")),
28047
+ store: fileStore(join10(folder, "chips-state.json")),
27207
28048
  // A person's Restart (Chips ADR 0094), heard only where the bridge runs OpenCode's server: one a
27208
28049
  // person runs themselves isn't the bridge's to restart, so this connection doesn't say it hears them.
27209
- ...server === void 0 ? {} : { onRestart: () => bridge?.restart() }
28050
+ ...server === void 0 ? {} : { onRestart: () => bridge?.restart() },
28051
+ // What a person changes of an agent from its page (Chips ADR 0241): its model and its files.
28052
+ settings: settingsFor(() => bridge)
27210
28053
  });
27211
28054
  var git = await checkGit();
27212
28055
  if (!git.ok) log(`rooms' projects are off: ${git.why}`);
@@ -27221,7 +28064,7 @@ bridge = new Bridge({
27221
28064
  `, { mode: 384 }),
27222
28065
  log,
27223
28066
  restartServer: server?.restart,
27224
- ...config2.directory === void 0 ? { folders: config2.folders } : {}
28067
+ ...config2.directory === void 0 ? { folders: config2.folders } : { directory: config2.directory }
27225
28068
  });
27226
28069
  await bridge.start();
27227
28070
  log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through OpenCode at ${address.url}`);