hive-am 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +19 -0
  3. package/package.json +1 -1
  4. package/server/dist/context.js +62 -0
  5. package/server/dist/db/migrations.js +7 -0
  6. package/server/dist/db.js +4 -4
  7. package/server/dist/history/claude.js +8 -2
  8. package/server/dist/http/routes/agents.js +1 -1
  9. package/server/dist/http/validate.js +2 -0
  10. package/server/dist/instructions.js +4 -0
  11. package/server/dist/models.js +2 -0
  12. package/server/dist/objects/index.js +4 -6
  13. package/server/dist/objects/runner.js +20 -0
  14. package/server/dist/objects/supervisor.js +2 -2
  15. package/server/dist/providers/claude.js +13 -1
  16. package/server/dist/providers/opencode-server.js +59 -0
  17. package/server/dist/providers/opencode.js +4 -1
  18. package/server/dist/stats.js +15 -3
  19. package/web/.next/BUILD_ID +1 -1
  20. package/web/.next/app-build-manifest.json +80 -71
  21. package/web/.next/app-path-routes-manifest.json +6 -5
  22. package/web/.next/build-manifest.json +2 -2
  23. package/web/.next/prerender-manifest.json +73 -49
  24. package/web/.next/routes-manifest.json +6 -0
  25. package/web/.next/server/app/_not-found/page.js +1 -1
  26. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  27. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  28. package/web/.next/server/app/_not-found.html +1 -1
  29. package/web/.next/server/app/_not-found.rsc +3 -3
  30. package/web/.next/server/app/agents/[id]/page.js +4 -4
  31. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  32. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  33. package/web/.next/server/app/agents/page.js +1 -1
  34. package/web/.next/server/app/agents/page.js.nft.json +1 -1
  35. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  36. package/web/.next/server/app/agents.html +1 -1
  37. package/web/.next/server/app/agents.rsc +4 -4
  38. package/web/.next/server/app/chat/page.js +2 -0
  39. package/web/.next/server/app/chat/page.js.nft.json +1 -0
  40. package/web/.next/server/app/chat/page_client-reference-manifest.js +1 -0
  41. package/web/.next/server/app/chat.html +1 -0
  42. package/web/.next/server/app/chat.meta +15 -0
  43. package/web/.next/server/app/chat.rsc +25 -0
  44. package/web/.next/server/app/connections/page.js +1 -1
  45. package/web/.next/server/app/connections/page.js.nft.json +1 -1
  46. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  47. package/web/.next/server/app/connections.html +1 -1
  48. package/web/.next/server/app/connections.rsc +4 -4
  49. package/web/.next/server/app/index.html +1 -1
  50. package/web/.next/server/app/index.rsc +4 -4
  51. package/web/.next/server/app/page.js +1 -1
  52. package/web/.next/server/app/page.js.nft.json +1 -1
  53. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/web/.next/server/app/relations/page.js +2 -2
  55. package/web/.next/server/app/relations/page.js.nft.json +1 -1
  56. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  57. package/web/.next/server/app/relations.html +1 -1
  58. package/web/.next/server/app/relations.rsc +4 -4
  59. package/web/.next/server/app/resources/[id]/page.js +2 -2
  60. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -1
  61. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
  62. package/web/.next/server/app/resources/page.js +2 -2
  63. package/web/.next/server/app/resources/page.js.nft.json +1 -1
  64. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
  65. package/web/.next/server/app/resources.html +1 -1
  66. package/web/.next/server/app/resources.rsc +4 -4
  67. package/web/.next/server/app/schedules/page.js +1 -1
  68. package/web/.next/server/app/schedules/page.js.nft.json +1 -1
  69. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
  70. package/web/.next/server/app/schedules.html +1 -1
  71. package/web/.next/server/app/schedules.rsc +4 -4
  72. package/web/.next/server/app/sessions/page.js +2 -2
  73. package/web/.next/server/app/sessions/page.js.nft.json +1 -1
  74. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  75. package/web/.next/server/app/sessions.html +1 -1
  76. package/web/.next/server/app/sessions.rsc +4 -4
  77. package/web/.next/server/app/skills/page.js +1 -1
  78. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  79. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  80. package/web/.next/server/app/skills.html +1 -1
  81. package/web/.next/server/app/skills.rsc +4 -4
  82. package/web/.next/server/app/types/page.js +1 -1
  83. package/web/.next/server/app/types/page.js.nft.json +1 -1
  84. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  85. package/web/.next/server/app/types.html +1 -1
  86. package/web/.next/server/app/types.rsc +4 -4
  87. package/web/.next/server/app-paths-manifest.json +6 -5
  88. package/web/.next/server/chunks/143.js +1 -1
  89. package/web/.next/server/chunks/148.js +1 -0
  90. package/web/.next/server/chunks/213.js +6 -0
  91. package/web/.next/server/chunks/274.js +1 -1
  92. package/web/.next/server/chunks/368.js +1 -0
  93. package/web/.next/server/chunks/403.js +1 -0
  94. package/web/.next/server/chunks/{790.js → 435.js} +2 -2
  95. package/web/.next/server/chunks/505.js +1 -6
  96. package/web/.next/server/chunks/612.js +1 -1
  97. package/web/.next/server/chunks/716.js +2 -2
  98. package/web/.next/server/pages/404.html +1 -1
  99. package/web/.next/server/pages/500.html +1 -1
  100. package/web/.next/server/server-reference-manifest.json +1 -1
  101. package/web/.next/static/YT6HM6qmegMJaBMX5QvuP/_buildManifest.js +1 -0
  102. package/web/.next/static/chunks/281-113274e705940a55.js +1 -0
  103. package/web/.next/static/chunks/350-0fe82e622e7a670f.js +1 -0
  104. package/web/.next/static/chunks/391-18768499e741112e.js +1 -0
  105. package/web/.next/static/chunks/517-373a80c8ba432bdc.js +1 -0
  106. package/web/.next/static/chunks/587-8e4d2be01ba7d48f.js +1 -0
  107. package/web/.next/static/chunks/606-c9a19918b02a98d0.js +1 -0
  108. package/web/.next/static/chunks/654-92a63b3c4b412666.js +1 -0
  109. package/web/.next/static/chunks/656-f537149fe28f79aa.js +1 -0
  110. package/web/.next/static/chunks/705-f81face97b1bb1af.js +1 -0
  111. package/web/.next/static/chunks/app/agents/[id]/page-6ed6edb3e74b0ebe.js +1 -0
  112. package/web/.next/static/chunks/app/agents/{page-f4c4c80f955f3967.js → page-9bb8cba69776c719.js} +1 -1
  113. package/web/.next/static/chunks/app/chat/page-5261b12b7d50f5d1.js +1 -0
  114. package/web/.next/static/chunks/app/connections/{page-0936beaf02d863c6.js → page-eb11cfb1ffeab3ca.js} +1 -1
  115. package/web/.next/static/chunks/app/layout-fad968a635f9a4b3.js +1 -0
  116. package/web/.next/static/chunks/app/page-99e0351e6ccaa384.js +1 -0
  117. package/web/.next/static/chunks/app/relations/{page-fba25393c8688714.js → page-a44fd515a3b0525e.js} +1 -1
  118. package/web/.next/static/chunks/app/resources/[id]/{page-d110d89bb8b5fca7.js → page-fc494e85b01e8cb7.js} +1 -1
  119. package/web/.next/static/chunks/app/resources/{page-bbd0b450d3f3131d.js → page-61cccd8fdcc2f6c8.js} +1 -1
  120. package/web/.next/static/chunks/app/schedules/{page-a1124397fc760413.js → page-96976fff4ee23538.js} +1 -1
  121. package/web/.next/static/chunks/app/sessions/{page-0e322811b0cde724.js → page-a7534ebd90e982d6.js} +1 -1
  122. package/web/.next/static/chunks/app/skills/{page-22ea0ef89d9f7a13.js → page-bc1acd0663f36129.js} +1 -1
  123. package/web/.next/static/chunks/app/types/{page-18c4e1db2724d41d.js → page-ebd37e220f77c1f3.js} +1 -1
  124. package/web/.next/static/css/582723fcea1f9887.css +1 -0
  125. package/web/.next/trace +30 -29
  126. package/web/.next/types/app/chat/page.ts +84 -0
  127. package/web/.next/types/routes.d.ts +2 -1
  128. package/web/.next/types/validator.ts +9 -0
  129. package/web/.next/server/chunks/306.js +0 -1
  130. package/web/.next/server/chunks/555.js +0 -1
  131. package/web/.next/server/chunks/638.js +0 -1
  132. package/web/.next/server/chunks/802.js +0 -1
  133. package/web/.next/static/7BI__-ayGDTlOrSboD_bT/_buildManifest.js +0 -1
  134. package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +0 -1
  135. package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +0 -1
  136. package/web/.next/static/chunks/350-56599dc3bf06066d.js +0 -1
  137. package/web/.next/static/chunks/486-13e08542f3a1cbac.js +0 -1
  138. package/web/.next/static/chunks/491-6b4e6201f5110dff.js +0 -1
  139. package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +0 -1
  140. package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +0 -1
  141. package/web/.next/static/chunks/705-c7e75806f77b880c.js +0 -1
  142. package/web/.next/static/chunks/839-4aefe6e71be6e1ef.js +0 -1
  143. package/web/.next/static/chunks/844-fe55292e0701f436.js +0 -1
  144. package/web/.next/static/chunks/app/agents/[id]/page-3faf40800981a736.js +0 -1
  145. package/web/.next/static/chunks/app/layout-99620746d028dda8.js +0 -1
  146. package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +0 -1
  147. package/web/.next/static/css/b7a03c62468f4263.css +0 -1
  148. /package/web/.next/static/{7BI__-ayGDTlOrSboD_bT → YT6HM6qmegMJaBMX5QvuP}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.0] - 2026-10-15
10
+
11
+ ### Added
12
+
13
+ - **Chat** item in the side rail, above Colony. It opens the last conversation you had open (or the first agent if that one no longer exists). With no agents yet, it shows an empty state with a button to create the first one.
14
+ - **Resources strip** above each chat, folded by default. Closed, it is one line that counts the resources running, stopped or failing (failures in red). Open, it shows a card per resource with start or stop, restart and a link to its own screen. It shows the agent's resources and those of its colony, with a switch to see all of them; the choice and whether it is open are remembered.
15
+ - The README now has screenshots of the colony, chat, resources, clusters, changes explorer and HTTP requests.
16
+
17
+ ### Changed
18
+
19
+ - Servers: **Keeping it healthy** is split into **If it goes down** (restart policy and maximum restarts) and **Health check** (kind, target and restart when unhealthy). The check timing (every, wait, failures in a row) and the stop and log settings are folded, and the defaults are visible.
20
+ - Servers: a restart waits for the port to be free before starting again, so a stop command that does not really stop the service is reported instead of leaving a failed start behind.
21
+ - Servers: the first health check comes one interval after a start instead of right away.
22
+
23
+ ### Fixed
24
+
25
+ - Entering a chat no longer flashes the default layout first: saved choices (the agent list collapsed, the resources strip open) apply before the first paint, the side panel only animates when you toggle it, and the conversation fades in when its history arrives.
26
+
27
+ ## [1.3.0] - 2026-10-14
28
+
29
+ ### Added
30
+
31
+ - Claude agents can drive your Chrome through the Claude in Chrome extension. Turn on **Browser (Chrome)** in the agent's settings (it is off by default and adds nothing to the prompt). The chat shows each browser action (open page, read, click, run script) with its own icon and label. Chrome and the extension must be open on the machine where hive-am runs.
32
+ - Kiro: agents get an instruction to fully detach long-running processes (`setsid nohup … > log 2>&1 < /dev/null &`) so the shell tool returns at once and the turn and chat keep going, instead of waiting for the command and everything it started to finish.
33
+
34
+ ### Fixed
35
+
36
+ - Claude: after **/compact** the chat no longer shows the CLI's summary as if you had written it; a "Conversation compacted" divider with the size before and after appears instead.
37
+ - Claude: the context percentage drops after a compact, using the size the CLI reports, instead of keeping the old value until the next reply.
38
+ - Context gauge: it uses each model's real window instead of a fixed 200k, so a 1M model no longer shows 100% at 200k tokens. The window is learned from the CLIs and kept in `context-windows.json`: Claude reports it in every turn, Kiro lists it with its models and OpenCode has it in its model catalog (read once an hour with a short-lived server when a model is unknown). When no window is known the chat shows tokens instead of a made-up percentage. The gauge also counts the last reply, ignores synthetic entries (errors) that used to reset it, and its tooltip shows tokens used and window.
39
+
9
40
  ## [1.2.2] - 2026-10-13
10
41
 
11
42
  ### Fixed
@@ -155,7 +186,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
155
186
  - Complete documentation in English and Spanish.
156
187
  - npm package with the `hive-am` command.
157
188
 
158
- [Unreleased]: https://github.com/hughcbscrt/hive-am/compare/v1.2.2...HEAD
189
+ [Unreleased]: https://github.com/hughcbscrt/hive-am/compare/v1.4.0...HEAD
190
+ [1.4.0]: https://github.com/hughcbscrt/hive-am/compare/v1.3.0...v1.4.0
191
+ [1.3.0]: https://github.com/hughcbscrt/hive-am/compare/v1.2.2...v1.3.0
159
192
  [1.2.2]: https://github.com/hughcbscrt/hive-am/compare/v1.2.1...v1.2.2
160
193
  [1.2.1]: https://github.com/hughcbscrt/hive-am/compare/v1.2.0...v1.2.1
161
194
  [1.2.0]: https://github.com/hughcbscrt/hive-am/compare/v1.1.0...v1.2.0
package/README.md CHANGED
@@ -44,6 +44,25 @@ have installed; it does not replace them and it never copies your conversations
44
44
  - **Terminals.** A bottom panel (Ctrl+`) with real shells and object logs, reachable only from this machine.
45
45
  - **Local and private.** The server binds to `127.0.0.1`. Data lives in `~/.hive-am/`.
46
46
 
47
+ ## Screenshots
48
+
49
+ <table>
50
+ <tr>
51
+ <td width="50%"><img src="docs/manual/en/img/colonia-mapa.jpg" alt="The colony map: colonies, agents and resources as hexagons" /><br /><sub><b>The colony.</b> Agents and resources as hexagons, grouped by colony.</sub></td>
52
+ <td width="50%"><img src="docs/manual/en/img/chat-conversacion.jpg" alt="A conversation with an orchestrator" /><br /><sub><b>Chat.</b> The real session of the CLI, with tokens, cost and tool calls.</sub></td>
53
+ </tr>
54
+ <tr>
55
+ <td width="50%"><img src="docs/manual/en/img/recurso-servidor-logs.jpg" alt="A server resource with its live logs" /><br /><sub><b>Resources.</b> Servers, containers and databases with live logs.</sub></td>
56
+ <td width="50%"><img src="docs/manual/en/img/recurso-cluster.jpg" alt="A cluster that starts several resources together" /><br /><sub><b>Clusters.</b> Start and stop a group of resources in order.</sub></td>
57
+ </tr>
58
+ <tr>
59
+ <td width="50%"><img src="docs/manual/en/img/cambios-diff.jpg" alt="The changes explorer showing a diff" /><br /><sub><b>Changes.</b> Git tree, diff, history and commit per agent.</sub></td>
60
+ <td width="50%"><img src="docs/manual/en/img/http-respuesta.jpg" alt="An HTTP request and its response" /><br /><sub><b>HTTP requests.</b> Run <code>.http</code> files and read the response.</sub></td>
61
+ </tr>
62
+ </table>
63
+
64
+ More in the [user manual](docs/manual/en/README.md).
65
+
47
66
  ## Providers
48
67
 
49
68
  | Provider | Run a turn | Transcript source |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hive-am",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
4
4
  "description": "A colony of coding agents: orchestrators and subagents over Claude Code, OpenCode and Kiro, with real session resume, a browser UI and Telegram.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -0,0 +1,62 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { DATA_DIR } from './db.js';
4
+ /**
5
+ * Size of each model's context window, learned from what the CLIs report: Claude says it in every result, Kiro lists it with its
6
+ * models and OpenCode's server has a catalog. A saved conversation does not keep it, so what was learned is stored on disk
7
+ * (`context-windows.json`) and used for any conversation of that model, also from before it was learned.
8
+ */
9
+ const FILE = join(DATA_DIR, 'context-windows.json');
10
+ let known = null;
11
+ const key = (provider, model) => `${provider}:${model}`;
12
+ function load() {
13
+ if (known)
14
+ return known;
15
+ try {
16
+ known = existsSync(FILE) ? JSON.parse(readFileSync(FILE, 'utf8')) : {};
17
+ }
18
+ catch {
19
+ known = {};
20
+ }
21
+ return known;
22
+ }
23
+ /** Remembers a model's window. Writes the file only when something changed. */
24
+ export function learnWindows(provider, windows) {
25
+ const all = load();
26
+ let changed = false;
27
+ for (const [model, tokens] of Object.entries(windows)) {
28
+ if (!model || !Number.isFinite(tokens) || tokens <= 0)
29
+ continue;
30
+ const k = key(provider, model);
31
+ if (all[k] !== tokens) {
32
+ all[k] = tokens;
33
+ changed = true;
34
+ }
35
+ }
36
+ if (changed) {
37
+ try {
38
+ writeFileSync(FILE, JSON.stringify(all, null, 1));
39
+ }
40
+ catch { /* best effort: it is learned again next time */ }
41
+ }
42
+ }
43
+ /**
44
+ * The window of a model in tokens, or null when it is not known (the chat then shows tokens instead of a made-up percentage).
45
+ * For a Claude model that was never seen on this machine it is derived from the id: Claude 5 and newer have 1M, older ones 200k,
46
+ * and a `[1m]` suffix is the 1M variant.
47
+ */
48
+ export function contextWindow(provider, model) {
49
+ if (!provider || !model || model.startsWith('<'))
50
+ return null;
51
+ const learned = load()[key(provider, model)];
52
+ if (learned)
53
+ return learned;
54
+ if (provider !== 'claude')
55
+ return null;
56
+ if (/\[1m\]/i.test(model))
57
+ return 1_000_000;
58
+ const major = /^claude-(?:opus|sonnet|haiku)-(\d+)/.exec(model)?.[1];
59
+ if (major)
60
+ return Number(major) >= 5 ? 1_000_000 : 200_000;
61
+ return model.startsWith('claude-') ? 200_000 : null;
62
+ }
@@ -182,6 +182,13 @@ CREATE TABLE IF NOT EXISTS object_agents (
182
182
  db.exec("UPDATE skills SET name='Colony resources' WHERE id='default-objects' AND name='Colony objects' AND NOT EXISTS (SELECT 1 FROM skills WHERE lower(name)='colony resources')");
183
183
  },
184
184
  },
185
+ {
186
+ version: 3, name: 'agent-browser',
187
+ up(db) {
188
+ // Whether a Claude agent may drive the person's Chrome (`--chrome`). Off for every agent that already exists.
189
+ addColumn(db, 'agents', 'browser INTEGER NOT NULL DEFAULT 0');
190
+ },
191
+ },
185
192
  ];
186
193
  export const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
187
194
  /** Brings a database up to the current schema. Returns the versions it applied (empty when it was already current). */
package/server/dist/db.js CHANGED
@@ -184,7 +184,7 @@ function agentOf(r) {
184
184
  skill_loads: { ...(follows('skills') ? col.skill_loads : {}), ...loadsOf('agent', r.id) },
185
185
  inherited,
186
186
  };
187
- return { ...r, skill_ids, skill_loads: loadsOf('agent', r.id), worker_ids, overrides, effective };
187
+ return { ...r, browser: !!r.browser, skill_ids, skill_loads: loadsOf('agent', r.id), worker_ids, overrides, effective };
188
188
  }
189
189
  /** The agent as it actually runs: colony-inherited values already folded into cwd/permission/prompt/skills. */
190
190
  export function resolved(a) {
@@ -197,7 +197,7 @@ export const agents = {
197
197
  /** The agent, its skills and its workers are written together: either all of it exists afterwards or none. */
198
198
  create: db.transaction((p) => {
199
199
  const id = uid(), t = now();
200
- q('INSERT INTO agents (id,name,description,role,type_id,provider,model,system_prompt,permission,cwd,session_id,status,created_at,updated_at,colony_id,overrides) VALUES (?,?,?,?,?,?,?,?,?,?,NULL,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.role, p.type_id ?? null, p.provider, p.model ?? '', p.system_prompt ?? '', (p.permission ?? 'acceptEdits'), p.cwd ?? '', 'idle', t, t, p.colony_id ?? null, JSON.stringify(p.overrides ?? []));
200
+ q('INSERT INTO agents (id,name,description,role,type_id,provider,model,system_prompt,permission,cwd,session_id,status,created_at,updated_at,colony_id,overrides,browser) VALUES (?,?,?,?,?,?,?,?,?,?,NULL,?,?,?,?,?,?)').run(id, p.name, p.description ?? '', p.role, p.type_id ?? null, p.provider, p.model ?? '', p.system_prompt ?? '', (p.permission ?? 'acceptEdits'), p.cwd ?? '', 'idle', t, t, p.colony_id ?? null, JSON.stringify(p.overrides ?? []), p.browser && p.provider === 'claude' ? 1 : 0);
201
201
  agents.setSkills(id, p.skill_ids ?? [], p.skill_loads);
202
202
  agents.setWorkers(id, p.worker_ids ?? []);
203
203
  return agents.get(id);
@@ -207,8 +207,8 @@ export const agents = {
207
207
  if (!cur)
208
208
  return;
209
209
  const n = { ...cur, ...p };
210
- q('UPDATE agents SET name=?, description=?, role=?, type_id=?, provider=?, model=?, system_prompt=?, permission=?, cwd=?, colony_id=?, overrides=?, updated_at=? WHERE id=?')
211
- .run(n.name, n.description, n.role, n.type_id, n.provider, n.model, n.system_prompt, n.permission, n.cwd, n.colony_id ?? null, JSON.stringify(n.overrides ?? []), now(), id);
210
+ q('UPDATE agents SET name=?, description=?, role=?, type_id=?, provider=?, model=?, system_prompt=?, permission=?, cwd=?, colony_id=?, overrides=?, browser=?, updated_at=? WHERE id=?')
211
+ .run(n.name, n.description, n.role, n.type_id, n.provider, n.model, n.system_prompt, n.permission, n.cwd, n.colony_id ?? null, JSON.stringify(n.overrides ?? []), n.browser && n.provider === 'claude' ? 1 : 0, now(), id);
212
212
  if (p.skill_ids || p.skill_loads)
213
213
  agents.setSkills(id, p.skill_ids ?? cur.skill_ids, p.skill_loads);
214
214
  if (p.worker_ids)
@@ -61,7 +61,13 @@ function parseClaude(file) {
61
61
  const toolBlocks = new Map();
62
62
  const toolStart = new Map();
63
63
  for (const e of jsonLines(file)) {
64
- if (e.isSidechain || e.isMeta || (e.type !== 'user' && e.type !== 'assistant'))
64
+ // `/compact` leaves a boundary (with the size before and after) and the summary as a user entry: the summary is the CLI's, not the person's.
65
+ if (e.type === 'system' && e.subtype === 'compact_boundary') {
66
+ const c = e.compactMetadata ?? {};
67
+ out.push({ id: e.uuid, role: 'assistant', ts: e.timestamp ? Date.parse(e.timestamp) : null, blocks: [], compact: { pre: c.preTokens ?? 0, post: c.postTokens ?? 0 } });
68
+ continue;
69
+ }
70
+ if (e.isSidechain || e.isMeta || e.isCompactSummary || (e.type !== 'user' && e.type !== 'assistant'))
65
71
  continue;
66
72
  const m = e.message;
67
73
  if (!m)
@@ -133,7 +139,7 @@ function parseClaude(file) {
133
139
  }
134
140
  }
135
141
  }
136
- return out.filter((m) => m.blocks.length);
142
+ return out.filter((m) => m.blocks.length || m.compact);
137
143
  }
138
144
  export function claudeMeta(file) {
139
145
  const st = statSync(file);
@@ -104,7 +104,7 @@ route('GET', '/api/agents/:id/history', ({ params, url }) => {
104
104
  });
105
105
  route('GET', '/api/agents/:id/stats', ({ params, url }) => {
106
106
  const a = agentOrFail(params[0]);
107
- return sessionStats(readHistory(resolved(a), sessionToRead(a, url.searchParams.get('session'))));
107
+ return sessionStats(readHistory(resolved(a), sessionToRead(a, url.searchParams.get('session'))), a.provider);
108
108
  });
109
109
  route('GET', '/api/agents/:id/live', ({ params }) => liveTurn(params[0]));
110
110
  route('GET', '/api/agents/:id/sessions', ({ params }) => agents.sessions(params[0]));
@@ -27,6 +27,8 @@ export function validateAgentInput(p, partial = false) {
27
27
  if (p.role !== undefined && !['orchestrator', 'worker'].includes(p.role))
28
28
  throw bad('Role must be orchestrator or worker');
29
29
  checkFolder(p.cwd);
30
+ if (p.browser !== undefined && typeof p.browser !== 'boolean')
31
+ throw bad('browser must be true or false');
30
32
  if (p.colony_id && (typeof p.colony_id !== 'string' || !colonies.get(p.colony_id)))
31
33
  throw bad('That colony no longer exists');
32
34
  return p;
@@ -54,6 +54,10 @@ export function composeInstructions(a, delegated = false) {
54
54
  if (a.provider === 'opencode' && mcpCaps(a).length > 0)
55
55
  parts.push(`## Calling the hive tools
56
56
  The hive tools (\`tools.hive.*\`) are called from the \`execute\` tool, with \`await\`, using exactly the names given in these instructions. Do not \`search\` for them and do not probe with \`shell\` first: call them directly, e.g. \`execute\` with \`return await tools.hive.channel_reply({ text: "..." });\`. At the start of a turn the hive tools are still connecting, so your very first call can fail with \`Unknown tool 'hive.…'. Use search to find available tools.\` That is expected: ignore the hint, do not \`search\` and do not change tools; make the same call again right away with the same arguments and it will work. Use \`shell\` only for real shell work (commands, files), never to run JavaScript or as a placeholder. For a simple question, answer it in a single call.`);
57
+ // Kiro's shell tool returns only when the command's output pipes close, so a plain `cmd &` keeps the whole turn (and the chat) waiting until it ends.
58
+ if (a.provider === 'kiro')
59
+ parts.push(`## Background commands
60
+ Your shell tool waits until the command and everything it started has closed its output. To leave something running (a dev server, a watcher, a long build, a tunnel), detach it completely so the tool returns at once and the conversation can go on: \`setsid nohup <command> > <log file> 2>&1 < /dev/null & echo $!\`. All three redirections are required; a bare \`<command> &\` still blocks. Then read the log file when you need its output, and never wait for the process to end.`);
57
61
  if (a.system_prompt.trim())
58
62
  parts.push(a.system_prompt.trim());
59
63
  const lazy = lazySkills(a);
@@ -1,5 +1,6 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { learnWindows } from './context.js';
3
4
  const run = promisify(execFile);
4
5
  const cache = new Map();
5
6
  const CLAUDE = [
@@ -21,6 +22,7 @@ async function discover(p) {
21
22
  const { stdout } = await run('kiro-cli', ['chat', '--list-models', '--format', 'json'], { timeout: 20000, maxBuffer: 4e6 });
22
23
  const j = JSON.parse(stdout);
23
24
  const list = Array.isArray(j) ? j : j.models ?? [];
25
+ learnWindows('kiro', Object.fromEntries(list.map((m) => [m.model_id ?? m.id ?? m.name, Number(m.context_window_tokens)])));
24
26
  return list.map((m) => ({ id: m.model_id ?? m.id ?? m.name, label: m.model_name ?? m.name ?? m.model_id ?? m.id })).filter((m) => m.id);
25
27
  }
26
28
  catch {
@@ -10,7 +10,7 @@ import { dbColumns, dbQuery, dbSchema, dbState, dbTest, forgetDatabase } from '.
10
10
  import { addSaved, clearQueries, forgetQueries, listQueries, listSaved, recordQuery, removeSaved, updateSaved } from './db-history.js';
11
11
  import { exportCollection, parseHttpFile, createHttpFile, exportHttp, describeHttp, dropSession, inspectHttp, readHttpFile, runHttp, scanHttp, writeHttpFile, sessionClear, sessionOf, sessionSet, sessionView } from './http.js';
12
12
  import { ObjectError, parseDatabase } from './model.js';
13
- import { forgetServer, serverLogs, serverState, startServer, stopServer } from './runner.js';
13
+ import { forgetServer, serverLogs, restartServer, serverState, startServer, stopServer } from './runner.js';
14
14
  import { forgetSupervision, resetSupervision, supervise } from './supervisor.js';
15
15
  import { maskConfig, objectsStore } from './store.js';
16
16
  export { ObjectError } from './model.js';
@@ -59,7 +59,7 @@ function clusterState(o) {
59
59
  async function serverStateSupervised(o) {
60
60
  const cfg = o.config;
61
61
  const base = await serverState(o.id, cfg);
62
- return supervise(o, cfg, base, { start: () => startServer(o.id, cfg), restart: async () => { await stopServer(o.id, cfg); await startServer(o.id, cfg); } });
62
+ return supervise(o, cfg, base, { start: () => startServer(o.id, cfg), restart: () => restartServer(o.id, cfg) });
63
63
  }
64
64
  export async function stateOf(o) {
65
65
  const s = o.kind === 'server' ? await serverStateSupervised(o)
@@ -113,10 +113,8 @@ async function act(o, action) {
113
113
  await startServer(o.id, c);
114
114
  else if (action === 'stop')
115
115
  await stopServer(o.id, c);
116
- else {
117
- await stopServer(o.id, c);
118
- await startServer(o.id, c);
119
- }
116
+ else
117
+ await restartServer(o.id, c);
120
118
  }
121
119
  else {
122
120
  const c = o.config;
@@ -139,6 +139,26 @@ export async function stopServer(id, cfg) {
139
139
  await sleep(100);
140
140
  }
141
141
  }
142
+ /**
143
+ * Stop, then start again. With a port, the old listener must be gone before the new one starts: otherwise the check on the port would
144
+ * report "running" for the old process (a stop command that does not really stop the service) while the new one failed to bind.
145
+ */
146
+ export async function restartServer(id, cfg) {
147
+ await stopServer(id, cfg);
148
+ if (cfg.port) {
149
+ const grace = cfg.stopTimeoutSec ? cfg.stopTimeoutSec * 1000 : STOP_GRACE_MS;
150
+ let waited = 0;
151
+ while (await listening(cfg.port) && waited < grace) {
152
+ await sleep(300);
153
+ waited += 300;
154
+ }
155
+ if (await listening(cfg.port)) {
156
+ note(id, `port ${cfg.port} is still in use after stopping: not starting it again. Check that the stop command really stops the service`);
157
+ throw new ObjectError(`Port ${cfg.port} is still in use after stopping, so it was not started again. Check that the stop command really stops the service.`);
158
+ }
159
+ }
160
+ await startServer(id, cfg);
161
+ }
142
162
  /** Lines the object printed. `after` is a byte offset from an earlier call; without it the last `tail` lines come back. */
143
163
  export function serverLogs(id, o) {
144
164
  const { log } = files(id);
@@ -54,8 +54,8 @@ export async function supervise(o, cfg, state, act) {
54
54
  if (S.startedAt !== saved?.startedAt) {
55
55
  S.startedAt = saved?.startedAt ?? 0;
56
56
  S.fails = 0;
57
- S.lastCheck = 0;
58
- }
57
+ S.lastCheck = now;
58
+ } // a new start: the first check comes one interval later, not right away
59
59
  if (now - S.lastCheck >= (cfg.health.intervalSec ?? 15) * 1000) {
60
60
  S.lastCheck = now;
61
61
  const r = await check(cfg, cfg.health);
@@ -6,6 +6,7 @@ import { dispatchMcpConfig } from '../mcp-config.js';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { inboxDir } from '../connections/files.js';
8
8
  import { withInstructions } from './preamble.js';
9
+ import { learnWindows } from '../context.js';
9
10
  /** What editing a project must not touch: the repository's own data and private keys (.env stays editable, like in OpenCode). */
10
11
  export const CLAUDE_EDIT_DENY = ['Edit(**/.git/**)', 'Edit(**/*.pem)', 'Edit(**/*.key)'];
11
12
  /** Claude Code: `claude -p` with stream-json; the prompt goes through stdin, the session is resumed by id. */
@@ -28,13 +29,22 @@ export async function* runClaude(o) {
28
29
  if (o.instructions)
29
30
  args.push('--append-system-prompt', o.instructions);
30
31
  const prompt = withInstructions(o.prompt, o.instructions, !!agent.session_id, o.refreshInstructions);
32
+ // Agents with the Browser switch on drive the person's Chrome through its extension; `-p` has nobody to approve each call, so the tools are allowed up front.
33
+ const allowed = [];
34
+ if (agent.browser && agent.permission !== 'plan') {
35
+ args.push('--chrome');
36
+ allowed.push('mcp__claude-in-chrome');
37
+ }
31
38
  if (o.mcpCaps.length) {
32
39
  const dir = join(DATA_DIR, 'mcp');
33
40
  mkdirSync(dir, { recursive: true });
34
41
  const file = join(dir, `${agent.id}.json`);
35
42
  writeFileSync(file, JSON.stringify(dispatchMcpConfig(agent.id, o.mcpCaps)));
36
- args.push('--mcp-config', file, '--allowedTools', 'mcp__hive');
43
+ args.push('--mcp-config', file);
44
+ allowed.push('mcp__hive');
37
45
  }
46
+ if (allowed.length)
47
+ args.push('--allowedTools', ...allowed);
38
48
  const toolNames = new Map();
39
49
  let streamedText = false;
40
50
  let sawSession = false;
@@ -74,6 +84,8 @@ export async function* runClaude(o) {
74
84
  }
75
85
  }
76
86
  else if (ev.type === 'result') {
87
+ // The CLI says how big each model's window is: keep it, a saved conversation does not.
88
+ learnWindows('claude', Object.fromEntries(Object.entries(ev.modelUsage ?? {}).map(([m, x]) => [m, Number(x?.contextWindow)])));
77
89
  const u = ev.usage;
78
90
  if (u)
79
91
  yield {
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { DATA_DIR } from '../db.js';
5
5
  import { createHash, randomBytes } from 'node:crypto';
6
+ import { learnWindows } from '../context.js';
6
7
  /**
7
8
  * One long-lived `opencode serve` per agent. A private server per turn (`opencode run --standalone`) boots from zero every time and its
8
9
  * hive MCP server connects only after the model has already taken its first step, so the first `tools.hive.*` call always fails
@@ -57,6 +58,64 @@ function reapOrphans() {
57
58
  }
58
59
  reapOrphans();
59
60
  const auth = (password) => ({ authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` });
61
+ let windowsAt = 0;
62
+ /**
63
+ * Reads the context window of every model from OpenCode's catalog, using this turn's server, any running one or a short-lived one
64
+ * (about 4 s and 150 MB, once). At most once an hour, and a failure waits ten minutes: OpenCode 1.x has no catalog endpoint at all.
65
+ */
66
+ export async function learnOpencodeWindows(cwd, access) {
67
+ if (Date.now() - windowsAt < 60 * 60_000)
68
+ return;
69
+ windowsAt = Date.now();
70
+ let proc;
71
+ try {
72
+ let url = access?.url, password = access?.password;
73
+ if (!url) {
74
+ const live = [...servers.values()].find((x) => x.url && x.proc.exitCode === null);
75
+ if (live) {
76
+ url = live.url;
77
+ password = live.password;
78
+ }
79
+ }
80
+ if (!url) {
81
+ password = randomBytes(18).toString('base64url');
82
+ const child = proc = spawn('opencode', ['serve', '--hostname', '127.0.0.1', '--port', '0'], { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PWD: cwd, OPENCODE_SERVER_PASSWORD: password } });
83
+ url = await new Promise((resolve, reject) => {
84
+ let out = '';
85
+ const timer = setTimeout(() => reject(new Error('opencode serve did not start in time')), START_TIMEOUT_MS);
86
+ const onData = (d) => { out = (out + d).slice(-2000); const m = /listening on (http:\/\/\S+)/i.exec(out); if (m) {
87
+ clearTimeout(timer);
88
+ resolve(m[1].replace(/\/$/, ''));
89
+ } };
90
+ child.stdout.on('data', onData);
91
+ child.stderr.on('data', onData);
92
+ child.on('error', (e) => { clearTimeout(timer); reject(e); });
93
+ child.on('exit', (c) => { clearTimeout(timer); reject(new Error(`opencode serve exited with code ${c}`)); });
94
+ });
95
+ }
96
+ // Right after it starts the catalog is empty for a moment: ask again until it has models.
97
+ const until = Date.now() + 10_000;
98
+ let list = [];
99
+ while (!list.length && Date.now() < until) {
100
+ const r = await fetch(`${url}/api/model`, { headers: auth(password), signal: AbortSignal.timeout(5000) });
101
+ if (!r.ok)
102
+ return; // no catalog in this OpenCode: leave it for an hour
103
+ list = (await r.json())?.data ?? [];
104
+ if (!list.length)
105
+ await new Promise((res) => setTimeout(res, 400));
106
+ }
107
+ if (!list.length)
108
+ throw new Error('the model catalog stayed empty');
109
+ learnWindows('opencode', Object.fromEntries(list.map((m) => [`${m.providerID}/${m.id}`, Number(m.limit?.context)])));
110
+ }
111
+ catch {
112
+ windowsAt = Date.now() - 50 * 60_000; /* try again in ten minutes */
113
+ }
114
+ finally {
115
+ if (proc && !proc.killed)
116
+ proc.kill('SIGTERM');
117
+ }
118
+ }
60
119
  /** Waits until every MCP server the config asks for has finished connecting (connected or failed): no more "Unknown tool" at the first step. */
61
120
  async function mcpSettled(s, signal) {
62
121
  const until = Date.now() + MCP_WAIT_MS;
@@ -6,7 +6,8 @@ import { spawnJson } from './spawn.js';
6
6
  import { dispatchMcpConfigOpencode } from '../mcp-config.js';
7
7
  import { opencodeSessionDir } from '../history/opencode.js';
8
8
  import { withInstructions } from './preamble.js';
9
- import { opencodeServer, stopOpencodeServer } from './opencode-server.js';
9
+ import { contextWindow } from '../context.js';
10
+ import { learnOpencodeWindows, opencodeServer, stopOpencodeServer } from './opencode-server.js';
10
11
  /**
11
12
  * What OpenCode may do for each permission level. It has no such modes, so they are written as its own rules (a `deny` holds even with `--auto`):
12
13
  * read-only changes nothing; editing changes files of the project (including .env, never .git or keys), runs no commands and stays in the agent's folder
@@ -84,6 +85,8 @@ export async function* runOpencode(o) {
84
85
  console.warn(`[opencode] no persistent server for ${agent.name}, using a private one: ${e instanceof Error ? e.message : e}`);
85
86
  }
86
87
  }
88
+ if (!contextWindow('opencode', agent.model))
89
+ void learnOpencodeWindows(agent.cwd, access);
87
90
  if (access) {
88
91
  args.splice(1, 0, '--server', access.url);
89
92
  env.OPENCODE_SERVER_PASSWORD = access.password;
@@ -1,5 +1,6 @@
1
1
  import { addUsage, emptyUsage } from './pricing.js';
2
- export function sessionStats(msgs) {
2
+ import { contextWindow } from './context.js';
3
+ export function sessionStats(msgs, provider) {
3
4
  let usage = emptyUsage();
4
5
  let cost = 0;
5
6
  let any = false;
@@ -9,8 +10,13 @@ export function sessionStats(msgs) {
9
10
  const timeline = [];
10
11
  let cur = null;
11
12
  let lastCtx = 0;
13
+ let ctxModel;
12
14
  let toolCalls = 0, toolErrors = 0, toolTime = 0;
13
15
  for (const m of msgs) {
16
+ if (m.compact) {
17
+ lastCtx = m.compact.post;
18
+ continue;
19
+ } // nothing is sent after a compact, so the context is what it left
14
20
  if (m.role === 'user') {
15
21
  const prompt = m.blocks.map((b) => (b.type === 'text' ? b.text : '')).join('').slice(0, 120);
16
22
  cur = { ts: m.ts, prompt, tokens: 0, cost: null, durationMs: 0, tools: 0 };
@@ -20,7 +26,11 @@ export function sessionStats(msgs) {
20
26
  const u = m.meta?.usage;
21
27
  if (u) {
22
28
  usage = addUsage(usage, u);
23
- lastCtx = u.input + u.cacheRead + u.cacheWrite;
29
+ // A synthetic entry (an error, "no response requested") has no real request behind it: it must not reset the gauge to 0.
30
+ if (u.input + u.cacheRead + u.cacheWrite > 0 && !m.meta?.model?.startsWith('<')) {
31
+ lastCtx = u.input + u.cacheRead + u.cacheWrite + u.output;
32
+ ctxModel = m.meta?.model ?? ctxModel;
33
+ }
24
34
  if (cur)
25
35
  cur.tokens += u.input + u.output + u.cacheRead + u.cacheWrite;
26
36
  }
@@ -56,11 +66,13 @@ export function sessionStats(msgs) {
56
66
  tools.set(b.name, t);
57
67
  }
58
68
  }
69
+ const window = contextWindow(provider, ctxModel);
59
70
  return {
60
71
  usage, cost: any ? cost : null, costEstimated: est, turns: timeline.length, toolCalls, toolErrors, toolTimeMs: toolTime,
61
72
  durationMs: timeline.reduce((n, t) => n + t.durationMs, 0),
62
73
  tools: [...tools.values()].sort((a, b) => b.count - a.count),
63
74
  models: [...models.values()].sort((a, b) => b.messages - a.messages),
64
- lastContext: lastCtx, contextPct: usage.contextPct ?? null, timeline,
75
+ lastContext: lastCtx, contextWindow: window,
76
+ contextPct: usage.contextPct ?? (window && lastCtx ? Math.min(100, (lastCtx / window) * 100) : null), timeline,
65
77
  };
66
78
  }
@@ -1 +1 @@
1
- 7BI__-ayGDTlOrSboD_bT
1
+ YT6HM6qmegMJaBMX5QvuP