hive-am 1.4.0 → 1.6.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 (169) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/README.md +6 -9
  3. package/bin/hive-am.mjs +242 -31
  4. package/bin/run.mjs +135 -0
  5. package/package.json +1 -1
  6. package/server/dist/connections/format.js +26 -0
  7. package/server/dist/connections/index.js +2 -0
  8. package/server/dist/connections/manager.js +18 -6
  9. package/server/dist/connections/router.js +43 -13
  10. package/server/dist/connections/secrets.js +9 -1
  11. package/server/dist/connections/slack.js +488 -0
  12. package/server/dist/connections/store.js +2 -0
  13. package/server/dist/db/migrations.js +80 -0
  14. package/server/dist/db.js +26 -13
  15. package/server/dist/dispatches.js +126 -0
  16. package/server/dist/efforts.js +105 -0
  17. package/server/dist/http/routes/agents.js +4 -3
  18. package/server/dist/http/routes/meta.js +12 -0
  19. package/server/dist/http/routes/notebook.js +10 -2
  20. package/server/dist/http/routes/orchestration.js +13 -2
  21. package/server/dist/http/routes/resources.js +3 -0
  22. package/server/dist/http/routes/types.js +18 -1
  23. package/server/dist/http/validate.js +27 -4
  24. package/server/dist/index.js +6 -1
  25. package/server/dist/instructions.js +11 -3
  26. package/server/dist/objects/agent-db.js +258 -0
  27. package/server/dist/objects/database.js +496 -34
  28. package/server/dist/objects/index.js +16 -8
  29. package/server/dist/providers/kiro.js +7 -1
  30. package/server/dist/providers/opencode-server.js +11 -1
  31. package/server/dist/providers/opencode.js +4 -3
  32. package/server/dist/providers/spawn.js +38 -4
  33. package/server/dist/retention.js +52 -0
  34. package/server/dist/runtime.js +69 -27
  35. package/server/dist/services/agents.js +53 -1
  36. package/server/dist/services/connections.js +18 -3
  37. package/server/dist/services/notebook.js +7 -3
  38. package/server/dist/skills/defaults.js +41 -5
  39. package/server/dist/skills/notebook.js +57 -13
  40. package/server/dist/watch.js +1 -1
  41. package/server/mcp/dispatch.mjs +116 -5
  42. package/web/.next/BUILD_ID +1 -1
  43. package/web/.next/app-build-manifest.json +98 -82
  44. package/web/.next/app-path-routes-manifest.json +7 -7
  45. package/web/.next/build-manifest.json +2 -2
  46. package/web/.next/prerender-manifest.json +51 -51
  47. package/web/.next/server/app/_not-found/page.js +2 -2
  48. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  49. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  50. package/web/.next/server/app/_not-found.html +1 -1
  51. package/web/.next/server/app/_not-found.rsc +3 -3
  52. package/web/.next/server/app/agents/[id]/page.js +4 -4
  53. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  54. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  55. package/web/.next/server/app/agents/page.js +2 -2
  56. package/web/.next/server/app/agents/page.js.nft.json +1 -1
  57. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  58. package/web/.next/server/app/agents.html +1 -1
  59. package/web/.next/server/app/agents.rsc +21 -18
  60. package/web/.next/server/app/chat/page.js +2 -2
  61. package/web/.next/server/app/chat/page.js.nft.json +1 -1
  62. package/web/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  63. package/web/.next/server/app/chat.html +1 -1
  64. package/web/.next/server/app/chat.rsc +4 -4
  65. package/web/.next/server/app/connections/page.js +2 -2
  66. package/web/.next/server/app/connections/page.js.nft.json +1 -1
  67. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  68. package/web/.next/server/app/connections.html +1 -1
  69. package/web/.next/server/app/connections.rsc +4 -4
  70. package/web/.next/server/app/index.html +1 -1
  71. package/web/.next/server/app/index.rsc +4 -4
  72. package/web/.next/server/app/page.js +2 -2
  73. package/web/.next/server/app/page.js.nft.json +1 -1
  74. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  75. package/web/.next/server/app/relations/page.js +2 -2
  76. package/web/.next/server/app/relations/page.js.nft.json +1 -1
  77. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  78. package/web/.next/server/app/relations.html +1 -1
  79. package/web/.next/server/app/relations.rsc +4 -4
  80. package/web/.next/server/app/resources/[id]/page.js +2 -2
  81. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -1
  82. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
  83. package/web/.next/server/app/resources/page.js +2 -2
  84. package/web/.next/server/app/resources/page.js.nft.json +1 -1
  85. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
  86. package/web/.next/server/app/resources.html +1 -1
  87. package/web/.next/server/app/resources.rsc +4 -4
  88. package/web/.next/server/app/schedules/page.js +2 -2
  89. package/web/.next/server/app/schedules/page.js.nft.json +1 -1
  90. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
  91. package/web/.next/server/app/schedules.html +1 -1
  92. package/web/.next/server/app/schedules.rsc +4 -4
  93. package/web/.next/server/app/sessions/page.js +2 -2
  94. package/web/.next/server/app/sessions/page.js.nft.json +1 -1
  95. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  96. package/web/.next/server/app/sessions.html +1 -1
  97. package/web/.next/server/app/sessions.rsc +4 -4
  98. package/web/.next/server/app/skills/page.js +2 -2
  99. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  100. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  101. package/web/.next/server/app/skills.html +1 -1
  102. package/web/.next/server/app/skills.rsc +4 -4
  103. package/web/.next/server/app/types/page.js +2 -2
  104. package/web/.next/server/app/types/page.js.nft.json +1 -1
  105. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  106. package/web/.next/server/app/types.html +1 -1
  107. package/web/.next/server/app/types.rsc +4 -4
  108. package/web/.next/server/app-paths-manifest.json +7 -7
  109. package/web/.next/server/chunks/{505.js → 111.js} +1 -1
  110. package/web/.next/server/chunks/143.js +1 -1
  111. package/web/.next/server/chunks/213.js +1 -1
  112. package/web/.next/server/chunks/368.js +1 -1
  113. package/web/.next/server/chunks/403.js +1 -1
  114. package/web/.next/server/chunks/612.js +1 -1
  115. package/web/.next/server/chunks/741.js +2 -0
  116. package/web/.next/server/chunks/837.js +1 -0
  117. package/web/.next/server/chunks/{435.js → 939.js} +3 -3
  118. package/web/.next/server/pages/404.html +1 -1
  119. package/web/.next/server/pages/500.html +1 -1
  120. package/web/.next/server/server-reference-manifest.json +1 -1
  121. package/web/.next/static/chunks/162-68ba190086f5ad39.js +1 -0
  122. package/web/.next/static/chunks/214-35b88db02113ae02.js +1 -0
  123. package/web/.next/static/chunks/281-4506bc98b7111729.js +1 -0
  124. package/web/.next/static/chunks/491-196bcb3dd11bb460.js +1 -0
  125. package/web/.next/static/chunks/517-8f2db464d10a4624.js +1 -0
  126. package/web/.next/static/chunks/{586-f7017d585a84738d.js → 586-4f67b823df060b46.js} +1 -1
  127. package/web/.next/static/chunks/587-3c53d91a1c4f99d6.js +1 -0
  128. package/web/.next/static/chunks/606-14fb5aaa4d0ff22d.js +1 -0
  129. package/web/.next/static/chunks/642-dc874c8caf203ee1.js +1 -0
  130. package/web/.next/static/chunks/{705-f81face97b1bb1af.js → 705-ce86175dea00e6a9.js} +1 -1
  131. package/web/.next/static/chunks/757-03361e7a97a605a2.js +1 -0
  132. package/web/.next/static/chunks/771-db19da9b1a24820c.js +1 -0
  133. package/web/.next/static/chunks/app/agents/[id]/page-803d53aedd10c8e5.js +1 -0
  134. package/web/.next/static/chunks/app/agents/layout-71dd98d696935e29.js +1 -0
  135. package/web/.next/static/chunks/app/agents/page-9da499db6d22731f.js +1 -0
  136. package/web/.next/static/chunks/app/chat/{page-5261b12b7d50f5d1.js → page-383bc24e72dab7fa.js} +1 -1
  137. package/web/.next/static/chunks/app/connections/page-b0ea2eae62cef74e.js +1 -0
  138. package/web/.next/static/chunks/app/layout-cb5af29b0742c513.js +1 -0
  139. package/web/.next/static/chunks/app/page-5e547e87de23d9fa.js +1 -0
  140. package/web/.next/static/chunks/app/relations/page-02710fe1f8a670a9.js +1 -0
  141. package/web/.next/static/chunks/app/resources/[id]/page-1e2afddcb1f04884.js +1 -0
  142. package/web/.next/static/chunks/app/resources/{page-61cccd8fdcc2f6c8.js → page-195b0af83b8ace0d.js} +1 -1
  143. package/web/.next/static/chunks/app/sessions/page-85638705fdc18174.js +1 -0
  144. package/web/.next/static/chunks/app/types/page-c4c2b2217c4667e8.js +1 -0
  145. package/web/.next/static/css/c7faf690373117bb.css +1 -0
  146. package/web/.next/trace +30 -30
  147. package/web/.next/types/routes.d.ts +2 -1
  148. package/web/.next/types/validator.ts +9 -0
  149. package/web/.next/server/chunks/274.js +0 -1
  150. package/web/.next/static/chunks/281-113274e705940a55.js +0 -1
  151. package/web/.next/static/chunks/316-26da9cc233ba7557.js +0 -1
  152. package/web/.next/static/chunks/391-18768499e741112e.js +0 -1
  153. package/web/.next/static/chunks/517-373a80c8ba432bdc.js +0 -1
  154. package/web/.next/static/chunks/562-7c2e33d838e869bf.js +0 -1
  155. package/web/.next/static/chunks/587-8e4d2be01ba7d48f.js +0 -1
  156. package/web/.next/static/chunks/606-c9a19918b02a98d0.js +0 -1
  157. package/web/.next/static/chunks/656-f537149fe28f79aa.js +0 -1
  158. package/web/.next/static/chunks/app/agents/[id]/page-6ed6edb3e74b0ebe.js +0 -1
  159. package/web/.next/static/chunks/app/agents/page-9bb8cba69776c719.js +0 -1
  160. package/web/.next/static/chunks/app/connections/page-eb11cfb1ffeab3ca.js +0 -1
  161. package/web/.next/static/chunks/app/layout-fad968a635f9a4b3.js +0 -1
  162. package/web/.next/static/chunks/app/page-99e0351e6ccaa384.js +0 -1
  163. package/web/.next/static/chunks/app/relations/page-a44fd515a3b0525e.js +0 -1
  164. package/web/.next/static/chunks/app/resources/[id]/page-fc494e85b01e8cb7.js +0 -1
  165. package/web/.next/static/chunks/app/sessions/page-a7534ebd90e982d6.js +0 -1
  166. package/web/.next/static/chunks/app/types/page-ebd37e220f77c1f3.js +0 -1
  167. package/web/.next/static/css/582723fcea1f9887.css +0 -1
  168. /package/web/.next/static/{YT6HM6qmegMJaBMX5QvuP → tdnUXVa01m25EjOobxVd1}/_buildManifest.js +0 -0
  169. /package/web/.next/static/{YT6HM6qmegMJaBMX5QvuP → tdnUXVa01m25EjOobxVd1}/_ssgManifest.js +0 -0
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { DATA_DIR } from '../db.js';
5
5
  import { createHash, randomBytes } from 'node:crypto';
6
6
  import { learnWindows } from '../context.js';
7
+ import { learnEfforts } from '../efforts.js';
7
8
  /**
8
9
  * One long-lived `opencode serve` per agent. A private server per turn (`opencode run --standalone`) boots from zero every time and its
9
10
  * hive MCP server connects only after the model has already taken its first step, so the first `tools.hive.*` call always fails
@@ -63,7 +64,14 @@ let windowsAt = 0;
63
64
  * 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
65
  * (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
  */
66
- export async function learnOpencodeWindows(cwd, access) {
67
+ export function learnOpencodeWindows(cwd, access) {
68
+ if (learning)
69
+ return learning; // a read is under way (a turn and the agent form can ask at the same time): both wait for it
70
+ learning = readCatalog(cwd, access).finally(() => { learning = null; });
71
+ return learning;
72
+ }
73
+ let learning = null;
74
+ async function readCatalog(cwd, access) {
67
75
  if (Date.now() - windowsAt < 60 * 60_000)
68
76
  return;
69
77
  windowsAt = Date.now();
@@ -107,6 +115,8 @@ export async function learnOpencodeWindows(cwd, access) {
107
115
  if (!list.length)
108
116
  throw new Error('the model catalog stayed empty');
109
117
  learnWindows('opencode', Object.fromEntries(list.map((m) => [`${m.providerID}/${m.id}`, Number(m.limit?.context)])));
118
+ // Every model carries the variants it takes (`id` goes after `#`, `settings.reasoningEffort` is the level it stands for); a model with none takes no effort.
119
+ learnEfforts('opencode', Object.fromEntries(list.map((m) => [`${m.providerID}/${m.id}`, (Array.isArray(m.variants) ? m.variants : []).map((v) => ({ id: String(v?.id), level: typeof v?.settings?.reasoningEffort === 'string' ? v.settings.reasoningEffort : undefined }))])));
110
120
  }
111
121
  catch {
112
122
  windowsAt = Date.now() - 50 * 60_000; /* try again in ten minutes */
@@ -2,7 +2,7 @@ import { DATA_DIR } from '../db.js';
2
2
  import { join } from 'node:path';
3
3
  import { execFile } from 'node:child_process';
4
4
  const INBOX_ROOT = join(DATA_DIR, 'inbox');
5
- import { spawnJson } from './spawn.js';
5
+ import { spawnJson, StalledError } 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';
@@ -93,7 +93,8 @@ export async function* runOpencode(o) {
93
93
  }
94
94
  else if (has('--standalone'))
95
95
  args.splice(1, 0, '--standalone'); // 1.x always runs its own server
96
- args.push(prompt);
96
+ // After `--` so a message that begins with a dash is the prompt and never an option; nothing may be pushed after it.
97
+ args.push('--', prompt);
97
98
  // Stopping a turn must really stop the model: a client that is killed would leave the server working on, so the server goes down with it.
98
99
  if (access)
99
100
  o.signal.addEventListener('abort', () => stopOpencodeServer(agent.id), { once: true });
@@ -144,7 +145,7 @@ export async function* runOpencode(o) {
144
145
  throw new Error(failure);
145
146
  }
146
147
  catch (e) {
147
- if (failure && !o.signal.aborted)
148
+ if (failure && !o.signal.aborted && !(e instanceof StalledError))
148
149
  throw new Error(failure); // OpenCode's own reason says more than "exited with code 1"
149
150
  throw e;
150
151
  }
@@ -1,6 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
+ /** A CLI that prints nothing for this long has hung (a long turn is fine as long as it keeps emitting). */
4
+ const STALL_MS = () => Number(process.env.HIVE_AM_TURN_STALL_MS) || 10 * 60_000;
5
+ const quietFor = (ms) => ms >= 60_000 ? `${Math.round(ms / 6000) / 10} minutes` : `${Math.round(ms / 100) / 10} seconds`;
3
6
  const KILL_GRACE_MS = Number(process.env.HIVE_AM_KILL_GRACE_MS) || 5000;
7
+ /** The CLI hung and was cut off; adapters must not replace this reason with an error the dying CLI printed. */
8
+ export class StalledError extends Error {
9
+ }
4
10
  /** Spawns a CLI and yields its stdout lines. Throws with stderr tail if it exits non-zero and nothing aborted it. */
5
11
  export async function* spawnLines(o) {
6
12
  const child = spawn(o.cmd, o.args, {
@@ -21,6 +27,15 @@ export async function* spawnLines(o) {
21
27
  hardKill.unref?.();
22
28
  };
23
29
  o.signal.addEventListener('abort', kill, { once: true });
30
+ // Silence, not length: the timer restarts on every stdout line and, when it fires, ends the CLI like a Stop would, but the turn fails with its own text.
31
+ let stalled = false;
32
+ let stallTimer;
33
+ const armStall = () => {
34
+ clearTimeout(stallTimer);
35
+ stallTimer = setTimeout(() => { stalled = true; kill(); }, STALL_MS());
36
+ stallTimer.unref?.();
37
+ };
38
+ armStall();
24
39
  let stderr = '';
25
40
  child.stderr.on('data', (d) => { stderr = (stderr + d).slice(-4000); });
26
41
  const exit = new Promise((res) => {
@@ -28,7 +43,7 @@ export async function* spawnLines(o) {
28
43
  // 'exit' (not 'close'): a daemon the CLI forks (e.g. opencode's background service) can keep
29
44
  // inheriting our stdout pipe forever, so 'close' would never fire. Drain briefly, then cut the pipe.
30
45
  child.on('exit', (c) => { if (hardKill)
31
- clearTimeout(hardKill); setTimeout(() => child.stdout.destroy(), 250); res(c); });
46
+ clearTimeout(hardKill); setTimeout(() => { child.stdout.destroy(); child.stderr.destroy(); }, 250); res(c); });
32
47
  });
33
48
  child.stdin.on('error', () => { });
34
49
  if (o.stdin !== undefined)
@@ -36,15 +51,23 @@ export async function* spawnLines(o) {
36
51
  else
37
52
  child.stdin.end();
38
53
  try {
39
- for await (const line of createInterface({ input: child.stdout }))
54
+ // readline only ends on the stream's 'end'; a stream we destroy (the exit handler above) emits 'close' alone, so close it ourselves.
55
+ const rl = createInterface({ input: child.stdout });
56
+ child.stdout.on('close', () => rl.close());
57
+ for await (const line of rl) {
58
+ armStall();
40
59
  if (line.trim())
41
60
  yield line;
61
+ }
42
62
  }
43
63
  finally {
64
+ clearTimeout(stallTimer);
44
65
  o.signal.removeEventListener('abort', kill);
45
66
  kill();
46
67
  }
47
68
  const code = await exit;
69
+ if (stalled && !o.signal.aborted)
70
+ throw new StalledError(`${o.cmd} stopped answering (nothing on stdout for ${quietFor(STALL_MS())})`);
48
71
  if (code !== 0 && !o.signal.aborted)
49
72
  throw new Error(`${o.cmd} exited with code ${code}${stderr ? `: ${stderr.trim().slice(-600)}` : ''}`);
50
73
  }
@@ -56,13 +79,24 @@ export function safeJson(line) {
56
79
  return null;
57
80
  }
58
81
  }
59
- /** The JSON objects a CLI prints, one per line; lines that are not JSON (banners, warnings) are skipped. */
82
+ /**
83
+ * The JSON objects a CLI prints, one per line; lines that are not JSON (banners, warnings) are skipped.
84
+ * A CLI that exits cleanly having printed only prose (its usage text, say) has not answered: that is an error carrying its first line.
85
+ */
60
86
  export async function* spawnJson(o) {
87
+ let events = 0;
88
+ let firstNoise = '';
61
89
  for await (const line of spawnLines(o)) {
62
90
  const ev = safeJson(line);
63
- if (ev)
91
+ if (ev) {
92
+ events++;
64
93
  yield ev;
94
+ }
95
+ else
96
+ firstNoise ||= line.trim();
65
97
  }
98
+ if (!events && firstNoise && !o.signal.aborted)
99
+ throw new Error(`${o.cmd} produced no events, it printed: ${firstNoise.slice(0, 300)}`);
66
100
  }
67
101
  /** The text of a tool result, which CLIs send as a string, a list of blocks or any other JSON. */
68
102
  export function flattenText(c) {
@@ -0,0 +1,52 @@
1
+ import { db } from './db.js';
2
+ import { historySig } from './history/index.js';
3
+ import { RETENTION_DAYS } from './connections/files.js';
4
+ /**
5
+ * What hive-am keeps of its own record, and for how long. Three tables only ever grew: the messages of channel conversations, the log of
6
+ * delegations and the list of known conversations. The conversations themselves live in each CLI's own store and are never touched here:
7
+ * only hive-am's copy (the row that lists them) goes. A row is removed only when it is past its cap AND old enough, so nothing recent is lost.
8
+ */
9
+ export const KEEP_THREAD_MESSAGES = 500; // the newest of each thread...
10
+ export const MESSAGE_DAYS = 30; // ...and anything younger than this, whatever the count
11
+ export const KEEP_DISPATCHES = 200; // the screen shows 50
12
+ export const KEEP_DELEGATIONS = 50; // delegated conversations per agent (a read-only log), kept while younger than SESSION_FORGET_DAYS
13
+ export const SESSION_FORGET_DAYS = 90; // a conversation not used for this long, and that the CLI no longer has, is forgotten
14
+ // Received files live RETENTION_DAYS (connections/files.ts, 14): the record of a file on a message goes with the file.
15
+ const DAY = 86_400_000;
16
+ export function retentionPass({ now = Date.now() } = {}) {
17
+ const forgetBefore = now - SESSION_FORGET_DAYS * DAY;
18
+ // A direct conversation goes only when it is old AND its transcript is gone. `historySig` is null for a failed read too: the age is what makes that harmless.
19
+ const gone = [];
20
+ const old = db.prepare("SELECT s.agent_id, s.session_id, s.provider, s.cwd FROM agent_sessions s WHERE s.kind='direct' AND s.last_seen < ? AND s.session_id IS NOT (SELECT session_id FROM agents WHERE id=s.agent_id)")
21
+ .all(forgetBefore);
22
+ for (const s of old)
23
+ if (historySig({ provider: s.provider, cwd: s.cwd }, s.session_id) === null)
24
+ gone.push(s);
25
+ return db.transaction(() => {
26
+ const out = { messages: 0, dispatches: 0, sessions: 0, files: 0 };
27
+ // Messages: beyond the newest KEEP_THREAD_MESSAGES of their thread and older than the window.
28
+ const cut = db.prepare('SELECT id FROM thread_messages WHERE thread_id=? ORDER BY id DESC LIMIT 1 OFFSET ?');
29
+ const drop = db.prepare('DELETE FROM thread_messages WHERE thread_id=? AND id<? AND ts<?');
30
+ for (const { thread_id } of db.prepare('SELECT DISTINCT thread_id FROM thread_messages').all()) {
31
+ const edge = cut.get(thread_id, KEEP_THREAD_MESSAGES - 1);
32
+ if (edge)
33
+ out.messages += drop.run(thread_id, edge.id, now - MESSAGE_DAYS * DAY).changes;
34
+ }
35
+ // The record of a received file goes with the file.
36
+ out.files = db.prepare('UPDATE thread_messages SET files=NULL WHERE files IS NOT NULL AND ts<?').run(now - RETENTION_DAYS * DAY).changes;
37
+ // Delegations: all but the newest KEEP_DISPATCHES. Never one that has not finished, nor a background one whose result nobody has had
38
+ // yet (not handed to the orchestrator, nor read with dispatch_status): that one stays until it is.
39
+ out.dispatches = db.prepare(`DELETE FROM dispatches WHERE status NOT IN ('queued','running') AND NOT (mode='background' AND notified_at IS NULL AND read_at IS NULL)
40
+ AND id IN (SELECT id FROM dispatches ORDER BY created_at DESC, id DESC LIMIT -1 OFFSET ?)`).run(KEEP_DISPATCHES).changes;
41
+ // Delegated conversations: past the newest KEEP_DELEGATIONS of their agent and not used for SESSION_FORGET_DAYS.
42
+ out.sessions += db.prepare(`DELETE FROM agent_sessions WHERE kind='delegation' AND last_seen<? AND (agent_id, session_id) IN (
43
+ SELECT agent_id, session_id FROM (SELECT agent_id, session_id, ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY last_seen DESC, session_id) AS n FROM agent_sessions WHERE kind='delegation') WHERE n>?)`)
44
+ .run(forgetBefore, KEEP_DELEGATIONS).changes;
45
+ // Direct conversations the CLI no longer has (the current one of an agent was left out above, and is checked again here).
46
+ const del = db.prepare("DELETE FROM agent_sessions WHERE agent_id=? AND session_id=? AND kind='direct' AND session_id IS NOT (SELECT session_id FROM agents WHERE id=?)");
47
+ for (const g of gone)
48
+ out.sessions += del.run(g.agent_id, g.session_id, g.agent_id).changes;
49
+ return out;
50
+ })();
51
+ }
52
+ export const retentionTouched = (r) => r.messages + r.dispatches + r.sessions + r.files > 0;
@@ -4,6 +4,7 @@ import { agents, dispatches, resolved } from './db.js';
4
4
  import { composeInstructions, mcpCaps, notebookBlock } from './instructions.js';
5
5
  import { runners } from './providers/index.js';
6
6
  import { notebooks } from './skills/notebook.js';
7
+ import { resolveEffort } from './efforts.js';
7
8
  export const bus = new EventEmitter();
8
9
  bus.setMaxListeners(0);
9
10
  const emit = (m) => bus.emit('msg', m);
@@ -22,6 +23,19 @@ export const liveOrigin = (id) => states.get(id)?.live?.origin;
22
23
  export const liveSource = (id) => states.get(id)?.live?.source;
23
24
  /** Whether the turn running now is the agent's own conversation (not work delegated to it by an orchestrator). */
24
25
  export const liveIsDirect = (id) => states.get(id)?.live?.source === 'user';
26
+ /**
27
+ * The reasoning effort a turn asks for: the one a connection sets for its own turns, else the agent's. One that does not apply to the agent's
28
+ * model (it changed, or the model never had that level) falls back to the CLI's own default, with a line in the log, instead of failing the turn.
29
+ */
30
+ function effortOf(agent, fromOrigin) {
31
+ const asked = fromOrigin || agent.effort;
32
+ if (!asked)
33
+ return undefined;
34
+ const use = resolveEffort(agent.provider, agent.model, asked);
35
+ if (!use)
36
+ console.warn(`[effort] ${agent.name}: "${asked}" does not apply to ${agent.model || 'the default model'} (${agent.provider}): the turn runs at the CLI's own default`);
37
+ return use || undefined;
38
+ }
25
39
  /** Queue a prompt for an agent. Turns for one agent run strictly one at a time. */
26
40
  export function sendTurn(agentId, prompt, source = 'user', from, dispatchId, origin) {
27
41
  const s = st(agentId);
@@ -34,36 +48,48 @@ export function sendTurn(agentId, prompt, source = 'user', from, dispatchId, ori
34
48
  async function execute(agentId, prompt, source, from, dispatchId, origin) {
35
49
  const s = st(agentId);
36
50
  s.queued--;
37
- const raw = agents.get(agentId);
38
- if (!raw)
39
- return { ok: false, text: '', error: 'Agent no longer exists' };
40
- const agent = resolved(raw);
41
- if (!agent.cwd)
42
- return { ok: false, text: '', error: 'This agent has no working folder. Set one on the agent or on its colony.' };
43
- const delegated = source === 'dispatch';
44
- if (delegated) {
45
- // Every delegation gets its own native session, so the agent's direct conversation never fills up with delegated work.
46
- agent.session_id = null;
51
+ let raw;
52
+ let agent;
53
+ let delegated = false;
54
+ try {
55
+ raw = agents.get(agentId);
56
+ if (!raw)
57
+ return { ok: false, text: '', error: 'Agent no longer exists' };
58
+ agent = resolved(raw);
59
+ if (!agent.cwd)
60
+ return { ok: false, text: '', error: 'This agent has no working folder. Set one on the agent or on its colony.' };
61
+ delegated = source === 'dispatch';
62
+ if (delegated) {
63
+ // Every delegation gets its own native session, so the agent's direct conversation never fills up with delegated work.
64
+ agent.session_id = null;
65
+ }
66
+ else if (raw.session_id && raw.session_cwd && raw.session_cwd !== agent.cwd) {
67
+ // Native sessions are tied to the folder they started in; if the folder changed, start a fresh one.
68
+ agents.setSession(raw, null);
69
+ agent.session_id = null;
70
+ emit({ kind: 'agents_changed' });
71
+ }
47
72
  }
48
- else if (raw.session_id && raw.session_cwd && raw.session_cwd !== agent.cwd) {
49
- // Native sessions are tied to the folder they started in; if the folder changed, start a fresh one.
50
- agents.setSession(raw, null);
51
- agent.session_id = null;
52
- emit({ kind: 'agents_changed' });
73
+ catch (e) {
74
+ // Preparing a turn can fail too (a damaged row, a database write): that is this turn's error, not a rejected promise nobody waits for.
75
+ return { ok: false, text: '', error: e instanceof Error ? e.message : String(e) };
53
76
  }
54
77
  const turnId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
55
78
  const controller = new AbortController();
56
- s.controller = controller;
57
- s.live = { turnId, prompt, events: [], source, origin };
58
- agents.setStatus(agentId, 'running');
59
- emit({ kind: 'turn_start', agentId, turnId, prompt, source, from, channel: origin && { platform: origin.platform, place: origin.place, user: origin.userName } });
60
- emit({ kind: 'status', agentId, status: 'running', queued: s.queued });
61
79
  let text = '';
62
80
  let summary;
63
81
  let error;
82
+ let events = 0;
64
83
  let hash = '', nbVersion = 0;
65
84
  const push = (event) => { s.live?.events.push(event); emit({ kind: 'event', agentId, turnId, event }); };
66
85
  try {
86
+ s.controller = controller;
87
+ s.live = { turnId, prompt, events: [], source, origin };
88
+ if (dispatchId)
89
+ dispatches.setRunning(dispatchId);
90
+ agents.setStatus(agentId, 'running');
91
+ emit({ kind: 'turn_start', agentId, turnId, prompt, source, from, channel: origin && { platform: origin.platform, place: origin.place, user: origin.userName } });
92
+ emit({ kind: 'status', agentId, status: 'running', queued: s.queued });
67
93
  // Composing the instructions can fail too (a damaged skill row...): that is this turn's error, and it must not leave the agent "running" forever.
68
94
  const base = composeInstructions(agent, delegated);
69
95
  nbVersion = notebooks.get(agentId).version;
@@ -74,9 +100,11 @@ async function execute(agentId, prompt, source, from, dispatchId, origin) {
74
100
  const stream = runners[agent.provider]({
75
101
  agent, prompt, signal: controller.signal,
76
102
  instructions, refreshInstructions,
77
- mcpCaps: mcpCaps(agent), effort: origin?.effort,
103
+ mcpCaps: mcpCaps(agent), effort: effortOf(agent, origin?.effort),
78
104
  });
79
105
  for await (const ev of stream) {
106
+ if (ev.t !== 'done')
107
+ events++;
80
108
  if (ev.t === 'session') {
81
109
  if (delegated) {
82
110
  agents.recordSession(agent, ev.sessionId, 'delegation', from, prompt);
@@ -107,6 +135,11 @@ async function execute(agentId, prompt, source, from, dispatchId, origin) {
107
135
  push({ t: 'error', message: error });
108
136
  }
109
137
  }
138
+ // A turn with no event of any kind (no session, text, tool or usage) never reached the model: it is not an empty answer.
139
+ if (!events && !error && !controller.signal.aborted) {
140
+ error = 'The agent produced no events, so the turn did not run';
141
+ push({ t: 'error', message: error });
142
+ }
110
143
  if (controller.signal.aborted && !error)
111
144
  push({ t: 'done', ok: false, summary: 'Stopped' });
112
145
  s.controller = null;
@@ -126,8 +159,11 @@ export function stopAgent(agentId, clearQueue = true) {
126
159
  s.controller.abort();
127
160
  return true;
128
161
  }
129
- /** Orchestrator → worker delegation. Enforces the explicit assignment rule. */
130
- export async function dispatch(fromId, workerName, task) {
162
+ /**
163
+ * Orchestrator → worker delegation. Enforces the explicit assignment rule. The task is stored and queued on the worker at once; `done`
164
+ * settles when the worker's turn ends, after the row holds the result, so nothing depends on the caller still waiting for it.
165
+ */
166
+ export function startDispatch(fromId, workerName, task, mode = 'sync', origin = null) {
131
167
  const from = agents.get(fromId);
132
168
  if (!from)
133
169
  throw new Error('Unknown orchestrator');
@@ -136,8 +172,14 @@ export async function dispatch(fromId, workerName, task) {
136
172
  throw new Error(`No agent named "${workerName}"`);
137
173
  if (!from.worker_ids.includes(worker.id))
138
174
  throw new Error(`"${worker.name}" is not assigned to ${from.name}`);
139
- const id = dispatches.start(from.id, worker.id, task);
140
- const res = await sendTurn(worker.id, task, 'dispatch', from.id, id);
141
- dispatches.finish(id, res.ok ? 'done' : 'failed', res.text || res.error || '');
142
- return res;
175
+ const id = dispatches.start(from.id, worker.id, task, mode, origin);
176
+ const done = sendTurn(worker.id, task, 'dispatch', from.id, id).then((res) => {
177
+ dispatches.finish(id, res.ok ? 'done' : 'failed', res.text || res.error || '');
178
+ return res;
179
+ });
180
+ return { id, worker, done };
181
+ }
182
+ /** A delegation awaited to the end (the original blocking form, kept for callers that hold the request open). */
183
+ export async function dispatch(fromId, workerName, task) {
184
+ return startDispatch(fromId, workerName, task).done;
143
185
  }
@@ -1,4 +1,6 @@
1
- import { agents, db, resolved } from '../db.js';
1
+ import { agents, colonies, db, resolved } from '../db.js';
2
+ import { notebooks } from '../skills/notebook.js';
3
+ import { checkEffort } from '../efforts.js';
2
4
  import { channelPermissionError } from '../connections/rules.js';
3
5
  import { liveTurn, notifyAgentsChanged, stopAgent } from '../runtime.js';
4
6
  import { findRepo } from '../git/repo.js';
@@ -20,9 +22,14 @@ function create(p) {
20
22
  notifyAgentsChanged();
21
23
  return a;
22
24
  }
25
+ /** An effort that does not apply to the provider and model is refused (an empty one always applies). */
26
+ const checkAgentEffort = (provider, model, effort) => { const why = checkEffort(provider, model, effort); if (why)
27
+ throw bad(why); };
23
28
  export function createAgent(p) {
24
29
  const name = trimmed(p.name);
25
30
  uniqueName(name);
31
+ if (p.effort !== undefined && p.provider)
32
+ checkAgentEffort(p.provider, p.model ?? '', p.effort);
26
33
  return create({ ...p, name, cwd: p.cwd ?? '' });
27
34
  }
28
35
  /** A new agent that starts from a type: the type lends everything but name, description, folder and colony. */
@@ -31,13 +38,51 @@ export function spawnFromType(t, p) {
31
38
  uniqueName(name);
32
39
  return create({
33
40
  name, description: p.description ?? t.description, role: t.role, type_id: t.id, provider: t.provider, model: t.model,
41
+ effort: checkEffort(t.provider, t.model, t.effort) === null ? t.effort : '', // a type whose level the model no longer takes starts at the default
34
42
  system_prompt: t.system_prompt, permission: t.permission, cwd: p.cwd ?? '', skill_ids: t.skill_ids, skill_loads: t.skill_loads, colony_id: p.colony_id ?? null,
35
43
  });
36
44
  }
45
+ /**
46
+ * A separate agent with the same configuration as `sourceId`: it runs in parallel with it (the turns of one agent are serialized, so conversations at the
47
+ * same time need separate agents) and READS THE SAME NOTEBOOK. It copies what the agent is (description, role, type, provider, model, prompt, permission,
48
+ * folder, overrides, browser, skills and, for an orchestrator, its team) and nothing it has done or is attached to: not its conversation or sessions,
49
+ * connections, wake-ups, schedules, watches, delegations or managed resources. Its colony is the source's unless another is given.
50
+ */
51
+ export function cloneAgent(sourceId, p) {
52
+ const src = agentOrFail(sourceId);
53
+ const name = trimmed(typeof p.name === 'string' ? p.name : '');
54
+ if (!name)
55
+ throw bad('Give the clone a name');
56
+ uniqueName(name);
57
+ const colony = p.colony_id === undefined ? src.colony_id : p.colony_id ? String(p.colony_id) : null;
58
+ if (colony && !colonies.get(colony))
59
+ throw bad('That colony no longer exists');
60
+ const clone = create({
61
+ name, description: src.description, role: src.role, type_id: src.type_id, provider: src.provider, model: src.model, effort: src.effort, system_prompt: src.system_prompt,
62
+ permission: src.permission, cwd: src.cwd, overrides: src.overrides, browser: src.browser, skill_ids: src.skill_ids, skill_loads: src.skill_loads,
63
+ worker_ids: src.role === 'orchestrator' ? src.worker_ids : [], colony_id: colony, cloned_from: src.id,
64
+ });
65
+ try {
66
+ notebooks.share(src.id, clone.id);
67
+ }
68
+ catch (e) {
69
+ agents.remove(clone.id);
70
+ throw e;
71
+ } // never leave a clone that does not read the notebook of its source
72
+ notifyAgentsChanged();
73
+ return agents.get(clone.id);
74
+ }
37
75
  export function updateAgent(id, p) {
38
76
  const cur = agentOrFail(id);
39
77
  if (p.name && p.name.toLowerCase() !== cur.name.toLowerCase())
40
78
  uniqueName(p.name, id);
79
+ // The effort has to apply to the provider and model the agent will have. One that is sent is checked; one that was saved and no longer applies
80
+ // (the provider or the model changed) falls back to the CLI's default instead of failing every turn.
81
+ const provider = p.provider ?? cur.provider, model = p.model ?? cur.model;
82
+ if (p.effort !== undefined)
83
+ checkAgentEffort(provider, model, p.effort);
84
+ else if (cur.effort && checkEffort(provider, model, cur.effort) !== null)
85
+ p = { ...p, effort: '' };
41
86
  // Changing provider invalidates the native session pointer. (A changed folder is detected when the next turn starts.)
42
87
  // All-or-nothing: a change that would leave a linked agent in Plan mode is rolled back.
43
88
  const a = db.transaction(() => {
@@ -68,6 +113,13 @@ export function sessionToRead(a, requested) {
68
113
  return sid;
69
114
  }
70
115
  /** Only the agent's own conversations can be made current; delegated ones are read-only. */
116
+ /** True when an OpenCode conversation started under a different permission than the agent has now: OpenCode keeps the tools a session started with. */
117
+ export function permissionStale(a) {
118
+ if (a.provider !== 'opencode' || !a.session_id)
119
+ return false;
120
+ const row = agents.sessions(a.id).find((s) => s.session_id === a.session_id);
121
+ return !!row && row.permission !== null && row.permission !== a.effective.permission;
122
+ }
71
123
  export function resumeSession(a, sessionId) {
72
124
  if (!agents.sessions(a.id).some((s) => s.session_id === sessionId && s.kind !== 'delegation'))
73
125
  throw bad('Only the agent’s own conversations can be made current. Delegated sessions are read-only.');
@@ -5,13 +5,28 @@ import { connections } from '../connections/store.js';
5
5
  import { notifyAgentsChanged } from '../runtime.js';
6
6
  import { bad, notFound } from '../http/errors.js';
7
7
  import { CONNECTION_KINDS, isRecord, trimmed, validateConnection } from '../http/validate.js';
8
+ /** Ids with an optional name: trimmed, de-duplicated, blanks dropped. */
9
+ function cleanIds(list) {
10
+ const seen = new Set();
11
+ return list
12
+ .map((c) => { const r = isRecord(c) ? c : {}; return { id: trimmed(r.id), name: r.name ? String(r.name).trim() : undefined }; })
13
+ .filter((c) => c.id && !seen.has(c.id) && seen.add(c.id));
14
+ }
15
+ /** One group's mode: `everyone` (or nothing) keeps the entry as `{ id, name }`; `members` keeps its cleaned list; anything else is left for validation to refuse. */
16
+ function cleanChat(c) {
17
+ const r = isRecord(c) ? c : {};
18
+ const [chat] = cleanIds([r]);
19
+ if (!chat || r.mode === undefined || r.mode === null || r.mode === 'everyone')
20
+ return chat;
21
+ if (r.mode !== 'members')
22
+ return { ...chat, mode: r.mode, members: r.members };
23
+ return { ...chat, mode: 'members', members: Array.isArray(r.members) ? cleanIds(r.members) : r.members ?? [] };
24
+ }
8
25
  /** Group ids and alias names from the form: trimmed, de-duplicated, blanks dropped. */
9
26
  function cleanGroupConfig(config) {
10
27
  if (Array.isArray(config.chats)) {
11
28
  const seen = new Set();
12
- config.chats = config.chats
13
- .map((c) => { const r = isRecord(c) ? c : {}; return { id: trimmed(r.id), name: r.name ? String(r.name).trim() : undefined }; })
14
- .filter((c) => c.id && !seen.has(c.id) && seen.add(c.id));
29
+ config.chats = config.chats.map(cleanChat).filter((c) => c && !seen.has(c.id) && seen.add(c.id));
15
30
  }
16
31
  if (Array.isArray(config.aliases))
17
32
  config.aliases = [...new Set(config.aliases.map((x) => String(x).trim()).filter(Boolean))];
@@ -1,13 +1,17 @@
1
1
  import { agents, resolved } from '../db.js';
2
2
  import { mcpCaps } from '../instructions.js';
3
3
  import { liveOrigin } from '../runtime.js';
4
- import { NOTEBOOK_MAX } from '../skills/notebook.js';
4
+ import { NOTEBOOK_MAX, notebooks } from '../skills/notebook.js';
5
5
  import { NOTEBOOK_SKILL_ID } from '../skills/defaults.js';
6
6
  import { bad } from '../http/errors.js';
7
- /** Who a note came from, so it can be audited: the date plus, for chat turns, where and who. */
7
+ /**
8
+ * Who a note came from, so it can be audited: the date plus, for chat turns, where and who. In a notebook that several agents share it also
9
+ * says which agent wrote it: "who wrote this" is what tells a useful note from a mystery.
10
+ */
8
11
  export function noteSource(agentId) {
9
12
  const day = new Date().toISOString().slice(0, 10), o = liveOrigin(agentId);
10
- return o ? `${day} · ${o.platform}: ${o.userName}` : day;
13
+ const who = notebooks.sharedWith(agentId).length ? agents.get(agentId)?.name : undefined;
14
+ return [day, who, o ? `${o.platform}: ${o.userName}` : ''].filter(Boolean).join(' · ');
11
15
  }
12
16
  /** The agent behind a notebook tool call: it must exist and have the Notebook skill. */
13
17
  export function notebookAgent(from) {
@@ -5,16 +5,18 @@ export const CHANNELS_SKILL_ID = 'default-channels';
5
5
  export const WAKEUPS_SKILL_ID = 'default-wakeups';
6
6
  /** Running the servers and containers of the colony. Its tools (`object_list`, `object_action`, `object_logs`) exist only for agents that have this skill. */
7
7
  export const OBJECTS_SKILL_ID = 'default-objects';
8
+ export const DATABASES_SKILL_ID = 'default-databases';
8
9
  /**
9
10
  * How to react to messages in a chat. It is part of the "Chat channels" skill; agents that already had that skill get it added once
10
- * (see `migrateChannelSkill`). Telegram's list is the one in `connections/telegram.ts` (`TELEGRAM_REACTIONS`): keep them in step.
11
+ * (see `migrateChannelSkill`). Telegram's list is the one in `connections/telegram.ts` (`TELEGRAM_REACTIONS`), Slack's the one in `connections/slack.ts`
12
+ * (`SLACK_REACTION_NAMES`): keep them in step.
11
13
  */
12
14
  export const REACTIONS_SECTION = `## Reactions
13
- - \`channel_react\` puts ONE emoji on the message you are handling. It is lighter than a reply and counts as your answer when there is nothing to add. A new reaction replaces the previous one on that message.
15
+ - \`channel_react\` puts ONE emoji on the message you are handling. It is lighter than a reply and counts as your answer when there is nothing to add. On Telegram a new reaction replaces the previous one on that message; on Slack they add up.
14
16
  - React instead of writing when a text would only be noise: 👍 or 🫡 for "understood, on it", 🙏 or ❤ for thanks, 🎉 when someone shares good news, 👀 for "I am looking at it", 👌 for "done" when nothing more needs saying.
15
17
  - Do not react instead of answering a question or a request that needs a result: say it with \`channel_reply\`. You can react first (👀) and answer when you finish.
16
18
  - Do not react to every message, do not react to chatter nobody aimed at you (in a group with \`Addressed: no\` stay silent), and never use an emoji that could read as mocking, sarcastic or as a judgement of a person.
17
- - **Each platform allows only some emoji.** On Telegram a bot can use only this list: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 😢 🎉 🤩 🙏 👌 🕊 🤡 🥱 🥴 😍 🐳 ❤‍🔥 🌚 🌭 💯 🤣 ⚡ 🍌 🏆 💔 🤨 😐 🍓 🍾 💋 😈 😴 😭 🤓 👻 👨‍💻 👀 🎃 🙈 😇 😨 🤝 ✍ 🤗 🫡 🎅 🎄 ☃ 💅 🤪 🗿 🆒 💘 🙉 🦄 😘 💊 🙊 😎 👾 🤷. The ones that fit work best: 👍 🙏 ❤ 🎉 👀 🫡 👌 🔥 💯. Common ones that are NOT allowed there: ✅ ❌ ✔ 😀 😊 😂 🚀 ⭐ (use 👌 or 👍 instead of ✅, 😁 or 🤣 instead of 😂). Stick to those if you are not sure about another platform.
19
+ - **Each platform allows only some emoji.** On Telegram a bot can use only this list: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 😢 🎉 🤩 🙏 👌 🕊 🤡 🥱 🥴 😍 🐳 ❤‍🔥 🌚 🌭 💯 🤣 ⚡ 🍌 🏆 💔 🤨 😐 🍓 🍾 💋 😈 😴 😭 🤓 👻 👨‍💻 👀 🎃 🙈 😇 😨 🤝 ✍ 🤗 🫡 🎅 🎄 ☃ 💅 🤪 🗿 🆒 💘 🙉 🦄 😘 💊 🙊 😎 👾 🤷. The ones that fit work best: 👍 🙏 ❤ 🎉 👀 🫡 👌 🔥 💯. Common ones that are NOT allowed there: ✅ ❌ ✔ 😀 😊 😂 🚀 ⭐ (use 👌 or 👍 instead of ✅, 😁 or 🤣 instead of 😂). On Slack a wider set works (✅ ❌ 🚀 😂 ⭐ 😊 among them; Slack's \`:name:\` form is accepted too), and the Telegram list above works there as well. Stick to the Telegram list if you are not sure about another platform.
18
20
  - If the tool refuses the emoji (a platform, or a group that limited its reactions), it says so and lists what you can use: pick another one, or answer with text if the message needs an answer. Do not retry the same emoji.
19
21
  `;
20
22
  export const DEFAULT_SKILLS = [
@@ -59,6 +61,7 @@ Only things that will still help in a *future* conversation and that you could n
59
61
 
60
62
  ## Answering
61
63
  - **Your normal text is not delivered** to the chat. To answer, call the \`channel_reply\` tool with the text; it goes to the thread of the message you are handling. You can call it more than once (for example a short heads-up before long work, then the result).
64
+ - On Slack a conversation is a thread: your reply goes into the thread of the message you are handling (it opens one on a message written in the channel), and people who keep talking in that thread are talking to you. A wake-up or a scheduled message is posted to the channel itself, not into a thread. A DM has no threads.
62
65
  - Once you have sent your answer and have nothing else to do, end the turn at once without writing more text: it is never delivered.
63
66
  - Keep replies short and conversational. Use plain Markdown. Never use interactive question tools: if you need something, ask in the reply.
64
67
  - The header also says the current local time (\`Now: …\`). Use it to answer what time or day it is, without running \`date\`.
@@ -72,7 +75,7 @@ Only things that will still help in a *future* conversation and that you could n
72
75
  - For a picture the block may include "What the picture shows": a text written for you by an image model, because you cannot open images yourself. That is how you see the picture, so talk about it naturally ("I can see…"). Never tell the person that you received a description or that an image model was used, and never say you cannot see the image. It can contain mistakes: if a detail is unclear or missing, say it is not clear in the picture and ask for a sharper one.
73
76
 
74
77
  ## Groups: listen to everything, speak only when it helps
75
- - In groups the header also says \`Addressed: yes|no\`. **\`yes\`** means somebody called you (your name, an @mention, a reply to you, a command): answer that. **\`no\`** means people are talking among themselves and nobody asked you anything: you keep reading, and **silence is the default** — do not call \`channel_reply\` and write no text. Speak without being called only when what you add is real, verified and worth interrupting for:
78
+ - In groups the header also says \`Addressed: yes|no\`. **\`yes\`** means somebody called you (your name, an @mention, a reply to you, a message in a Slack thread you already answered, a command): answer that. **\`no\`** means people are talking among themselves and nobody asked you anything: you keep reading, and **silence is the default** — do not call \`channel_reply\` and write no text. Speak without being called only when what you add is real, verified and worth interrupting for:
76
79
  1. **A mistake**: somebody says something wrong about the project, the environment, the branch, the deploy or what you did, and you checked (files, logs, commands) that it is wrong. Correct it briefly with what is right and where you saw it.
77
80
  2. **A danger — this one you must never let pass.** Somebody suggests or is about to run something destructive or irreversible (\`rm -rf\`, deleting or overwriting data, \`DROP\`/\`TRUNCATE\`, force-push, restarting or changing a production server), uses the wrong environment (production instead of QA), or pastes a secret in the chat. Speak at once, even if the message is for somebody else and even if you were not called: say what would break, why, and the safer way, in two or three lines. Staying silent here is the worst answer.
78
81
  3. **A missing fact that changes what they do**: they are about to test something that you know is down or not deployed, or a question to the whole group that nobody answered and you answered from something you checked, in a few lines.
@@ -150,7 +153,40 @@ The resources of your own colony, or the ones that belong to no colony if you ar
150
153
  ## Care
151
154
  - Stopping or restarting a server or a container interrupts whoever is using it. If the person did not ask for it and the resource is running fine, ask first.
152
155
  - Logs can contain secrets (tokens, passwords, connection strings). Never repeat them in a chat; replace them with \`***\`.
153
- - Do not run the resource's command yourself from a terminal as a way around \`object_action\`: use the tool, so hive-am knows what is running.`,
156
+ - Do not run the resource's command yourself from a terminal as a way around \`object_action\`: use the tool, so hive-am knows what is running.
157
+
158
+ ## Databases
159
+ A **database** resource is not started or stopped: it is used. \`object_list\` shows its state, but to look inside it or run SQL you need the "Database management" skill.`,
160
+ },
161
+ {
162
+ slug: 'databases',
163
+ load: 'always',
164
+ name: 'Database management',
165
+ description: 'Look inside the database resources of the colony and use them: structure, queries, writes, saved queries, history, and (with Full access) their connection.',
166
+ content: `The colony you belong to can have **database resources** (MySQL or MariaDB, PostgreSQL, SQL Server, SQLite). You can look at their structure, run SQL, keep saved queries and, with Full access, change how they connect. hive-am opens a connection when you ask something and closes it right after.
167
+
168
+ ## Which databases are yours
169
+ The ones of your own colony, or the ones that belong to no colony if you are in none, unless a person chose one agent to look after a database (then only that agent). Another colony's databases are not visible to you. Use the exact name that \`object_db_list\` shows. \`object_db_list\` answers even when you do not have the Colony resources skill.
170
+
171
+ ## How to work
172
+ 1. **Look before you run.** \`object_db_list\`, then \`object_db_schema\` (databases, tables and views), \`object_db_columns\` and \`object_db_describe\` (keys, indexes, foreign keys, row count), \`object_db_search\` (find a table or column by name), \`object_db_ddl\` (the CREATE statement) and \`object_db_sample\` (the first rows without writing SQL). \`object_db_explain\` shows the plan of a statement without running it.
173
+ 2. **Run SQL** with \`object_db_query\` (one statement: a read, a write or a DDL) or \`object_db_exec\` (several statements in ONE transaction: all are applied or none; MySQL commits by itself on a CREATE, ALTER or DROP, so those cannot be undone there). Another database of the same server: pass \`database\`.
174
+ 3. **Keep what is useful**: \`object_db_saved\` (list, add, update, remove named queries) and \`object_db_history\` (the last 200 queries, including the ones people ran). \`object_db_export\` writes a result as CSV or JSON to a file inside your own working folder (it never overwrites unless you say so).
175
+ 4. **A result is cut at the resource's row limit and a query at its time limit**: ask for fewer rows or columns instead of working around it. Never \`SELECT *\` from a big table without a \`LIMIT\` or a \`WHERE\`: it fills your context.
176
+
177
+ ## What you may do
178
+ Your permission decides it (the \`readOnly\` setting of the resource is for people using its panel and does not apply to you):
179
+ - **Read-only (plan)**: everything that looks and reads. The connection is read-only and only SELECT, WITH, SHOW, DESCRIBE and EXPLAIN pass.
180
+ - **Edit files (acceptEdits)**: also INSERT, UPDATE, DELETE, CREATE and ALTER, scripts, saved queries, clearing the history, exports and \`object_db_sessions\`. **DROP and TRUNCATE are refused**, also inside a script (nothing of it runs).
181
+ - **Full access (bypassPermissions)**: also DROP and TRUNCATE, \`object_db_config_set\` (host, port, user, password, database, SSL, driver properties, limits, name, colony, manager), \`object_db_create\`, \`object_db_delete\` and \`object_db_kill\`.
182
+
183
+ ## Care
184
+ - **Writes and DDL are real and irreversible.** Think first: say what you are going to change and on which database, prefer a transaction (\`object_db_exec\`), try a \`SELECT\` with the same \`WHERE\` before an UPDATE or DELETE, and suggest a backup before anything that cannot be undone. A DELETE or UPDATE without a WHERE is the commonest mistake.
185
+ - hive-am refuses DROP and TRUNCATE without Full access, but that is a guardrail, not a sandbox: it does not see a DELETE without a WHERE, a CALL, a PostgreSQL DO block or a stored procedure. The real boundary is what the database user is allowed to do: recommend a user with limited grants.
186
+ - **Results can contain secrets** (password hashes, tokens, personal data). Never repeat them in a chat; replace them with \`***\`.
187
+ - **Passwords are write-only.** You can set or replace one with \`object_db_create\` or \`object_db_config_set\`, but no tool will ever show you a stored one (\`object_db_config\` only says whether one is set). Never repeat a password, write it in a file, a note or a chat, or put it in a query that is not needed. If a person types one in a chat, use it once in the tool without echoing it, and prefer asking them to type it in the resource's panel. When you write SQL that sets a password (\`CREATE USER … IDENTIFIED BY\`), hive-am replaces the literal with \`***\` in the history and the logs.
188
+ - Changing a connection (\`object_db_config_set\`) points hive-am at that server with those credentials. Do it only when asked.
189
+ - \`object_db_kill\` cancels what another session is running: look at \`object_db_sessions\` first and do it only when asked.`,
154
190
  },
155
191
  {
156
192
  slug: 'git-workflow',