relay-companion 0.1.231 → 0.1.232

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/mcp.js +94 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.231",
3
+ "version": "0.1.232",
4
4
  "description": "Companion CLI for Relay (sendrelays.com): pairs this machine with your Relay account so coding agents like Claude Code and Codex can send and receive Relay messages.",
5
5
  "homepage": "https://sendrelays.com",
6
6
  "repository": {
package/src/mcp.js CHANGED
@@ -5,7 +5,7 @@ import { prepareOrdinaryRelayAttachments } from "./attachments.js";
5
5
  import { workspacePassportFromDeclaration } from "./repo-identity.js";
6
6
  import { fragileLinkWarning } from "./links.js";
7
7
  import { createRequire } from "node:module";
8
- import { randomUUID } from "node:crypto";
8
+ import { createHash, randomUUID } from "node:crypto";
9
9
  import { RelayClient } from "./client.js";
10
10
  import { apiUrl, readConfig } from "./config.js";
11
11
  import { productFeatures } from "./product-features.js";
@@ -18,11 +18,14 @@ import {
18
18
  normalizeCompanionMode,
19
19
  } from "./config.js";
20
20
 
21
+ export const FOR_HUMAN_SOFT_WORD_LIMIT = 80;
22
+ export const FOR_HUMAN_DEFAULT_SENTENCE_LIMIT = 4;
23
+
21
24
  export const RELAY_MCP_INSTRUCTIONS = [
22
25
  "Relay is general person-to-person messaging with optional agent context and agent work, not an engineering-only workflow.",
23
26
  "A visible chat is one chronological room for an exact person or saved group. threadId is only opaque AI retrieval metadata: never invent or expose a thread/topic name or separate visible thread UI.",
24
27
  "Reading message bodies through relay_inbox_list, relay_thread_fetch, or relay_chat_fetch marks the returned inbound Relays read and sends their read receipts. Summary-only relay_chats_list does not.",
25
- "relay_send always has a 3-6 word title plus forHuman, and may have forAgent. Write the complete forAgent document first whenever agent context is useful, then derive forHuman as the concise message this person actually needs. forHuman is normally one short paragraph in the sender's recipient-specific voice; lengthen it only when the recipient genuinely needs context to weigh, discuss, or form an opinion. forAgent may be as long and detailed as necessary: under-sending to the recipient's agent is worse than over-sending. Never move technical detail into forHuman merely because forAgent is long, and never duplicate one report into both documents.",
28
+ `relay_send has a 3-6 word title, forHuman, and optional forAgent. forHuman normally uses no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words. A longer draft is stopped for a mandatory second review and may proceed only when shortening would lose the user's intended message. Preserve the sender's vocabulary, rhythm, directness, formality, warmth, and sign-off while making it digestible; history teaches voice and relationship register, not length. Put technical detail in forAgent, which may be as long and detailed as necessary, and never duplicate the two documents.`,
26
29
  "Choose relay_send kind from the requested outcome: human correspondence is message; external work to be carried out by the recipient's agent is task (a visible Request), even when the sentence addresses 'you' or the operation is small.",
27
30
  "Provider AI-session tools control native Claude/Codex sessions; they do not send Relay correspondence. A Relay-owned Request Run attaches its provider's terminal answer automatically; do not send a separate completion Relay.",
28
31
  ].join(" ");
@@ -31,7 +34,7 @@ const REQUESTS_DISABLED_INSTRUCTIONS = [
31
34
  "Relay is general person-to-person messaging with optional agent context, not an engineering-only workflow.",
32
35
  "A visible chat is one chronological room for an exact person or saved group. threadId is only opaque AI retrieval metadata: never invent or expose a thread/topic name or separate visible thread UI.",
33
36
  "relay_send sends ordinary correspondence with kind='message'. Requests are unavailable in this Relay release. Never attempt kind='task' or promise that the recipient can Start agent work.",
34
- "relay_send always has a 3-6 word title plus forHuman, and may have forAgent. Write complete agent context first whenever useful, then derive the concise human message. Technical detail belongs in forAgent, not forHuman.",
37
+ `relay_send always has a 3-6 word title plus forHuman, and may have forAgent. Write complete agent context first whenever useful, then derive the concise human message. forHuman normally uses no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words; a longer draft must be reviewed a second time and explicitly confirmed only when shortening would lose the user's intended message. Preserve the sender's voice while making it digestible. Technical detail belongs in forAgent, not forHuman.`,
35
38
  ].join(" ");
36
39
 
37
40
  export const TOOLS = [
@@ -92,7 +95,7 @@ export const TOOLS = [
92
95
  {
93
96
  name: "relay_send",
94
97
  description:
95
- "This is the normal sending tool. Use relay_send for BOTH ordinary Relay correspondence and a direct Request to one other person's agent. A kind='message' send has two visible species: forHuman by itself is a plain text message in the chat; forHuman plus a non-empty forAgent is a two-document Relay. Never invent an agent document merely to turn a text into a Relay. CLASSIFY THE OUTCOME, NOT THE SENTENCE'S ADDRESSEE: use kind='message' when the sender is telling, asking, discussing, sharing, or seeking the PERSON'S opinion, memory, judgment, or decision; use kind='task' when the recipient is asked to carry out external work through their agent, even if the wording says 'you' and never names an agent. A technical subject is not automatically a Request, and including a forAgent document never turns a message into one. Relay-owned Run here sessions attach their provider's final answer automatically after the native turn settles, so do not call relay_send merely to report that run's completion. A two-document Relay has exactly two content documents: forHuman and forAgent. When agent context is useful, compose the complete forAgent document first, then derive forHuman as the concise message the person actually needs. forHuman is normally one short paragraph; use more only when the person genuinely needs context to weigh, discuss, or form an opinion. forAgent may be as long and detailed as necessary: under-sending to the recipient's agent is worse than over-sending. Never lengthen forHuman merely because forAgent is long, and never duplicate one report into both. A technical Relay gives the person only the high-level consequence, ask, or decision in forHuman and puts mechanisms, evidence, paths, logs, code, reproduction steps, constraints, and implementation detail in forAgent. A personal message may need only forHuman; that is a text message, not a one-section Relay. Relay shows the two documents as 'For you' and 'For <agent app>' when forAgent is non-empty. RELATED RELAYS — check this before every send: people see each Relay as a message inside their direct or group chat; they do not see named threads or topics. Relay keeps an unnamed reply chain only so an AI can fetch related Relays together. Look at BOTH sides of the recent exchange with this recipient: relay_sent_list for what this human already sent them, and relay_inbox_list for what they sent back. You MUST call relay_sent_list rather than rely on memory — your own earlier sends are not in relay_inbox_list, and a send from a previous session is not in your context. If the new message continues an existing exchange, pass inReplyToRelayId set to the LATEST related relay id (whichever side sent it). A one-way series of updates can still be related: rounds 2, 3 and 4 of a piece of work, a correction, or a status update on something already relayed should link back so an AI can retrieve the set in one call. This linkage never creates a visible topic, title, pane, or conversation name. Omit inReplyToRelayId only when no earlier Relay supplies useful context. Granular digital employees appear as managed, workspace-labelled contacts; use relay_contacts_search and pass the exact matching contactId so a user with several workspaces reaches the intended employee. That message is authored by this human and wakes their employee; it is NOT a direct Request. For other recipients, an on-Relay message appears in their companion and Relays list; an off-Relay recipient receives an email open link.",
98
+ `Send ordinary Relay correspondence or a direct Request. CLASSIFY THE OUTCOME, NOT THE SENTENCE'S ADDRESSEE: kind='message' is telling, asking, discussing, sharing, or seeking the PERSON'S opinion or decision; kind='task' asks the recipient's AGENT to perform external work. A technical subject or non-empty forAgent does not itself make a Request. Relay-owned Request Runs attach their provider's final answer automatically, so do not call relay_send merely to report completion. HUMAN COPY IS SHORT BY DEFAULT: forHuman normally uses no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words. Relay stops a longer first attempt for review. Shorten repetition and move mechanisms, evidence, paths, logs, chronology, and implementation detail into forAgent; confirm the exact longer draft only when shortening would lose the user's intended message. When agent context is useful, compose the complete forAgent document first, then derive forHuman as the concise message. Preserve the sender's vocabulary, rhythm, directness, formality, warmth, and sign-off; message history teaches voice and relationship register, not a default word count. forAgent may be as detailed as necessary—under-sending to the recipient's agent is worse than over-sending—and must not duplicate forHuman. Before continuing an exchange, inspect BOTH relay_sent_list and relay_inbox_list; your earlier sends never appear in the inbox. Set inReplyToRelayId to the latest related Relay from either side. A one-way series of updates can still be related: this is an unnamed reply chain only so an AI can fetch related Relays together, never a visible topic. For a Granular digital employee, use relay_contacts_search and the exact matching workspace-labelled contactId.`,
96
99
  inputSchema: {
97
100
  type: "object",
98
101
  properties: {
@@ -111,12 +114,6 @@ export const TOOLS = [
111
114
  "A contact-group id (grp_...) from relay_groups_list. Sends one Relay into the group chat for every member.",
112
115
  },
113
116
  },
114
- anyOf: [
115
- { required: ["contactId"] },
116
- { required: ["relayUserId"] },
117
- { required: ["email"] },
118
- { required: ["groupId"] },
119
- ],
120
117
  },
121
118
  kind: {
122
119
  type: "string",
@@ -132,9 +129,14 @@ export const TOOLS = [
132
129
  forHuman: {
133
130
  type: "string",
134
131
  description:
135
- "The sender's message to the human recipient: what they should know, answer, feel, discuss, or decide, ghostwritten in the sender's voice. DEFAULT TO ONE SHORT PARAGRAPH. Use more than one paragraph only when the sender is sharing thoughts or context that the recipient genuinely needs to weigh, react to, discuss, or form an opinion about. Lots of available detail is never a reason to lengthen forHuman. For technical or operational subjects, give only the high-level consequence, current state, ask, or decision; put mechanisms, evidence, code, paths, logs, reproduction steps, constraints, chronology, and implementation detail in forAgent. Do not use headings, lists, tables, or code blocks here. Preserve recipient-specific voice, rhythm, directness, formality, and sign-off habits without inventing context or padding. The sender's words in this chat are evidence, but their real sent messages are the best guide: when their voice or relationship register is not already clear, inspect several real messages with relay_sent_list and relay_chat_fetch before writing. A personal message, opinion question, invitation, or ordinary update may belong entirely here. Never repeat the title, narrate your drafting process, or duplicate the agent document.",
132
+ `The sender's message to the person: what they should know, answer, feel, discuss, or decide, ghostwritten in the sender's voice. DEFAULT TO NO MORE THAN ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} NORMALLY SIZED SENTENCES AND ${FOR_HUMAN_SOFT_WORD_LIMIT} WORDS. A longer draft is stopped for review; shorten it unless extra length is necessary to preserve the user's intended message. Preserve recipient-specific vocabulary, rhythm, directness, formality, warmth, emphasis, and sign-off while making it digestible. History from relay_sent_list and relay_chat_fetch teaches voice and relationship register, not target length. Lots of detail never justifies a longer human message. Keep only the high-level consequence, state, ask, or decision here; put mechanisms, evidence, code, paths, logs, reproduction steps, constraints, chronology, and implementation detail in forAgent. No headings, lists, tables, code blocks, title repetition, drafting narration, or duplication of forAgent.`,
133
+ },
134
+ longForHumanConfirmed: {
135
+ type: "boolean",
136
+ description:
137
+ `Set true only when Relay has already rejected this exact over-${FOR_HUMAN_SOFT_WORD_LIMIT}-word draft, you reviewed it again, and you genuinely believe shortening would lose what the user is trying to say to this recipient. Never set it preemptively or merely because more detail is available.`,
136
138
  },
137
- forAgent: { type: "string", description: "The recipient agent's complete document: everything useful that the human does not need to read. Draft this first whenever agent context is useful. Write it agent-to-agent, precise and self-contained. It may be as long and detailed as necessary; under-sending here is worse than over-sending. Preserve conclusions, judgments, constraints, rejected options, failed attempts, preferences, open questions, next steps, and any source material the recipient's agent may need. For technical or operational subjects, this is where mechanisms, evidence, code, paths, logs, reproduction steps, chronology, references, machine-readable data, and execution or verification guidance belong. For nontechnical subjects, preserve useful background without forcing an engineering template. Structure it with Markdown when structure improves use. Do not repeat conversational prose from forHuman. Leave it empty only when the recipient's agent truly needs nothing beyond the human message; an empty value makes the send a plain text message. When non-empty, Relay shows it under 'For <agent app>' and the agent receives both documents." },
139
+ forAgent: { type: "string", description: "The recipient agent's complete document, self-contained and containing everything useful that the person need not read. Draft it first whenever agent context is useful. It may be as long and detailed as necessary; under-sending here is worse than over-sending. Preserve conclusions, constraints, rejected options, failures, preferences, questions, next steps, sources, mechanisms, evidence, code, paths, logs, reproduction steps, chronology, data, and verification guidance. Use Markdown when useful and do not repeat forHuman. Leave empty only when the recipient's agent needs nothing beyond the human message; that makes the send plain text." },
138
140
  targetSurfaces: {
139
141
  type: "array",
140
142
  description:
@@ -178,12 +180,6 @@ export const TOOLS = [
178
180
  idempotencyKey: { type: "string" },
179
181
  },
180
182
  required: ["recipient", "kind", "title", "forHuman", "idempotencyKey"],
181
- allOf: [
182
- {
183
- if: { properties: { kind: { const: "task" } } },
184
- then: { properties: { recipient: { not: { required: ["groupId"] } } } },
185
- },
186
- ],
187
183
  },
188
184
  },
189
185
  {
@@ -337,7 +333,7 @@ export const TOOLS = [
337
333
  {
338
334
  name: "relay_chat_reply",
339
335
  description:
340
- "Reply with a human-only message inside an existing Relay chat. This always sends kind 'message'; it cannot create a direct Request and it has no forAgent document. You never name a recipient, so you cannot get it wrong — Relay addresses the reply from the message it answers, and when that message came through a contact group the reply fans out to the WHOLE roster so every member sees it, exactly like a group chat. Identify the chat by chatId (from relay_chats_list) or threadId (an internal lookup key from any Relay), and pass forHuman. Read the conversation with relay_chat_fetch first so you are answering what was actually said. Use relay_send when starting a new chat, creating a Request, including forAgent context, or when the human explicitly addresses a particular person or group.",
336
+ `Reply inside an existing Relay chat; this always sends kind 'message' and cannot create a direct Request. Read it with relay_chat_fetch first. forHuman defaults to no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words; a longer first attempt is stopped for review and may be confirmed only when shortening would lose the user's intended message. Preserve the sender's voice while making it digestible. Never name a recipient: Relay addresses the reply from the message it answers and fans a group reply out to the whole roster. Use chatId or threadId. Use relay_send to start a chat, create a Request, or include forAgent context.`,
341
337
  inputSchema: {
342
338
  type: "object",
343
339
  properties: {
@@ -346,7 +342,12 @@ export const TOOLS = [
346
342
  type: "string",
347
343
  description: "Opaque internal reply-chain key from a Relay. Resolves to its enclosing person/group chat. Pass this or chatId.",
348
344
  },
349
- forHuman: { type: "string", description: "The message, in Markdown, and it is GHOSTWRITTEN: the chat shows it as the SENDER speaking, so write it in their inferred voice and at their length — see relay_send forHuman for the full law. A chat message, not a memo and not a report: no headings, no bullet lists, no wall of text. If the reply needs to hand the other side's agent file paths, commands, or verification steps, do not paste them here — send it with relay_send (inReplyToRelayId set so the AI can retrieve the related Relays) and put that detail in forAgent." },
345
+ forHuman: { type: "string", description: `A message ghostwritten as the SENDER speaking. Preserve their vocabulary, rhythm, directness, formality, warmth, and sign-off while making it digestible. Default to no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words. History teaches voice, not target length. No headings, lists, or wall of text. Put paths, commands, and verification detail in relay_send.forAgent instead.` },
346
+ longForHumanConfirmed: {
347
+ type: "boolean",
348
+ description:
349
+ `Set true only when Relay has already rejected this exact over-${FOR_HUMAN_SOFT_WORD_LIMIT}-word reply and, after reviewing it again, you genuinely believe shortening would lose the user's intended message.`,
350
+ },
350
351
  title: {
351
352
  type: "string",
352
353
  description:
@@ -360,7 +361,6 @@ export const TOOLS = [
360
361
  idempotencyKey: { type: "string", description: "A unique key of at least 8 characters for this reply." },
361
362
  },
362
363
  required: ["forHuman", "idempotencyKey"],
363
- anyOf: [{ required: ["chatId"] }, { required: ["threadId"] }],
364
364
  },
365
365
  },
366
366
  {
@@ -601,12 +601,11 @@ function toolsForFeatures(tools, { requests = true } = {}) {
601
601
  if (tool.name !== "relay_send") return tool;
602
602
  const send = structuredClone(tool);
603
603
  send.description =
604
- "Send ordinary person-to-person Relay correspondence. Requests are unavailable in this release, so kind must be 'message'. Use forHuman for the concise message the person needs and forAgent for complete agent context when useful. Check relay_sent_list and relay_inbox_list before continuing an existing exchange, and pass the newest related Relay as inReplyToRelayId.";
604
+ `Send ordinary person-to-person Relay correspondence. Requests are unavailable in this release, so kind must be 'message'. Use forHuman for the concise message the person needs and forAgent for complete agent context when useful. forHuman normally uses no more than ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences and ${FOR_HUMAN_SOFT_WORD_LIMIT} words; a longer draft is stopped for a mandatory second review and may proceed only when shortening would lose the user's intended message. Preserve the sender's voice while making the message digestible. Check relay_sent_list and relay_inbox_list before continuing an existing exchange, and pass the newest related Relay as inReplyToRelayId.`;
605
605
  send.inputSchema.properties.kind.enum = ["message"];
606
606
  send.inputSchema.properties.kind.description =
607
607
  "Required. Must be 'message' for ordinary correspondence. Requests (kind='task') are unavailable in this release.";
608
608
  delete send.inputSchema.properties.targetSurfaces;
609
- delete send.inputSchema.allOf;
610
609
  return send;
611
610
  });
612
611
  }
@@ -752,6 +751,74 @@ function relayTitleWordCount(value) {
752
751
  return String(value || "").trim().split(/\s+/u).filter(Boolean).length;
753
752
  }
754
753
 
754
+ const pendingLongForHumanReviews = new Map();
755
+ const MAX_PENDING_LONG_FOR_HUMAN_REVIEWS = 256;
756
+
757
+ function relayHumanWordCount(value) {
758
+ return String(value || "").trim().split(/\s+/u).filter(Boolean).length;
759
+ }
760
+
761
+ function longForHumanReviewKey(toolName, args) {
762
+ return `${toolName}:${String(args?.idempotencyKey || "").trim()}`;
763
+ }
764
+
765
+ function longForHumanFingerprint(toolName, args) {
766
+ return createHash("sha256")
767
+ .update(toolName)
768
+ .update("\0")
769
+ .update(String(args?.idempotencyKey || ""))
770
+ .update("\0")
771
+ .update(String(args?.forHuman || ""))
772
+ .digest("hex");
773
+ }
774
+
775
+ function rememberLongForHumanReview(key, fingerprint) {
776
+ pendingLongForHumanReviews.delete(key);
777
+ pendingLongForHumanReviews.set(key, fingerprint);
778
+ while (pendingLongForHumanReviews.size > MAX_PENDING_LONG_FOR_HUMAN_REVIEWS) {
779
+ pendingLongForHumanReviews.delete(pendingLongForHumanReviews.keys().next().value);
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Make an overlong agent-written human message a deliberate second-pass choice,
785
+ * not a soft adjective the model can silently reinterpret. The first attempt is
786
+ * rejected before any fetch, attachment read, or API call. A confirmation is
787
+ * accepted only for that exact draft after Relay has already returned the review
788
+ * instruction in this MCP process; changing the draft starts a fresh review.
789
+ */
790
+ function requireLongForHumanReview(toolName, args) {
791
+ const wordCount = relayHumanWordCount(args?.forHuman);
792
+ const key = longForHumanReviewKey(toolName, args);
793
+ if (wordCount <= FOR_HUMAN_SOFT_WORD_LIMIT) {
794
+ pendingLongForHumanReviews.delete(key);
795
+ return;
796
+ }
797
+
798
+ const fingerprint = longForHumanFingerprint(toolName, args);
799
+ const reviewedExactDraft = pendingLongForHumanReviews.get(key) === fingerprint;
800
+ if (args?.longForHumanConfirmed === true && reviewedExactDraft) {
801
+ pendingLongForHumanReviews.delete(key);
802
+ return;
803
+ }
804
+
805
+ rememberLongForHumanReview(key, fingerprint);
806
+ throw new Error(
807
+ `forHuman is ${wordCount} words; Relay's normal human-message limit is ${FOR_HUMAN_SOFT_WORD_LIMIT} words `
808
+ + `(about ${FOR_HUMAN_DEFAULT_SENTENCE_LIMIT} normally sized sentences). Nothing was sent. Review this exact draft again. `
809
+ + "Shorten it in the sender's own voice by removing repetition and moving mechanisms, evidence, paths, logs, chronology, and implementation detail into forAgent (use relay_send for a two-document Relay). "
810
+ + "If, after that review, you genuinely believe the extra length is necessary to preserve what the user is trying to say to this recipient, retry this exact draft with longForHumanConfirmed: true and the same idempotencyKey. Do not confirm merely because more detail is available.",
811
+ );
812
+ }
813
+
814
+ function requireRelaySendRecipient(recipient) {
815
+ const supplied = [recipient?.contactId, recipient?.relayUserId, recipient?.email, recipient?.groupId]
816
+ .some((value) => String(value || "").trim());
817
+ if (!supplied) {
818
+ throw new Error("recipient must include contactId, relayUserId, email, or groupId");
819
+ }
820
+ }
821
+
755
822
  export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_MODE, features = { requests: true } } = {}) {
756
823
  const normalizedMode = normalizeCompanionMode(mode);
757
824
  if (
@@ -819,6 +886,7 @@ export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_
819
886
  );
820
887
  }
821
888
  case "relay_send": {
889
+ requireRelaySendRecipient(args.recipient);
822
890
  if (!args.kind) {
823
891
  throw new Error(
824
892
  "kind is required: choose 'message' for correspondence with the person or 'task' for a direct Request to their agent",
@@ -837,9 +905,10 @@ export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_
837
905
  if (titleWordCount < 3 || titleWordCount > 6) {
838
906
  throw new Error(
839
907
  `title must be a 3-6 word gist; received ${titleWordCount} words. `
840
- + "Move evidence, chronology, qualifications, and additional findings into forHuman or forAgent, then retry with the same idempotencyKey.",
908
+ + "Move implementation evidence, chronology, technical qualifications, and additional findings into forAgent. Keep forHuman to the consequence, ask, opinion, or decision the recipient needs, then retry with the same idempotencyKey.",
841
909
  );
842
910
  }
911
+ requireLongForHumanReview("relay_send", args);
843
912
  return text(
844
913
  relaySendResultForAgent(await client.sendRelay({
845
914
  recipient: args.recipient,
@@ -960,6 +1029,7 @@ export async function handleCall(client, name, args, { mode = DEFAULT_COMPANION_
960
1029
  case "relay_chat_fetch":
961
1030
  return text(withoutThreadTitles(await markFetchedInboundRead(client, await fetchChatForAgent(client, args))));
962
1031
  case "relay_chat_reply": {
1032
+ requireLongForHumanReview("relay_chat_reply", args);
963
1033
  const chat = await markFetchedInboundRead(client, await fetchChatForAgent(client, args));
964
1034
  const replyTo = chat?.replyToRelayId;
965
1035
  if (!replyTo) {