hive-am 1.2.2 → 1.3.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 (110) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/package.json +1 -1
  3. package/server/dist/context.js +62 -0
  4. package/server/dist/db/migrations.js +7 -0
  5. package/server/dist/db.js +4 -4
  6. package/server/dist/history/claude.js +8 -2
  7. package/server/dist/http/routes/agents.js +1 -1
  8. package/server/dist/http/validate.js +2 -0
  9. package/server/dist/instructions.js +4 -0
  10. package/server/dist/models.js +2 -0
  11. package/server/dist/providers/claude.js +13 -1
  12. package/server/dist/providers/opencode-server.js +59 -0
  13. package/server/dist/providers/opencode.js +4 -1
  14. package/server/dist/stats.js +15 -3
  15. package/web/.next/BUILD_ID +1 -1
  16. package/web/.next/app-build-manifest.json +70 -70
  17. package/web/.next/app-path-routes-manifest.json +7 -7
  18. package/web/.next/build-manifest.json +2 -2
  19. package/web/.next/prerender-manifest.json +49 -49
  20. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/web/.next/server/app/_not-found.html +1 -1
  22. package/web/.next/server/app/_not-found.rsc +3 -3
  23. package/web/.next/server/app/agents/[id]/page.js +3 -3
  24. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  25. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  26. package/web/.next/server/app/agents/page.js +1 -1
  27. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  28. package/web/.next/server/app/agents.html +1 -1
  29. package/web/.next/server/app/agents.rsc +4 -4
  30. package/web/.next/server/app/connections/page.js +1 -1
  31. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  32. package/web/.next/server/app/connections.html +1 -1
  33. package/web/.next/server/app/connections.rsc +4 -4
  34. package/web/.next/server/app/index.html +1 -1
  35. package/web/.next/server/app/index.rsc +4 -4
  36. package/web/.next/server/app/page.js +1 -1
  37. package/web/.next/server/app/page.js.nft.json +1 -1
  38. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  39. package/web/.next/server/app/relations/page.js +1 -1
  40. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  41. package/web/.next/server/app/relations.html +1 -1
  42. package/web/.next/server/app/relations.rsc +4 -4
  43. package/web/.next/server/app/resources/[id]/page.js +1 -1
  44. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -1
  45. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
  46. package/web/.next/server/app/resources/page.js +1 -1
  47. package/web/.next/server/app/resources/page.js.nft.json +1 -1
  48. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
  49. package/web/.next/server/app/resources.html +1 -1
  50. package/web/.next/server/app/resources.rsc +4 -4
  51. package/web/.next/server/app/schedules/page.js +1 -1
  52. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
  53. package/web/.next/server/app/schedules.html +1 -1
  54. package/web/.next/server/app/schedules.rsc +4 -4
  55. package/web/.next/server/app/sessions/page.js +1 -1
  56. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  57. package/web/.next/server/app/sessions.html +1 -1
  58. package/web/.next/server/app/sessions.rsc +4 -4
  59. package/web/.next/server/app/skills/page.js +1 -1
  60. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  61. package/web/.next/server/app/skills.html +1 -1
  62. package/web/.next/server/app/skills.rsc +4 -4
  63. package/web/.next/server/app/types/page.js +1 -1
  64. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  65. package/web/.next/server/app/types.html +1 -1
  66. package/web/.next/server/app/types.rsc +4 -4
  67. package/web/.next/server/app-paths-manifest.json +7 -7
  68. package/web/.next/server/chunks/143.js +1 -1
  69. package/web/.next/server/chunks/274.js +1 -1
  70. package/web/.next/server/chunks/306.js +1 -1
  71. package/web/.next/server/chunks/451.js +1 -0
  72. package/web/.next/server/chunks/505.js +1 -1
  73. package/web/.next/server/chunks/612.js +1 -1
  74. package/web/.next/server/chunks/716.js +2 -2
  75. package/web/.next/server/pages/404.html +1 -1
  76. package/web/.next/server/pages/500.html +1 -1
  77. package/web/.next/server/server-reference-manifest.json +1 -1
  78. package/web/.next/static/chunks/281-d8a0dcfaa6d064e4.js +1 -0
  79. package/web/.next/static/chunks/{291-efae53ea5f7ee92d.js → 291-23f314877fa39054.js} +1 -1
  80. package/web/.next/static/chunks/304-681348099f874a30.js +1 -0
  81. package/web/.next/static/chunks/350-0fe82e622e7a670f.js +1 -0
  82. package/web/.next/static/chunks/{491-6b4e6201f5110dff.js → 491-28f093772bd460ac.js} +1 -1
  83. package/web/.next/static/chunks/517-373a80c8ba432bdc.js +1 -0
  84. package/web/.next/static/chunks/654-92a63b3c4b412666.js +1 -0
  85. package/web/.next/static/chunks/705-780e48e55b0346bd.js +1 -0
  86. package/web/.next/static/chunks/{844-fe55292e0701f436.js → 844-519177e6198ca84f.js} +1 -1
  87. package/web/.next/static/chunks/app/agents/[id]/page-c005161cec5527d8.js +1 -0
  88. package/web/.next/static/chunks/app/agents/{page-f4c4c80f955f3967.js → page-9bb8cba69776c719.js} +1 -1
  89. package/web/.next/static/chunks/app/connections/{page-0936beaf02d863c6.js → page-eb11cfb1ffeab3ca.js} +1 -1
  90. package/web/.next/static/chunks/app/{layout-99620746d028dda8.js → layout-49b46c9b5aa512a8.js} +1 -1
  91. package/web/.next/static/chunks/app/{page-a7f292117b7d76bf.js → page-da042d146caeeab6.js} +1 -1
  92. package/web/.next/static/chunks/app/relations/{page-fba25393c8688714.js → page-a44fd515a3b0525e.js} +1 -1
  93. package/web/.next/static/chunks/app/resources/[id]/{page-d110d89bb8b5fca7.js → page-bf05836f97248ca7.js} +1 -1
  94. package/web/.next/static/chunks/app/resources/{page-bbd0b450d3f3131d.js → page-800f774e5cacf4c9.js} +1 -1
  95. package/web/.next/static/chunks/app/schedules/{page-a1124397fc760413.js → page-96976fff4ee23538.js} +1 -1
  96. package/web/.next/static/chunks/app/sessions/{page-0e322811b0cde724.js → page-a7534ebd90e982d6.js} +1 -1
  97. package/web/.next/static/chunks/app/skills/{page-22ea0ef89d9f7a13.js → page-bc1acd0663f36129.js} +1 -1
  98. package/web/.next/static/chunks/app/types/{page-18c4e1db2724d41d.js → page-ebd37e220f77c1f3.js} +1 -1
  99. package/web/.next/static/css/{b7a03c62468f4263.css → d42799b7e6a0e8a2.css} +1 -1
  100. package/web/.next/trace +29 -29
  101. package/web/.next/server/chunks/555.js +0 -1
  102. package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +0 -1
  103. package/web/.next/static/chunks/350-56599dc3bf06066d.js +0 -1
  104. package/web/.next/static/chunks/486-13e08542f3a1cbac.js +0 -1
  105. package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +0 -1
  106. package/web/.next/static/chunks/705-c7e75806f77b880c.js +0 -1
  107. package/web/.next/static/chunks/839-4aefe6e71be6e1ef.js +0 -1
  108. package/web/.next/static/chunks/app/agents/[id]/page-3faf40800981a736.js +0 -1
  109. /package/web/.next/static/{7BI__-ayGDTlOrSboD_bT → aSYzbHk1M5YeS_d3SQKTZ}/_buildManifest.js +0 -0
  110. /package/web/.next/static/{7BI__-ayGDTlOrSboD_bT → aSYzbHk1M5YeS_d3SQKTZ}/_ssgManifest.js +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] - 2026-10-14
10
+
11
+ ### Added
12
+
13
+ - 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.
14
+ - 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.
15
+
16
+ ### Fixed
17
+
18
+ - 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.
19
+ - Claude: the context percentage drops after a compact, using the size the CLI reports, instead of keeping the old value until the next reply.
20
+ - 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.
21
+
9
22
  ## [1.2.2] - 2026-10-13
10
23
 
11
24
  ### Fixed
@@ -155,7 +168,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
155
168
  - Complete documentation in English and Spanish.
156
169
  - npm package with the `hive-am` command.
157
170
 
158
- [Unreleased]: https://github.com/hughcbscrt/hive-am/compare/v1.2.2...HEAD
171
+ [Unreleased]: https://github.com/hughcbscrt/hive-am/compare/v1.3.0...HEAD
172
+ [1.3.0]: https://github.com/hughcbscrt/hive-am/compare/v1.2.2...v1.3.0
159
173
  [1.2.2]: https://github.com/hughcbscrt/hive-am/compare/v1.2.1...v1.2.2
160
174
  [1.2.1]: https://github.com/hughcbscrt/hive-am/compare/v1.2.0...v1.2.1
161
175
  [1.2.0]: https://github.com/hughcbscrt/hive-am/compare/v1.1.0...v1.2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hive-am",
3
- "version": "1.2.2",
3
+ "version": "1.3.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 {
@@ -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
+ aSYzbHk1M5YeS_d3SQKTZ
@@ -13,10 +13,10 @@
13
13
  "static/chunks/89-b194fcffe5120c44.js",
14
14
  "static/chunks/main-app-6086068e5fb5eacd.js",
15
15
  "static/css/945795da867f5250.css",
16
- "static/css/b7a03c62468f4263.css",
16
+ "static/css/d42799b7e6a0e8a2.css",
17
17
  "static/chunks/586-f7017d585a84738d.js",
18
- "static/chunks/839-4aefe6e71be6e1ef.js",
19
- "static/chunks/app/layout-99620746d028dda8.js"
18
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
19
+ "static/chunks/app/layout-49b46c9b5aa512a8.js"
20
20
  ],
21
21
  "/connections/page": [
22
22
  "static/chunks/webpack-ef515e41a83a16ac.js",
@@ -24,28 +24,21 @@
24
24
  "static/chunks/89-b194fcffe5120c44.js",
25
25
  "static/chunks/main-app-6086068e5fb5eacd.js",
26
26
  "static/chunks/586-f7017d585a84738d.js",
27
- "static/chunks/839-4aefe6e71be6e1ef.js",
28
- "static/chunks/app/connections/page-0936beaf02d863c6.js"
27
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
28
+ "static/chunks/app/connections/page-eb11cfb1ffeab3ca.js"
29
29
  ],
30
- "/agents/[id]/page": [
30
+ "/agents/page": [
31
31
  "static/chunks/webpack-ef515e41a83a16ac.js",
32
32
  "static/chunks/0937d497-114a2a3de515565e.js",
33
33
  "static/chunks/89-b194fcffe5120c44.js",
34
34
  "static/chunks/main-app-6086068e5fb5eacd.js",
35
35
  "static/chunks/586-f7017d585a84738d.js",
36
36
  "static/chunks/706-fa2be0f10ffebe6a.js",
37
- "static/chunks/837-33c7480524f48577.js",
38
- "static/chunks/486-13e08542f3a1cbac.js",
39
- "static/chunks/316-26da9cc233ba7557.js",
40
- "static/chunks/839-4aefe6e71be6e1ef.js",
41
- "static/chunks/517-ccaf8cc5c733cbd8.js",
42
- "static/chunks/350-56599dc3bf06066d.js",
43
- "static/chunks/291-efae53ea5f7ee92d.js",
44
- "static/chunks/30-9adcbe92b15a6815.js",
45
- "static/chunks/705-c7e75806f77b880c.js",
46
- "static/chunks/app/agents/[id]/page-3faf40800981a736.js"
37
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
38
+ "static/chunks/517-373a80c8ba432bdc.js",
39
+ "static/chunks/app/agents/page-9bb8cba69776c719.js"
47
40
  ],
48
- "/resources/[id]/page": [
41
+ "/resources/page": [
49
42
  "static/chunks/webpack-ef515e41a83a16ac.js",
50
43
  "static/chunks/0937d497-114a2a3de515565e.js",
51
44
  "static/chunks/89-b194fcffe5120c44.js",
@@ -53,27 +46,18 @@
53
46
  "static/chunks/586-f7017d585a84738d.js",
54
47
  "static/chunks/706-fa2be0f10ffebe6a.js",
55
48
  "static/chunks/837-33c7480524f48577.js",
56
- "static/chunks/486-13e08542f3a1cbac.js",
49
+ "static/chunks/304-681348099f874a30.js",
57
50
  "static/chunks/581-3ad5c4343dad3f50.js",
58
- "static/chunks/839-4aefe6e71be6e1ef.js",
59
- "static/chunks/517-ccaf8cc5c733cbd8.js",
60
- "static/chunks/350-56599dc3bf06066d.js",
61
- "static/chunks/291-efae53ea5f7ee92d.js",
62
- "static/chunks/844-fe55292e0701f436.js",
51
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
52
+ "static/chunks/517-373a80c8ba432bdc.js",
53
+ "static/chunks/350-0fe82e622e7a670f.js",
54
+ "static/chunks/291-23f314877fa39054.js",
55
+ "static/chunks/844-519177e6198ca84f.js",
63
56
  "static/chunks/30-9adcbe92b15a6815.js",
64
- "static/chunks/491-6b4e6201f5110dff.js",
65
- "static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js"
66
- ],
67
- "/schedules/page": [
68
- "static/chunks/webpack-ef515e41a83a16ac.js",
69
- "static/chunks/0937d497-114a2a3de515565e.js",
70
- "static/chunks/89-b194fcffe5120c44.js",
71
- "static/chunks/main-app-6086068e5fb5eacd.js",
72
- "static/chunks/586-f7017d585a84738d.js",
73
- "static/chunks/839-4aefe6e71be6e1ef.js",
74
- "static/chunks/app/schedules/page-a1124397fc760413.js"
57
+ "static/chunks/491-28f093772bd460ac.js",
58
+ "static/chunks/app/resources/page-800f774e5cacf4c9.js"
75
59
  ],
76
- "/resources/page": [
60
+ "/resources/[id]/page": [
77
61
  "static/chunks/webpack-ef515e41a83a16ac.js",
78
62
  "static/chunks/0937d497-114a2a3de515565e.js",
79
63
  "static/chunks/89-b194fcffe5120c44.js",
@@ -81,29 +65,25 @@
81
65
  "static/chunks/586-f7017d585a84738d.js",
82
66
  "static/chunks/706-fa2be0f10ffebe6a.js",
83
67
  "static/chunks/837-33c7480524f48577.js",
84
- "static/chunks/486-13e08542f3a1cbac.js",
68
+ "static/chunks/304-681348099f874a30.js",
85
69
  "static/chunks/581-3ad5c4343dad3f50.js",
86
- "static/chunks/839-4aefe6e71be6e1ef.js",
87
- "static/chunks/517-ccaf8cc5c733cbd8.js",
88
- "static/chunks/350-56599dc3bf06066d.js",
89
- "static/chunks/291-efae53ea5f7ee92d.js",
90
- "static/chunks/844-fe55292e0701f436.js",
70
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
71
+ "static/chunks/517-373a80c8ba432bdc.js",
72
+ "static/chunks/350-0fe82e622e7a670f.js",
73
+ "static/chunks/291-23f314877fa39054.js",
74
+ "static/chunks/844-519177e6198ca84f.js",
91
75
  "static/chunks/30-9adcbe92b15a6815.js",
92
- "static/chunks/491-6b4e6201f5110dff.js",
93
- "static/chunks/app/resources/page-bbd0b450d3f3131d.js"
76
+ "static/chunks/491-28f093772bd460ac.js",
77
+ "static/chunks/app/resources/[id]/page-bf05836f97248ca7.js"
94
78
  ],
95
- "/sessions/page": [
79
+ "/schedules/page": [
96
80
  "static/chunks/webpack-ef515e41a83a16ac.js",
97
81
  "static/chunks/0937d497-114a2a3de515565e.js",
98
82
  "static/chunks/89-b194fcffe5120c44.js",
99
83
  "static/chunks/main-app-6086068e5fb5eacd.js",
100
84
  "static/chunks/586-f7017d585a84738d.js",
101
- "static/chunks/706-fa2be0f10ffebe6a.js",
102
- "static/chunks/176-aeaf907a6ea5627d.js",
103
- "static/chunks/839-4aefe6e71be6e1ef.js",
104
- "static/chunks/350-56599dc3bf06066d.js",
105
- "static/chunks/705-c7e75806f77b880c.js",
106
- "static/chunks/app/sessions/page-0e322811b0cde724.js"
85
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
86
+ "static/chunks/app/schedules/page-96976fff4ee23538.js"
107
87
  ],
108
88
  "/skills/page": [
109
89
  "static/chunks/webpack-ef515e41a83a16ac.js",
@@ -112,45 +92,50 @@
112
92
  "static/chunks/main-app-6086068e5fb5eacd.js",
113
93
  "static/chunks/586-f7017d585a84738d.js",
114
94
  "static/chunks/706-fa2be0f10ffebe6a.js",
115
- "static/chunks/839-4aefe6e71be6e1ef.js",
116
- "static/chunks/app/skills/page-22ea0ef89d9f7a13.js"
95
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
96
+ "static/chunks/app/skills/page-bc1acd0663f36129.js"
117
97
  ],
118
- "/types/page": [
98
+ "/sessions/page": [
119
99
  "static/chunks/webpack-ef515e41a83a16ac.js",
120
100
  "static/chunks/0937d497-114a2a3de515565e.js",
121
101
  "static/chunks/89-b194fcffe5120c44.js",
122
102
  "static/chunks/main-app-6086068e5fb5eacd.js",
123
103
  "static/chunks/586-f7017d585a84738d.js",
124
104
  "static/chunks/706-fa2be0f10ffebe6a.js",
125
- "static/chunks/839-4aefe6e71be6e1ef.js",
126
- "static/chunks/517-ccaf8cc5c733cbd8.js",
127
- "static/chunks/app/types/page-18c4e1db2724d41d.js"
105
+ "static/chunks/654-92a63b3c4b412666.js",
106
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
107
+ "static/chunks/350-0fe82e622e7a670f.js",
108
+ "static/chunks/705-780e48e55b0346bd.js",
109
+ "static/chunks/app/sessions/page-a7534ebd90e982d6.js"
128
110
  ],
129
- "/agents/page": [
111
+ "/types/page": [
130
112
  "static/chunks/webpack-ef515e41a83a16ac.js",
131
113
  "static/chunks/0937d497-114a2a3de515565e.js",
132
114
  "static/chunks/89-b194fcffe5120c44.js",
133
115
  "static/chunks/main-app-6086068e5fb5eacd.js",
134
116
  "static/chunks/586-f7017d585a84738d.js",
135
117
  "static/chunks/706-fa2be0f10ffebe6a.js",
136
- "static/chunks/839-4aefe6e71be6e1ef.js",
137
- "static/chunks/517-ccaf8cc5c733cbd8.js",
138
- "static/chunks/app/agents/page-f4c4c80f955f3967.js"
118
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
119
+ "static/chunks/517-373a80c8ba432bdc.js",
120
+ "static/chunks/app/types/page-ebd37e220f77c1f3.js"
139
121
  ],
140
- "/page": [
122
+ "/agents/[id]/page": [
141
123
  "static/chunks/webpack-ef515e41a83a16ac.js",
142
124
  "static/chunks/0937d497-114a2a3de515565e.js",
143
125
  "static/chunks/89-b194fcffe5120c44.js",
144
126
  "static/chunks/main-app-6086068e5fb5eacd.js",
145
127
  "static/chunks/586-f7017d585a84738d.js",
146
128
  "static/chunks/706-fa2be0f10ffebe6a.js",
147
- "static/chunks/486-13e08542f3a1cbac.js",
148
- "static/chunks/839-4aefe6e71be6e1ef.js",
149
- "static/chunks/517-ccaf8cc5c733cbd8.js",
150
- "static/chunks/350-56599dc3bf06066d.js",
151
- "static/chunks/291-efae53ea5f7ee92d.js",
152
- "static/chunks/844-fe55292e0701f436.js",
153
- "static/chunks/app/page-a7f292117b7d76bf.js"
129
+ "static/chunks/837-33c7480524f48577.js",
130
+ "static/chunks/304-681348099f874a30.js",
131
+ "static/chunks/316-26da9cc233ba7557.js",
132
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
133
+ "static/chunks/517-373a80c8ba432bdc.js",
134
+ "static/chunks/350-0fe82e622e7a670f.js",
135
+ "static/chunks/291-23f314877fa39054.js",
136
+ "static/chunks/30-9adcbe92b15a6815.js",
137
+ "static/chunks/705-780e48e55b0346bd.js",
138
+ "static/chunks/app/agents/[id]/page-c005161cec5527d8.js"
154
139
  ],
155
140
  "/relations/page": [
156
141
  "static/chunks/webpack-ef515e41a83a16ac.js",
@@ -161,8 +146,23 @@
161
146
  "static/chunks/2b9ff8ee-969e65cc354b2150.js",
162
147
  "static/chunks/586-f7017d585a84738d.js",
163
148
  "static/chunks/562-7c2e33d838e869bf.js",
164
- "static/chunks/839-4aefe6e71be6e1ef.js",
165
- "static/chunks/app/relations/page-fba25393c8688714.js"
149
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
150
+ "static/chunks/app/relations/page-a44fd515a3b0525e.js"
151
+ ],
152
+ "/page": [
153
+ "static/chunks/webpack-ef515e41a83a16ac.js",
154
+ "static/chunks/0937d497-114a2a3de515565e.js",
155
+ "static/chunks/89-b194fcffe5120c44.js",
156
+ "static/chunks/main-app-6086068e5fb5eacd.js",
157
+ "static/chunks/586-f7017d585a84738d.js",
158
+ "static/chunks/706-fa2be0f10ffebe6a.js",
159
+ "static/chunks/304-681348099f874a30.js",
160
+ "static/chunks/281-d8a0dcfaa6d064e4.js",
161
+ "static/chunks/517-373a80c8ba432bdc.js",
162
+ "static/chunks/350-0fe82e622e7a670f.js",
163
+ "static/chunks/291-23f314877fa39054.js",
164
+ "static/chunks/844-519177e6198ca84f.js",
165
+ "static/chunks/app/page-da042d146caeeab6.js"
166
166
  ]
167
167
  }
168
168
  }