@sjawhar/opencode-legion-envoy 2.1.0 → 2.2.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.
@@ -13902,6 +13902,13 @@ var commentValidation = {
13902
13902
  },
13903
13903
  message: `${commentOwner.message} turn requires reply_to_ask.`
13904
13904
  };
13905
+ var messageValidation = {
13906
+ check: (value) => {
13907
+ const input = value;
13908
+ return typeof input.issue === "string" || typeof input.in_reply_to === "string";
13909
+ },
13910
+ message: "issue is required unless in_reply_to names a message delivered to this session, which is the one message with no issue."
13911
+ };
13905
13912
  var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
13906
13913
  function componentsArgument(z2) {
13907
13914
  return z2.object({
@@ -14119,12 +14126,13 @@ var dispatchToolSpecs = [
14119
14126
  {
14120
14127
  name: "dispatch_message",
14121
14128
  example: { issue: "DSP-1", body: "Implementation started." },
14122
- description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + `(dispatch_ask) or document feedback (dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
14129
+ description: "Post a note humans must read now: a reply to a human's message, a deliverable that landed, or a blocker only " + "they can clear. Never progress or status updates - Dispatch is a high-signal record, not a log. Not a decision " + "(dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation, and a second call with the same in_reply_to posts nothing because " + "Dispatch keeps the one reply per message. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
14123
14130
  arguments: (z2) => ({
14124
- issue: z2.string().describe(ISSUE_REFERENCE),
14131
+ issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
14125
14132
  body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
14126
- in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation.").optional()
14127
- })
14133
+ in_reply_to: z2.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation. " + "A bare id with no issue answers a human's direct message to this session; a " + "dispatch://KEY/message/<id> names the issue its message lives on, so that form is a " + "reply on that issue.").optional()
14134
+ }),
14135
+ validation: messageValidation
14128
14136
  },
14129
14137
  {
14130
14138
  name: "dispatch_doc_edit",
@@ -15171,6 +15179,9 @@ class DispatchClient {
15171
15179
  async message(issue2, input) {
15172
15180
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "messages"], input);
15173
15181
  }
15182
+ async messageReply(id, input) {
15183
+ return this.#json("POST", ["api", "v1", "messages", id, "reply"], input);
15184
+ }
15174
15185
  async getMessage(issue2, id) {
15175
15186
  return this.#json("GET", [
15176
15187
  "api",
@@ -16019,6 +16030,10 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
16019
16030
  owner: issue3 === undefined ? null : { kind: "issue", issue: issue3 }
16020
16031
  };
16021
16032
  }
16033
+ const replyTarget = args.in_reply_to;
16034
+ if (tool === "dispatch_message" && typeof replyTarget === "string" && !replyTarget.startsWith("dispatch://")) {
16035
+ return { args, ref, owner: null };
16036
+ }
16022
16037
  const legionIssue = env.LEGION_ISSUE;
16023
16038
  if (!legionIssue) {
16024
16039
  problems.push(ownerRequiredProblem);
@@ -16443,7 +16458,7 @@ async function executeDispatchTool(input) {
16443
16458
  const schema = toolSchema(input.tool);
16444
16459
  const parsed = schema.safeParse(ownerArguments.args, { reportInput: true });
16445
16460
  if (!parsed.success) {
16446
- const issues = parsed.error.issues.filter((issue3) => !ownerMissing || !(issue3.code === "invalid_type" && issue3.path.length === 1 && issue3.path[0] === "issue" && issue3.input === undefined || issue3.code === "custom" && issue3.path.length === 0 && issue3.message.startsWith("Exactly one of issue and project is required")));
16461
+ const issues = parsed.error.issues.filter((issue3) => !ownerMissing || !(issue3.code === "invalid_type" && issue3.path.length === 1 && issue3.path[0] === "issue" && issue3.input === undefined || issue3.code === "custom" && issue3.path.length === 0 && (issue3.message.startsWith("Exactly one of issue and project is required") || issue3.message.startsWith("issue is required unless in_reply_to"))));
16447
16462
  problems.push(...formatZodIssues(issues, schema));
16448
16463
  }
16449
16464
  problems.push(...argumentProblems(input.tool, ownerArguments.args));
@@ -16873,9 +16888,23 @@ ${followsAsk(askOwner)}`,
16873
16888
  }
16874
16889
  case "dispatch_message": {
16875
16890
  const inReplyTo = messageInReplyTo(args);
16891
+ const body = stringArg(args, "body");
16892
+ if (owner === null && inReplyTo !== undefined) {
16893
+ const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor });
16894
+ if (reply.body !== body) {
16895
+ return {
16896
+ text: `Message ${inReplyTo} was already answered by message ${reply.id}; Dispatch kept ` + "that reply and posted nothing. Wait for their next message rather than answering " + "this one again.",
16897
+ details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
16898
+ };
16899
+ }
16900
+ return {
16901
+ text: `Replied to message ${inReplyTo} with message ${reply.id}`,
16902
+ details: { message: reply.id, in_reply_to: inReplyTo, posted: true }
16903
+ };
16904
+ }
16876
16905
  const issueKey = issue2();
16877
16906
  const message = await client.message(issueKey, {
16878
- body: stringArg(args, "body"),
16907
+ body,
16879
16908
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
16880
16909
  actor
16881
16910
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -807,6 +807,31 @@ through Envoy or the hub. A bearer that targets over HTTP names its own session
807
807
  target only a session that advertises the mode you want. Sending to a session with no issue
808
808
  (`POST /api/v1/agents/{session_id}/messages`) stays human-only.
809
809
 
810
+ ### Answering a direct message
811
+
812
+ A human can also message you directly from the **Agents** page, with no issue at all. That frame
813
+ names no issue and its `reply_with` hint carries none either; answer it with the message's bare
814
+ id in `in_reply_to`, alone:
815
+
816
+ ```ts
817
+ dispatch_message({
818
+ in_reply_to: "<the direct message's id>",
819
+ body: "The requested answer.",
820
+ })
821
+ ```
822
+
823
+ Leave `issue` out — there is no issue to post into, and naming one would file your answer on
824
+ unrelated work. Dispatch threads the reply under their message in the same conversation, and the
825
+ human sees it on your agent card. Every other message still names its issue, so keep the `issue`
826
+ the frame gave you whenever it gave you one; a `dispatch://KEY/message/<id>` reference names the
827
+ issue its message lives on, so that form is a reply on that issue, not a direct message.
828
+
829
+ One reply per message they send. A second call with the same `in_reply_to` posts nothing:
830
+ Dispatch answers it with the reply already stored, and the tool result says the message was
831
+ already answered rather than reporting a send. That happens most often when your host answered
832
+ the **BTW** automatically before you got here — read the result before writing again, and wait
833
+ for their next message instead.
834
+
810
835
  ## Following
811
836
 
812
837
  An ask has followers: every session that wrote to it — the session that opened it and every session that replied with