eve-hub 0.13.8 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # eve-hub
2
2
 
3
+ ## 0.14.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix the published declaration of the wake schedule, so eve-hub packs.
8
+
9
+ ## 0.14.1
10
+
11
+ ### Patch Changes
12
+
13
+ - list_agents returns a Mac bot that is out as unavailable, with the reason, instead of leaving it out.
14
+
15
+ ## 0.14.0
16
+
17
+ ### Minor Changes
18
+
19
+ - Bot-armed wake-ups: `schedules/wake` is an eve dynamic schedule a hosted bot arms for its own thread (`schedule__wake__create` with a note, a delay and `until`, the last time it may fire). When it fires, the bot's deployment tells the hub (`POST /api/wakes`), which posts a system @-mention into the thread unless the bot has had a turn there since arming. A recurring one (a cron expression) posts whenever the thread has been quiet since its last firing. Every wake-up must set `until`, so a cron is always bounded, and the first firing past it deletes the schedule. The shared instructions say when to arm one.
20
+ - Pins eve 0.83.3 (with the hub's patches). Per-session caps move to eve's `limits.sessionUsage` check over the session window; the old keys stay for bots still on eve 0.81.
21
+
22
+ ## 0.13.9
23
+
24
+ ### Patch Changes
25
+
26
+ - Bots are told their closing text is not posted once their turn has posted into its thread, so a result always goes in a `post_message`. The `post_message` and `react` tool descriptions say the same.
27
+
3
28
  ## 0.13.8
4
29
 
5
30
  ### Patch Changes
package/kit-modules.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "memory/workspace",
12
12
  "schedules/dash",
13
13
  "schedules/prune",
14
+ "schedules/wake",
14
15
  "skills/forms",
15
16
  "skills/local-onboarding",
16
17
  "skills/onboarding-profile",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.13.8",
3
+ "version": "0.14.2",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
6
6
  "type": "module",
@@ -0,0 +1,6 @@
1
+ export function registryBase(): string;
2
+ export function sandboxRegistry(): string | null;
3
+ export function registryHeaders(init?: HeadersInit, env?: Record<string, string | undefined>): Record<string, string>;
4
+ export function connectionErrorCode(error: unknown): string | null;
5
+ export function hubHeaders(init?: HeadersInit, env?: Record<string, string | undefined>): Promise<Record<string, string>>;
6
+ export function registryFetch(path: string, init?: RequestInit, options?: { base?: string; deadlineMs?: number; attemptTimeoutMs?: number; fetchImpl?: typeof fetch }): Promise<Response>;
package/roster-model.mjs CHANGED
@@ -13,6 +13,7 @@
13
13
  // - `createRosterModel({ agents, subagentCap })`: bound to a roster given
14
14
  // explicitly (e.g. a test).
15
15
  import { defineDynamic } from "eve";
16
+ import evePackage from "eve/package.json" with { type: "json" };
16
17
  import { CONTEXT_WINDOW, selectModel } from "./bot-model.mjs";
17
18
  import { DEFAULT_SUBAGENT_CAP } from "./subagent-cap.mjs";
18
19
  import { sessionLimits } from "./session-limits.mjs";
@@ -55,7 +56,7 @@ const rosterOf = ({ agents, humans, subagentCap, limits }) => ({ agents, humans,
55
56
  // `subagentCap`) for `data`, validated the same way as the file.
56
57
  export function bindRoster(data) {
57
58
  const roster = rosterOf(parseWorkspace(data));
58
- return { ...roster, rosterModel: createRosterModel(roster), rosterLimits: (name) => sessionLimits(roster, name) };
59
+ return { ...roster, rosterModel: createRosterModel(roster), rosterLimits: (name) => sessionLimits(roster, name, evePackage.version) };
59
60
  }
60
61
 
61
62
  // Bots scaffolded before bindRoster read the roster from disk instead: the
@@ -71,5 +72,5 @@ export function rosterModel(name, options) {
71
72
  }
72
73
 
73
74
  export function rosterLimits(name) {
74
- return sessionLimits(workspaceRoster(), name);
75
+ return sessionLimits(workspaceRoster(), name, evePackage.version);
75
76
  }
@@ -0,0 +1,52 @@
1
+ import { defineDynamicSchedules } from "eve/experimental/schedules";
2
+ import { z } from "zod";
3
+ export declare const WakeInput: z.ZodObject<{
4
+ note: z.ZodString;
5
+ until: z.ZodString;
6
+ }, z.core.$strict>;
7
+ /** What the provider stores: the note, the arming session and the expiry (epoch ms). */
8
+ export type ArmedWake = {
9
+ readonly note: string;
10
+ readonly sessionId: string;
11
+ readonly until: number;
12
+ };
13
+ type Principal = {
14
+ readonly type: string;
15
+ readonly authenticator: string;
16
+ readonly issuer?: string;
17
+ readonly principalId: string;
18
+ readonly subject?: string;
19
+ };
20
+ type Occurrence = {
21
+ readonly executionId: string;
22
+ readonly name: string;
23
+ readonly scheduledAt: string;
24
+ };
25
+ /** The hub relays the workspace's human as `roster:<user>` (channels/eve-roster-user.ts): the one principal a wake-up runs as. Anyone else: null, and nothing fires. */
26
+ export declare function wakeAuth(principal: Principal): {
27
+ principalType: string;
28
+ authenticator: string;
29
+ principalId: string;
30
+ attributes: {};
31
+ issuer?: string | undefined;
32
+ subject?: string | undefined;
33
+ } | null;
34
+ /** The one call a firing makes: the hub decides whether to post, keyed on the occurrence so a retry posts once.
35
+ * A definitive refusal (a 4xx: the session is unknown, the thread archived) is returned, not thrown: retrying it can't change it. */
36
+ export declare function fireWake(payload: ArmedWake, occurrence: Occurrence, { fetchImpl, base }?: {
37
+ fetchImpl?: typeof fetch;
38
+ base?: string;
39
+ }): Promise<{
40
+ status: "posted" | "skipped";
41
+ } | {
42
+ status: "refused";
43
+ error: string;
44
+ }>;
45
+ /** A firing: one scheduled past `until` removes the schedule and never reaches the hub; any other fires. */
46
+ export declare function expireOrFire(payload: ArmedWake, occurrence: Occurrence, { remove, fire }: {
47
+ remove: (name: string) => Promise<unknown>;
48
+ fire: (payload: ArmedWake, occurrence: Occurrence) => Promise<unknown>;
49
+ }): Promise<unknown>;
50
+ type WakeSchedules = ReturnType<typeof defineDynamicSchedules<z.output<typeof WakeInput>, ArmedWake>>;
51
+ declare const definition: WakeSchedules;
52
+ export default definition;
@@ -0,0 +1,70 @@
1
+ // A bot's wake-up for its own thread: an eve dynamic schedule (schedule__wake__create
2
+ // with { name, expression, payload: { note, until } }). One-shot with
3
+ // { type: "delay", minutes }; recurring with { type: "cron", cron }. Every wake-up carries
4
+ // `until`, the last time it may fire: eve has no expiry of its own, and authored code never
5
+ // sees the expression (a timing-only update can turn a one-shot into cron without calling
6
+ // preparePayload), so requiring it of every payload is what keeps a cron bounded. The model
7
+ // gives the note, the timing and `until`; the thread comes from the arming turn's session,
8
+ // which the hub reserved for that thread.
9
+ // The stored payload is a function of the input and the session only: eve prepares it
10
+ // before approval and again before writing, and refuses the write if the two differ.
11
+ // When it was armed is the hub's: it records the schedule record the tool call returns.
12
+ // When it fires, run() tells the hub (POST /api/wakes, hosted-hub/src/api/wakes.ts)
13
+ // as the bot's deployment; the hub skips it if the bot has had a turn in the thread
14
+ // since arming or its last firing, and otherwise posts a system @-mention that queues
15
+ // behind any running turn. An occurrence scheduled past `until` deletes the schedule
16
+ // instead. run() never starts a session itself: a session eve starts here is one the
17
+ // hub cannot see.
18
+ import { byPrincipal, defineDynamicSchedules, schedules } from "eve/experimental/schedules";
19
+ import { z } from "zod";
20
+ import { registryFetch } from "../registry-fetch.mjs";
21
+ export const WakeInput = z.object({
22
+ note: z.string().min(1).max(500).describe("What to pick up when it fires, in your words."),
23
+ until: z.string().datetime({ offset: true }).describe("ISO 8601 datetime: the last time it may fire; after it, the schedule deletes itself. For a one-shot, a little after its delay."),
24
+ }).strict();
25
+ /** The hub relays the workspace's human as `roster:<user>` (channels/eve-roster-user.ts): the one principal a wake-up runs as. Anyone else: null, and nothing fires. */
26
+ export function wakeAuth(principal) {
27
+ if (principal.authenticator !== "roster" || principal.type !== "user" || !principal.principalId.startsWith("roster:"))
28
+ return null;
29
+ return { principalType: principal.type, authenticator: principal.authenticator, principalId: principal.principalId, attributes: {}, ...(principal.issuer === undefined ? {} : { issuer: principal.issuer }), ...(principal.subject === undefined ? {} : { subject: principal.subject }) };
30
+ }
31
+ /** The one call a firing makes: the hub decides whether to post, keyed on the occurrence so a retry posts once.
32
+ * A definitive refusal (a 4xx: the session is unknown, the thread archived) is returned, not thrown: retrying it can't change it. */
33
+ export async function fireWake(payload, occurrence, { fetchImpl = fetch, base } = {}) {
34
+ const res = await registryFetch("/api/wakes", {
35
+ method: "POST",
36
+ headers: { "content-type": "application/json" },
37
+ body: JSON.stringify({ sessionId: payload.sessionId, name: occurrence.name, note: payload.note, executionId: occurrence.executionId, scheduledAt: occurrence.scheduledAt }),
38
+ }, { fetchImpl, deadlineMs: 10_000, ...(base === undefined ? {} : { base }) });
39
+ if (res.ok)
40
+ return res.json();
41
+ const error = `POST /api/wakes: ${res.status} ${(await res.text()).slice(0, 300)}`;
42
+ if (res.status >= 400 && res.status < 500 && ![408, 425, 429].includes(res.status))
43
+ return { status: "refused", error };
44
+ throw new Error(error);
45
+ }
46
+ /** A firing: one scheduled past `until` removes the schedule and never reaches the hub; any other fires. */
47
+ export async function expireOrFire(payload, occurrence, { remove, fire }) {
48
+ if (Date.parse(occurrence.scheduledAt) > payload.until) {
49
+ await remove(occurrence.name);
50
+ return { status: "expired" };
51
+ }
52
+ return fire(payload, occurrence);
53
+ }
54
+ const definition = defineDynamicSchedules({
55
+ description: "A wake-up for this thread: arm it when you end a turn with your own work unfinished and nothing sure to wake you. One-shot: a delay set to the expected wait. Recurring: a cron expression. Always set `until`, the last time it may fire. The hub posts it into this thread unless you have had a turn here since it was set or last fired.",
56
+ inputSchema: WakeInput,
57
+ scope: byPrincipal,
58
+ tool: true,
59
+ // A wake-up only reaches the arming bot's own thread, and an unattended turn cannot wait for approval.
60
+ approval: { create: () => "not-applicable", update: () => "not-applicable", delete: () => "not-applicable" },
61
+ preparePayload(input, context) {
62
+ return { note: input.note, sessionId: context.session.id, until: Date.parse(input.until) };
63
+ },
64
+ auth: ({ principal }) => wakeAuth(principal),
65
+ async run({ payload, occurrence }) {
66
+ // eve runs this under the creator's auth: the bound client deletes within the same byPrincipal scope.
67
+ await expireOrFire(payload, occurrence, { remove: (name) => schedules(definition).then((client) => client.delete(name)), fire: fireWake });
68
+ },
69
+ });
70
+ export default definition;
@@ -5,11 +5,18 @@
5
5
  // When a session crosses a limit, eve pauses it before the next model call and
6
6
  // asks the human to Approve (a fresh window of the same size) or Stop; the
7
7
  // registry shows that as a "session-limit" request. Delegated sessions
8
- // (subagents, `agent` copies) draw from their parent's remaining budget.
8
+ // (subagents, `agent` copies): up to eve 0.81 they draw from their parent's
9
+ // remaining budget. From 0.82 eve runs the root's `sessionUsage` on each
10
+ // child's own window, so a child may spend up to the full cap, and what it
11
+ // spent counts toward its parent's window once it replies. The check sees only
12
+ // the session it is asked about, so the old parent-remaining bound can't be
13
+ // kept from here.
9
14
  //
10
15
  // Order: DEFAULT_SESSION_LIMITS, then the file's top-level `limits`, then the
11
16
  // bot's own `limits`. A key set to `false` uncaps that axis. Plain JS: the
12
17
  // workspace parser validates with LIMIT_KEYS, and tests import it directly.
18
+ // It imports nothing: the parser runs where eve isn't installed, so the caller
19
+ // passes the eve version in (roster-model.mjs).
13
20
 
14
21
  // The keys a workspace may set (eve's session usage limits).
15
22
  export const LIMIT_KEYS = Object.freeze(["maxTokenCostUsdPerSession", "maxInputTokensPerSession", "maxOutputTokensPerSession"]);
@@ -19,10 +26,27 @@ export const LIMIT_KEYS = Object.freeze(["maxTokenCostUsdPerSession", "maxInputT
19
26
  // Input tokens keep eve's own default (40M); output tokens stay uncapped.
20
27
  export const DEFAULT_SESSION_LIMITS = Object.freeze({ maxTokenCostUsdPerSession: 75 });
21
28
 
22
- // The `limits` value for `name`'s defineAgent.
23
- export function sessionLimits({ agents = [], limits = {} } = {}, name) {
29
+ // eve's own cap on new input tokens per window, kept when a workspace sets none.
30
+ const EVE_DEFAULT_INPUT_TOKENS = 40_000_000;
31
+
32
+ // The `limits` value for `name`'s defineAgent, in the shape of the eve the bot
33
+ // runs: up to 0.81 the three keys; from 0.82 one `sessionUsage` check, and the
34
+ // three keys are an error. A Mac bot on 0.81 and a hosted bot on 0.83 get one cap.
35
+ export function sessionLimits({ agents = [], limits = {} } = {}, name, eveVersion) {
24
36
  const own = agents.find((a) => a.name === name)?.limits ?? {};
25
- return { sessionTimeoutMs: false, ...DEFAULT_SESSION_LIMITS, ...limits, ...own };
37
+ const caps = { ...DEFAULT_SESSION_LIMITS, ...limits, ...own };
38
+ const [major, minor] = eveVersion.split(".").map(Number);
39
+ if (major === 0 && minor < 82) return { sessionTimeoutMs: false, ...caps };
40
+ return { sessionTimeoutMs: false, sessionUsage: sessionUsage(caps) };
41
+ }
42
+
43
+ // The same caps as one eve 0.82+ `sessionUsage` check over the current window
44
+ // (since the last approved continuation). `false` uncaps an axis.
45
+ function sessionUsage({ maxTokenCostUsdPerSession: cost, maxInputTokensPerSession: input = EVE_DEFAULT_INPUT_TOKENS, maxOutputTokensPerSession: output }) {
46
+ return ({ window }) =>
47
+ (typeof cost === "number" && (window.costUsd ?? 0) >= cost) ||
48
+ (typeof input === "number" && window.inputTokens - window.cacheReadTokens >= input) ||
49
+ (typeof output === "number" && window.outputTokens >= output);
26
50
  }
27
51
 
28
52
  // Validation for a `limits` object: null, or the rest of an error message
@@ -23,6 +23,9 @@
23
23
  - **Waiting**
24
24
  - Never sleep or poll more than 60 seconds in one command. Check CI or a deploy once, report, and say what you're waiting on.
25
25
  - While work runs, post at least every 30 minutes: a result, or the one thing blocking you.
26
+ - Ending a turn with your own work unfinished and nothing sure to wake you (CI, a deploy, a background task, stopping partway): arm a wake-up with `schedule__wake__create`, the delay set to the expected wait, the note saying what to pick up and `until` a little after it. It posts into this thread unless you've had a turn here since.
27
+ - To check back repeatedly (a long CI run, a slow rollout), arm it with a cron expression, `until` the last time it may fire. It posts each time the thread has been quiet since the last one.
28
+ - Don't arm one while waiting on the human or on a bot you just mentioned: their reply wakes you.
26
29
  - **Ending a turn**
27
30
  - End only with results, a blocker you can't clear, or a question for the human; never "I'll do it next".
28
31
  - If the human says stop or hold off, do it and confirm in one line.
@@ -1,6 +1,7 @@
1
1
  - **One channel**
2
2
  - Everything to the human or a bot is a `post_message`: into this thread, another thread, or a bot's DM.
3
3
  - Posting doesn't end your turn. Post when you start something long, when you find something that matters, and when you're done, each as its own post.
4
+ - Once a turn has posted into this thread, the hub drops its closing text, so your result always goes in a post, never only in the text you end the turn with.
4
5
  - **Mentions wake**
5
6
  - `@<name>` wakes one bot or person; `@all` wakes every participant in the thread.
6
7
  - Each wake costs that bot a turn, so mention at most one name per message: whoever acts next, in the message that hands off.
@@ -16,6 +16,7 @@ declare const _default: import("eve/tools").ToolDefinition<Record<string, never>
16
16
  clamped?: string[];
17
17
  };
18
18
  status: string;
19
+ reason?: string;
19
20
  busyIn: string[];
20
21
  process?: {
21
22
  status: string;
@@ -48,6 +49,7 @@ declare const _default: import("eve/tools").ToolDefinition<Record<string, never>
48
49
  clamped?: string[];
49
50
  };
50
51
  status: string;
52
+ reason?: string;
51
53
  busyIn: string[];
52
54
  process?: {
53
55
  status: string;
@@ -2,7 +2,7 @@ import { defineTool } from "eve/tools";
2
2
  import { z } from "zod";
3
3
  import { registryFetch } from "../registry-fetch.mjs";
4
4
  export default defineTool({
5
- description: "List the workspace's bots right now: each one's role, kind (its place in the team: coordinator, planner, builder, reviewer, git-ops, demo, or other), where it runs (hosted or local), the tools its runtime reports (null when not yet reported), its model, the model and reasoning each of its declared subagents and its built-in `agent` copies resolve to under the workspace subagent cap (clamped from hub.workspace.json, never rejected), whether it's idle or busy (and where; a bot's threads run in parallel, so busy elsewhere doesn't mean it can't start work in a new thread), its server's memory (process.rssMb) and uptime (process.uptimeS; only on a local hub, since a hosted hub runs no bot process to measure), and any restart in progress (restart_agent, or the registry recycling a bot that grew too big or ran too long). The team changes; check it before routing work.",
5
+ description: "List the workspace's bots right now: each one's role, kind (its place in the team: coordinator, planner, builder, reviewer, git-ops, demo, or other), where it runs (hosted or local), the tools its runtime reports (null when not yet reported), its model, the model and reasoning each of its declared subagents and its built-in `agent` copies resolve to under the workspace subagent cap (clamped from hub.workspace.json, never rejected), whether it's idle, busy (and where; a bot's threads run in parallel, so busy elsewhere doesn't mean it can't start work in a new thread) or unavailable (with `reason`: a Mac bot whose Mac couldn't set up the workspace; a post that @-mentions it is refused, so route the work elsewhere or ask the human), its server's memory (process.rssMb) and uptime (process.uptimeS; only on a local hub, since a hosted hub runs no bot process to measure), and any restart in progress (restart_agent, or the registry recycling a bot that grew too big or ran too long). The team changes; check it before routing work.",
6
6
  inputSchema: z.object({}),
7
7
  async execute() {
8
8
  // Anything but a 2xx list of bots is an error naming the status and the
@@ -22,7 +22,6 @@ declare const _default: import("eve/tools").ToolDefinition<{
22
22
  threadId: string;
23
23
  title: string;
24
24
  deliveries: Record<string, string>;
25
- comms?: boolean;
26
25
  }> & {
27
26
  execute(input: {
28
27
  threadId?: string | undefined;
@@ -48,7 +47,6 @@ declare const _default: import("eve/tools").ToolDefinition<{
48
47
  threadId: string;
49
48
  title: string;
50
49
  deliveries: Record<string, string>;
51
- comms?: boolean;
52
50
  }>;
53
51
  };
54
52
  export default _default;
@@ -18,7 +18,7 @@ import { registryFetch } from "../registry-fetch.mjs";
18
18
  // move), the text is its summary, `metadata` its PR and sha. The hub keeps the
19
19
  // latest one per bot and thread for the workspace state and the Dash.
20
20
  export default defineTool({
21
- description: "Post a message as you. Leave out threadId and agent to post into the thread you're in, while you work: posting doesn't end your turn, and when your turn's last step is only comms (a plain post into this thread, a reaction), your end-of-turn text isn't posted. Or post into another workspace thread or a bot's DM. @-mentions wake bots the same way your replies do (at most one, never yourself); `@all` wakes every participant; each bot's gate decides whether it replies. Posting into a bot's DM wakes that bot. Archived threads are refused. `policy` says how a post reaches the one bot it @-mentions: queue (default) runs as its next turn, or starts one now if it is idle; steer lands in its RUNNING turn at the next step boundary and the bot folds it in without a restart, to correct course mid-turn; abort stops its turn in that thread (running commands are killed, the process keeps running) and the text starts its fresh turn, \"stop, do this instead\". Steer and abort need exactly one @-mention, not `@all`, and cut into work already under way: use them only to change what a running turn is doing. A status post sets the thread's phase: planning, working, reviewing or done; allowed moves are start → planning or working, planning → working, working → reviewing, reviewing → working (fix first), planning (rethink) or done. Any other move is refused with the allowed next phases. The text is a phase marker, not a report: at most 8 plain words on what is happening now (\"Building the input-count fix\"), with no bot or role names, no restating the phase, no parentheticals. Test results, branch names and caveats go in a separate message. The PR URL and sha go in metadata, never in the text.",
21
+ description: "Post a message as you. Leave out threadId and agent to post into the thread you're in, while you work: posting doesn't end your turn, and once your turn has posted into this thread, its end-of-turn text isn't posted, so put your result in a post. Or post into another workspace thread or a bot's DM. @-mentions wake bots the same way your replies do (at most one, never yourself); `@all` wakes every participant; each bot's gate decides whether it replies. Posting into a bot's DM wakes that bot. Archived threads are refused. `policy` says how a post reaches the one bot it @-mentions: queue (default) runs as its next turn, or starts one now if it is idle; steer lands in its RUNNING turn at the next step boundary and the bot folds it in without a restart, to correct course mid-turn; abort stops its turn in that thread (running commands are killed, the process keeps running) and the text starts its fresh turn, \"stop, do this instead\". Steer and abort need exactly one @-mention, not `@all`, and cut into work already under way: use them only to change what a running turn is doing. A status post sets the thread's phase: planning, working, reviewing or done; allowed moves are start → planning or working, planning → working, working → reviewing, reviewing → working (fix first), planning (rethink) or done. Any other move is refused with the allowed next phases. The text is a phase marker, not a report: at most 8 plain words on what is happening now (\"Building the input-count fix\"), with no bot or role names, no restating the phase, no parentheticals. Test results, branch names and caveats go in a separate message. The PR URL and sha go in metadata, never in the text.",
22
22
  inputSchema: z.object({
23
23
  threadId: z.string().optional().describe("Another thread's id, e.g. thr_0002, or a DM id, dm-<bot>. Omit it to post into the thread you're in."),
24
24
  agent: z.string().optional().describe("Post into this bot's DM instead of a thread."),
package/tools/react.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { defineTool } from "eve/tools";
2
2
  import { z } from "zod";
3
3
  import { registryFetch } from "../registry-fetch.mjs";
4
- // Reactions are the bot's own choice, not delivery bookkeeping. `comms: true` declares it talks in the thread: a turn whose last step is only comms posts no end-of-turn text.
4
+ // Reactions are the bot's own choice, not delivery bookkeeping. A turn whose last step is only reactions posts no end-of-turn text: the hub reads the tool name; `comms: true` is for hubs that predate that.
5
5
  export default defineTool({
6
- description: "React to a message in this thread with an emoji (toggles). Use the [msg_…] id shown before each message. Good for light acknowledgement (👍 got it, 👀 looking, ✅ done, 🎉 nice) when a full reply isn't needed. A turn that ends on only reactions and posts posts no closing text, so never write one.",
6
+ description: "React to a message in this thread with an emoji (toggles). Use the [msg_…] id shown before each message. Good for light acknowledgement (👍 got it, 👀 looking, ✅ done, 🎉 nice) when a full reply isn't needed. A turn that ends on a reaction, or that posted into this thread, posts no closing text, so never write one.",
7
7
  inputSchema: z.object({
8
8
  messageId: z.string().describe("The id in brackets before the message, e.g. msg_0012."),
9
9
  emoji: z.string().describe("A single emoji."),