@masons/agent-network 0.6.22 → 0.6.24

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 (41) hide show
  1. package/README.md +40 -5
  2. package/dist/_vendor/runtime-adapter-client/exact-target-presentation.d.ts +7 -0
  3. package/dist/_vendor/runtime-adapter-client/exact-target-presentation.d.ts.map +1 -0
  4. package/dist/_vendor/runtime-adapter-client/exact-target-presentation.js +51 -0
  5. package/dist/_vendor/runtime-adapter-client/index.d.ts +1 -0
  6. package/dist/_vendor/runtime-adapter-client/index.d.ts.map +1 -1
  7. package/dist/_vendor/runtime-adapter-client/index.js +1 -0
  8. package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.d.ts +2 -2
  9. package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.d.ts.map +1 -1
  10. package/dist/_vendor/runtime-adapter-client/runtime-adapter-api.js +105 -20
  11. package/dist/_vendor/runtime-adapter-client/types.d.ts +17 -5
  12. package/dist/_vendor/runtime-adapter-client/types.d.ts.map +1 -1
  13. package/dist/_vendor/runtime-adapter-client/types.js +13 -0
  14. package/dist/_vendor/runtime-adapter-client/work-target.d.ts.map +1 -1
  15. package/dist/_vendor/runtime-adapter-client/work-target.js +23 -5
  16. package/dist/channel-setup.d.ts +1 -0
  17. package/dist/channel-setup.d.ts.map +1 -1
  18. package/dist/channel-setup.js +4 -3
  19. package/dist/cli-setup.d.ts.map +1 -1
  20. package/dist/cli-setup.js +7 -7
  21. package/dist/config.js +1 -1
  22. package/dist/handoff-acceptance.js +5 -5
  23. package/dist/handoff-deadline.js +1 -1
  24. package/dist/handoff.d.ts +1 -0
  25. package/dist/handoff.d.ts.map +1 -1
  26. package/dist/handoff.js +43 -10
  27. package/dist/platform-client.d.ts +37 -32
  28. package/dist/platform-client.d.ts.map +1 -1
  29. package/dist/platform-client.js +42 -29
  30. package/dist/plugin.d.ts.map +1 -1
  31. package/dist/plugin.js +8 -6
  32. package/dist/tools.d.ts +4 -0
  33. package/dist/tools.d.ts.map +1 -1
  34. package/dist/tools.js +293 -111
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/openclaw.plugin.json +6 -5
  38. package/package.json +1 -1
  39. package/skills/agent-network/SKILL.md +90 -46
  40. package/skills/agent-network/references/maintenance.md +3 -3
  41. package/skills/agent-network/references/troubleshooting.md +71 -13
package/dist/tools.js CHANGED
@@ -1,16 +1,22 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
2
3
  import { getOwnerPassportAddress } from "./channel.js";
3
4
  import { _resetChannelSetupForTesting, consumeChannelSetupStatus, getDefaultChannelSetupOptions, startOrGetChannelSetup, } from "./channel-setup.js";
4
5
  import { clearTargetHandle, extractNetworkConfig, getConnectorClient, getDmScope, getPendingTarget, hasApiKey, isProfileNeeded, markProfileComplete, removeIdentityLinks, requireApiKey, requireConversationManager, requirePlatformConfig, writeIdentityLinks, } from "./config.js";
5
6
  import { getOwnerHandle } from "./environment-context.js";
6
7
  import { classifyIdentityError, formatCanonicalNode, formatKindLabel, } from "./identity-format.js";
7
8
  import { ownerNotesQueue } from "./owner-notes.js";
8
- import { acceptRequest, declineRequest, getIdentity, listConnections, listRequests, PlatformApiError, PlatformNetworkError, requestConnection, updateProfile, } from "./platform-client.js";
9
+ import { acceptRequest, getIdentity, ignoreRequest, isPackagedTombstone, listConnections, listRequests, PlatformApiError, PlatformNetworkError, requestConnection, updateProfile, withdrawRequest, } from "./platform-client.js";
9
10
  import { isServicesRetainedReplyContext } from "./services-retained-reply-context.js";
10
11
  import { getCurrentTurnChannelId, getCurrentTurnIsOwnerNonConsuming, getCurrentTurnOwnerSignal, } from "./turn-context.js";
11
12
  import { getLatestPublishedVersion, getPluginVersion, getUpdateInfo, } from "./update-check.js";
12
13
  const PROFILE_FIELDS = new Set(["name", "scope", "about", "audience"]);
13
14
  const GATEWAY_RESTART_CMD = "openclaw gateway restart";
15
+ const PACKAGED_TOOL_ROSTER = pluginManifest.contracts.tools;
16
+ const ROSTER_HEADER = "This version ships these tools:";
17
+ function withRoster(text) {
18
+ return `${text}\n\n${ROSTER_HEADER}\n${PACKAGED_TOOL_ROSTER.join(", ")}`;
19
+ }
14
20
  function upgradeCmd(version) {
15
21
  return `openclaw plugins install @masons/agent-network@${version} --force`;
16
22
  }
@@ -47,11 +53,61 @@ function maybeAppendUpdateNotice(result) {
47
53
  function withUpdateNotice(fn) {
48
54
  return async (id, params) => maybeAppendUpdateNotice(await fn(id, params));
49
55
  }
50
- function formatConnectionResult(requestIds, status) {
51
- if (requestIds.length === 0) {
52
- return "Already connected or request already pending — no new request needed.";
56
+ function counterpartyName(c) {
57
+ if (isPackagedTombstone(c)) {
58
+ return c.handle ? `@${c.handle}` : (c.mstpAddress ?? c.id);
59
+ }
60
+ return c.displayName ?? (c.handle ? `@${c.handle}` : c.mstpAddress);
61
+ }
62
+ function suppressionKey(item) {
63
+ const c = item.counterparty;
64
+ if (isPackagedTombstone(c))
65
+ return `tombstone:${c.mstpAddress ?? c.id}`;
66
+ const address = c.mstpAddress;
67
+ if (!address || address.endsWith("/"))
68
+ return null;
69
+ return address;
70
+ }
71
+ function collapseToCounterpartyGrain(items) {
72
+ const seen = new Set();
73
+ const rows = [];
74
+ for (const item of items) {
75
+ if (item.shadowed)
76
+ continue;
77
+ const key = suppressionKey(item);
78
+ if (key !== null) {
79
+ if (seen.has(key))
80
+ continue;
81
+ seen.add(key);
82
+ }
83
+ rows.push(item);
84
+ }
85
+ return rows;
86
+ }
87
+ function directionLabel(status) {
88
+ if (status === "awaiting-your-decision")
89
+ return "incoming";
90
+ if (status === "awaiting-counterparty")
91
+ return "outgoing";
92
+ return "history";
93
+ }
94
+ function formatAlreadyResolved(status, act) {
95
+ const nothing = `Nothing to ${act} —`;
96
+ if (status == null) {
97
+ return `${nothing} this request is no longer pending.`;
53
98
  }
54
- return `Connection request sent. Status: ${status}. The other agent's owner will be notified.`;
99
+ const terminal = status === "accepted" || status === "ignored" || status === "withdrawn";
100
+ return terminal
101
+ ? `${nothing} this request is already resolved as "${status}".`
102
+ : `${nothing} this request is not in a state you can ${act} (status: "${status}").`;
103
+ }
104
+ function formatVerb404(err, verbLabel) {
105
+ return err.code === "not_found"
106
+ ? "Request not found, or it is not addressable from your perspective. Use masons_list_requests to see current requests."
107
+ : `This Services deployment does not serve the ${verbLabel} endpoint. Nothing changed — the request is unchanged.`;
108
+ }
109
+ function quoteRejected(value) {
110
+ return `"${typeof value === "string" ? value : JSON.stringify(value)}"`;
55
111
  }
56
112
  const IDENTITY_NO_FALLBACK = "This is not an answer — do not assume any locally-cached handle or name is this Runtime's identity.";
57
113
  function formatIdentityError(err) {
@@ -63,7 +119,7 @@ function formatIdentityError(err) {
63
119
  return `Identity: unavailable — the identity endpoint is not available at this host (HTTP 404). ${IDENTITY_NO_FALLBACK}`;
64
120
  case "credential-refused":
65
121
  return err.status === 401
66
- ? "Identity: the runtime key is not valid — the network rejected it. The owner can re-authenticate via masons_setup."
122
+ ? "Identity: the runtime key is not valid — the network rejected it. The owner can re-authenticate via masons_link."
67
123
  : "Identity: refused (HTTP 403) — this runtime key is not permitted to read this Runtime's identity. That is a credential-class refusal, not a transient outage: retrying will not change it.";
68
124
  default:
69
125
  return `Identity: unavailable right now (HTTP ${err.status}: ${err.code}). ${IDENTITY_NO_FALLBACK}`;
@@ -101,7 +157,7 @@ function formatReadinessSection() {
101
157
  }
102
158
  async function resolveIdentitySection() {
103
159
  if (!hasApiKey()) {
104
- return "Identity: no runtime key configured — cannot ask the network who this Runtime is. Run masons_setup first.";
160
+ return "Identity: no runtime key configured — cannot ask the network who this Runtime is. Run masons_link first.";
105
161
  }
106
162
  const cfg = requirePlatformConfig();
107
163
  const apiKey = requireApiKey();
@@ -132,10 +188,12 @@ function resolveSetupAuthority() {
132
188
  function getSetupOptionsFromToolContext(ctx) {
133
189
  const defaults = getDefaultChannelSetupOptions();
134
190
  const cfg = extractSetupConfig(ctx);
191
+ const configuredIdpBaseUrl = stringValue(cfg.idpBaseUrl);
135
192
  return {
136
193
  apiHost: stringValue(cfg.apiHost) ?? defaults.apiHost,
137
194
  connectorUrl: stringValue(cfg.connectorUrl),
138
- idpBaseUrl: stringValue(cfg.idpBaseUrl) ?? defaults.idpBaseUrl,
195
+ idpBaseUrl: configuredIdpBaseUrl ?? defaults.idpBaseUrl,
196
+ idpBaseUrlIsDefault: configuredIdpBaseUrl === undefined,
139
197
  };
140
198
  }
141
199
  function extractSetupConfig(ctx) {
@@ -164,7 +222,7 @@ function toRecord(value) {
164
222
  function stringValue(value) {
165
223
  return typeof value === "string" && value.length > 0 ? value : undefined;
166
224
  }
167
- function terminalSetupInstructions() {
225
+ export function terminalSetupInstructions() {
168
226
  return [
169
227
  "Run this in the terminal where OpenClaw is installed:",
170
228
  "",
@@ -177,24 +235,25 @@ function terminalSetupInstructions() {
177
235
  " 4. Return to the terminal after the encrypted handoff completes.",
178
236
  " 5. Persist credentials to openclaw.json. OpenClaw reloads the channel.",
179
237
  "",
180
- "Note: if you re-run this with another agent on this machine, this OpenClaw install receives",
181
- "its own runtime key. Re-running login here updates this OpenClaw install's credentials.",
238
+ "Note: on an already-linked install this is a Relink choosing another Node here changes",
239
+ "which Node this OpenClaw install drives. Each install holds its own runtime key, so",
240
+ "relinking updates this OpenClaw install's credentials.",
182
241
  ].join("\n");
183
242
  }
184
- function ownerRefusalText(reason) {
243
+ export function ownerRefusalText(reason) {
185
244
  if (reason === "known-non-owner") {
186
245
  return [
187
- "Setup is owner-only. You are reaching this agent through the MASONS",
188
- "network as a visitor or as a peer agent. Setup re-binds the OpenClaw",
246
+ "Link is owner-only. You are reaching this agent through the MASONS",
247
+ "network as a visitor or as a peer agent. Link binds the OpenClaw",
189
248
  "runtime to a specific agent identity, so it must be initiated by the",
190
249
  "agent's runtime owner from a channel their OpenClaw is configured for.",
191
250
  ].join("\n");
192
251
  }
193
252
  return [
194
- "Setup is owner-only on the MASONS network channel. I could not verify",
253
+ "Link is owner-only on the MASONS network channel. I could not verify",
195
254
  "that you are this agent's owner from the message received.",
196
255
  "",
197
- "If you are the agent's runtime owner, please initiate setup from a",
256
+ "If you are the agent's runtime owner, please initiate Link from a",
198
257
  "channel your OpenClaw runtime is configured for (e.g., Lark, Telegram,",
199
258
  "desktop). The terminal command works as a last resort if no other channel",
200
259
  "is reachable:",
@@ -202,36 +261,36 @@ function ownerRefusalText(reason) {
202
261
  " openclaw channels login --channel agent-network",
203
262
  ].join("\n");
204
263
  }
205
- function formatChannelSetupResult(result) {
264
+ export function formatChannelSetupResult(result) {
206
265
  if (result.status === "pending" && result.handoffUrl) {
207
266
  return [
208
- "MASONS setup link:",
267
+ "Link URL:",
209
268
  "",
210
269
  result.handoffUrl,
211
270
  "",
212
271
  "Open this link in your browser, sign in, choose an agent, then come back here.",
213
272
  "After browser sign-in and agent selection, OpenClaw will receive the encrypted handoff, persist the runtime key locally, and reload the channel.",
214
- "This link is sensitive setup material; only the OpenClaw owner should open it.",
273
+ "This URL is sensitive Link material; only the OpenClaw owner should open it.",
215
274
  ].join("\n");
216
275
  }
217
276
  if (result.status === "completed") {
218
277
  return [
219
- result.message ?? "Agent Network setup completed.",
278
+ result.message ?? "Agent Network Link completed.",
220
279
  "",
221
280
  "HTTP tools may work before the Gateway WebSocket is connected.",
222
- 'If the gateway tool is available, call it with action "restart" and reason "Activate Agent Network after setup". If not, ask the user to run `openclaw gateway restart`.',
281
+ 'If the gateway tool is available, call it with action "restart" and reason "Activate Agent Network after Link". If not, ask the user to run `openclaw gateway restart`.',
223
282
  ].join("\n");
224
283
  }
225
284
  if (result.status === "expired") {
226
285
  return [
227
- result.message ?? "The previous setup link expired.",
228
- "Ask again to generate a fresh owner-only setup link, or use the terminal fallback:",
286
+ result.message ?? "The previous Link URL expired.",
287
+ "Ask again to generate a fresh owner-only Link URL, or use the terminal fallback:",
229
288
  "",
230
289
  terminalSetupInstructions(),
231
290
  ].join("\n");
232
291
  }
233
292
  return [
234
- result.message ?? "Agent Network setup failed.",
293
+ result.message ?? "Agent Network Link failed.",
235
294
  "Ask again to retry, or use the terminal fallback:",
236
295
  "",
237
296
  terminalSetupInstructions(),
@@ -239,16 +298,23 @@ function formatChannelSetupResult(result) {
239
298
  }
240
299
  export function registerTools(api) {
241
300
  api.registerTool((ctx = {}) => ({
242
- name: "masons_setup",
301
+ name: "masons_link",
243
302
  description: [
244
- "Start or inspect Agent Network setup.",
245
- "Use when the user asks to set up Agent Network, when other Agent",
246
- "Network tools fail with a 'no credentials' error, or when the runtime",
247
- "owner wants to switch which MASONS agent OpenClaw is driving.",
303
+ "Link this OpenClaw Runtime to a MASONS Agent Node, or inspect a Link",
304
+ "already in progress.",
305
+ "Use when the user asks to link (or set up) Agent Network, when other",
306
+ "Agent Network tools fail with a 'no credentials' error, or when the",
307
+ "runtime owner wants a different MASONS agent driven by OpenClaw.",
308
+ "On a Runtime that is already linked, present this to the user as",
309
+ "Relink / Change Node — the same capability, not a second one.",
310
+ "",
311
+ "Not to be confused with masons_link_identity: this tool binds a",
312
+ "Runtime to a Node. masons_link_identity aliases your owner's identity",
313
+ "on one channel to their canonical owner identity.",
248
314
  "",
249
315
  "Pass `invitedBy` if the user came from another agent's profile page",
250
316
  "(handle extracted from the URL); the plugin will record it and propose",
251
- "a connection request after setup completes.",
317
+ "a connection request once the Link completes.",
252
318
  "",
253
319
  "This tool returns a browser handoff URL on the channel where you are",
254
320
  "running. The user opens it, signs in, picks an agent, and the encrypted",
@@ -257,13 +323,13 @@ export function registerTools(api) {
257
323
  "",
258
324
  "If the user is reaching this agent through the MASONS network as a",
259
325
  "visitor (not the agent's owner), this tool refuses with an explanation",
260
- "— setup is owner-only because it re-binds the OpenClaw runtime to a",
326
+ "— Link is owner-only because it binds the OpenClaw runtime to a",
261
327
  "specific agent identity.",
262
328
  ].join("\n"),
263
329
  parameters: Type.Object({
264
330
  invitedBy: Type.Optional(Type.String({
265
331
  description: "Handle of the agent whose profile page the user came from " +
266
- "(e.g., 'alice' from 'masons.ai/alice'). When setup completes, " +
332
+ "(e.g., 'alice' from 'masons.ai/alice'). When the Link completes, " +
267
333
  "the plugin records this as a pending connection target so the " +
268
334
  "agent can propose a connection request automatically.",
269
335
  })),
@@ -289,14 +355,12 @@ export function registerTools(api) {
289
355
  catch (err) {
290
356
  result = {
291
357
  status: "failed",
292
- message: err instanceof Error
293
- ? err.message
294
- : "Agent Network setup failed.",
358
+ message: err instanceof Error ? err.message : "Agent Network Link failed.",
295
359
  };
296
360
  }
297
361
  return textResult(formatChannelSetupResult(result));
298
362
  },
299
- }), { name: "masons_setup" });
363
+ }), { name: "masons_link" });
300
364
  api.registerTool({
301
365
  name: "masons_update_profile",
302
366
  description: [
@@ -417,15 +481,20 @@ export function registerTools(api) {
417
481
  const targetHandle = params.targetHandle;
418
482
  let result;
419
483
  try {
420
- result = await requestConnection(cfg, apiKey, {
421
- targetHandle,
422
- variants: ["distribute", "receive"],
423
- });
484
+ result = await requestConnection(cfg, apiKey, targetHandle);
424
485
  }
425
486
  catch (err) {
426
487
  if (err instanceof PlatformApiError) {
488
+ if (err.status === 409 && err.code === "already_connected") {
489
+ if (getPendingTarget()) {
490
+ await clearTargetHandle();
491
+ }
492
+ return textResult(`Already connected with @${targetHandle} — no request needed. Use masons_send_message to reach them.`);
493
+ }
427
494
  if (err.status === 404) {
428
- return textResult(`Agent @${targetHandle} not found. Check the handle and try again.`);
495
+ return textResult(err.code === "target_not_found"
496
+ ? `Agent @${targetHandle} not found. Check the handle and try again.`
497
+ : "This Services deployment does not serve the connection-request endpoint. Nothing was sent.");
429
498
  }
430
499
  if (err.status === 401) {
431
500
  return textResult("Authentication failed. The runtime key may be invalid. Ask the user to run `openclaw channels login --channel agent-network`.");
@@ -440,35 +509,73 @@ export function registerTools(api) {
440
509
  if (getPendingTarget()) {
441
510
  await clearTargetHandle();
442
511
  }
443
- return textResult(formatConnectionResult(result.requestIds, result.status));
512
+ const request = result.request;
513
+ if (request === null) {
514
+ return textResult("The network accepted the call but returned no request object — " +
515
+ "the outcome is unknown; use masons_list_requests to check.");
516
+ }
517
+ const who = counterpartyName(request.counterparty);
518
+ if (request.status === "accepted") {
519
+ return textResult(`Connection established with ${who}. They had already requested a connection with you, so sending yours completed it — nothing is pending. You can now send messages using masons_send_message.`);
520
+ }
521
+ if (!result.created) {
522
+ return textResult(`A connection request to ${who} is already pending — no new request was sent.`);
523
+ }
524
+ return textResult(`Connection request sent to ${who}. Status: ${request.status}. The other agent's owner will be notified.`);
444
525
  }),
445
526
  });
446
527
  api.registerTool({
447
528
  name: "masons_list_requests",
448
- description: "List connection requests — incoming from other agents, outgoing ones you sent, or all.",
529
+ description: "List connection requests — one entry per counterparty, keyed by the " +
530
+ "request ID every request tool takes. Shows what is awaiting your " +
531
+ "decision, what is awaiting theirs, and resolved history. Each entry " +
532
+ "is that counterparty's latest state; pass counterparty to see every " +
533
+ "row of one pair instead.",
449
534
  parameters: Type.Object({
450
- status: Type.Optional(Type.String({
451
- description: 'Filter by status: "pending" (default), "accepted", "declined", "withdrawn", or "all". ' +
452
- 'Note: "declined" only ever matches requests YOU declined (incoming). ' +
453
- "A recipient's refusal of your outgoing request is never disclosed — " +
454
- 'outgoing requests stay "pending" until accepted or withdrawn, so ' +
455
- 'filtering outgoing by "declined" always returns empty.',
535
+ direction: Type.Optional(Type.Union([
536
+ Type.Literal("all"),
537
+ Type.Literal("incoming"),
538
+ Type.Literal("outgoing"),
539
+ ], {
540
+ description: '"all" (default) both; "incoming" awaiting YOUR decision; ' +
541
+ '"outgoing" — awaiting the counterparty. Direction is derived ' +
542
+ "from each request's status; it is not a state you can filter " +
543
+ "the network by.",
456
544
  })),
457
- direction: Type.Optional(Type.String({
458
- description: '"incoming" (default) requests from others; "outgoing" requests you sent; "all" — both directions',
545
+ include_history: Type.Optional(Type.Boolean({
546
+ description: "Include resolved requests (accepted / ignored / withdrawn). " +
547
+ "Default true — pass false for pending requests only.",
548
+ })),
549
+ counterparty: Type.Optional(Type.String({
550
+ description: "Handle of one agent — show the full per-pair history with " +
551
+ "@handle — bypasses the one-row-per-counterparty collapse.",
459
552
  })),
460
553
  }),
461
554
  execute: withUpdateNotice(async (_id, params) => {
462
555
  const cfg = requirePlatformConfig();
463
556
  const apiKey = requireApiKey();
464
- const status = params.status ?? "pending";
465
- const direction = params.direction ?? "incoming";
557
+ const rawDirection = params.direction;
558
+ if (rawDirection !== undefined &&
559
+ rawDirection !== "all" &&
560
+ rawDirection !== "incoming" &&
561
+ rawDirection !== "outgoing") {
562
+ return textResult(`${quoteRejected(rawDirection)} is not a direction — use "all", "incoming", or "outgoing". Nothing was read.`);
563
+ }
564
+ const rawHistory = params.include_history;
565
+ if (rawHistory !== undefined && typeof rawHistory !== "boolean") {
566
+ return textResult(`${quoteRejected(rawHistory)} is not a boolean — include_history takes true or false. Nothing was read.`);
567
+ }
568
+ const direction = rawDirection ?? "all";
569
+ const includeHistory = rawHistory ?? true;
570
+ const rawCounterparty = params.counterparty;
571
+ const counterparty = typeof rawCounterparty === "string"
572
+ ? rawCounterparty.replace(/^@/, "").trim() || undefined
573
+ : undefined;
466
574
  let result;
467
575
  try {
468
576
  result = await listRequests(cfg, apiKey, {
469
- status,
470
- direction,
471
577
  limit: 100,
578
+ ...(counterparty ? { counterparty } : {}),
472
579
  });
473
580
  }
474
581
  catch (err) {
@@ -476,6 +583,9 @@ export function registerTools(api) {
476
583
  if (err.status === 401) {
477
584
  return textResult("Authentication failed. The runtime key may be invalid. Ask the user to run `openclaw channels login --channel agent-network`.");
478
585
  }
586
+ if (err.status === 404) {
587
+ return textResult("This Services deployment does not serve the packaged connection-request endpoint. Nothing was read.");
588
+ }
479
589
  return textResult(`Failed to list requests: ${err.message}`);
480
590
  }
481
591
  if (err instanceof PlatformNetworkError) {
@@ -483,53 +593,57 @@ export function registerTools(api) {
483
593
  }
484
594
  throw err;
485
595
  }
486
- if (result.total === 0) {
487
- const dirLabel = direction === "all"
488
- ? ""
489
- : direction === "outgoing"
490
- ? "outgoing "
491
- : "incoming ";
492
- return textResult(`No ${dirLabel}${status === "all" ? "" : `${status} `}connection requests.`);
493
- }
494
- const lines = result.items.map((item) => {
495
- const dir = item.direction ?? direction;
496
- const node = dir === "outgoing" ? item.toNode : item.fromNode;
497
- const otherName = node?.displayName || node?.handle || "unknown";
498
- const otherHandle = node?.handle || "unknown";
499
- const kindLabel = formatKindLabel(node?.kind);
500
- const dirArrow = dir === "outgoing" ? "→" : "←";
501
- const parts = [
502
- `• ${dirArrow} ${kindLabel}${otherName} (@${otherHandle})`,
503
- ` ID: ${item.id} | Variant: ${item.variant} | Status: ${item.status}`,
504
- ];
505
- if (item.matchmaking?.title) {
506
- parts.push(` Why: ${item.matchmaking.title}`);
507
- }
508
- if (item.matchmaking?.benefit) {
509
- parts.push(` Benefit: ${item.matchmaking.benefit}`);
510
- }
511
- if (item.matchmaking?.scenario) {
512
- parts.push(` Context: ${item.matchmaking.scenario}`);
513
- }
514
- return parts.join("\n");
596
+ const filtered = result.items.filter((item) => {
597
+ const label = directionLabel(item.status);
598
+ if (label === "history")
599
+ return includeHistory;
600
+ return direction === "all" || direction === label;
515
601
  });
516
- const dirLabel = direction === "all"
517
- ? ""
518
- : direction === "outgoing"
519
- ? "outgoing "
520
- : "incoming ";
521
- const suffix = direction === "incoming" || direction === "all"
522
- ? "\n\nUse masons_accept_request or masons_decline_request with the ID to act on incoming requests."
602
+ const rows = counterparty
603
+ ? filtered
604
+ : collapseToCounterpartyGrain(filtered);
605
+ const scope = direction !== "all" &&
606
+ rows.every((item) => directionLabel(item.status) === direction)
607
+ ? `${direction} `
523
608
  : "";
524
- return textResult(`${result.total} ${dirLabel}${status === "all" ? "" : `${status} `}request(s):\n\n${lines.join("\n\n")}${suffix}`);
609
+ const withWho = counterparty ? ` with @${counterparty}` : "";
610
+ const moreLine = result.nextCursor !== null
611
+ ? "\n\nMore requests exist beyond this page — this is the most recent page, not the whole history."
612
+ : "";
613
+ if (rows.length === 0) {
614
+ return textResult(`No ${scope}${includeHistory ? "" : "pending "}connection requests${withWho}.${moreLine}`);
615
+ }
616
+ const lines = rows.map((item) => {
617
+ const node = item.counterparty;
618
+ const label = directionLabel(item.status);
619
+ const arrow = label === "incoming" ? "←" : label === "outgoing" ? "→" : "·";
620
+ const kindLabel = formatKindLabel(node.kind);
621
+ const handle = node.handle ? `@${node.handle}` : "no handle";
622
+ const shadowMark = item.shadowed ? " | shadowed" : "";
623
+ return [
624
+ `• ${arrow} ${kindLabel}${counterpartyName(node)} (${handle})`,
625
+ ` ID: ${item.id} | Status: ${item.status} | ${label}${shadowMark}`,
626
+ ].join("\n");
627
+ });
628
+ const hints = [];
629
+ if (rows.some((item) => item.status === "awaiting-your-decision")) {
630
+ hints.push("Use masons_accept_request or masons_ignore_request with the ID to act on requests awaiting your decision.");
631
+ }
632
+ if (rows.some((item) => item.status === "awaiting-counterparty")) {
633
+ hints.push("Use masons_withdraw_request with the ID to take back a request you sent.");
634
+ }
635
+ const suffix = hints.length > 0 ? `\n\n${hints.join("\n")}` : "";
636
+ return textResult(`${rows.length} ${scope}request(s)${withWho}:\n\n${lines.join("\n\n")}${suffix}${moreLine}`);
525
637
  }),
526
638
  });
527
639
  api.registerTool({
528
640
  name: "masons_accept_request",
529
- description: "Accept an incoming connection request from another agent.",
641
+ description: "Accept a connection request awaiting your decision. Takes the ID " +
642
+ "masons_list_requests shows for that request; the whole request is " +
643
+ "resolved in one act.",
530
644
  parameters: Type.Object({
531
645
  requestId: Type.String({
532
- description: "ID of the connection request to accept",
646
+ description: "ID of the connection request to accept, as shown by masons_list_requests",
533
647
  }),
534
648
  }),
535
649
  execute: withUpdateNotice(async (_id, params) => {
@@ -543,15 +657,57 @@ export function registerTools(api) {
543
657
  const requestId = params.requestId;
544
658
  try {
545
659
  const result = await acceptRequest(cfg, apiKey, requestId);
546
- const node = result.counterparty;
547
- const who = node?.displayName || node?.handle || "that Node";
660
+ if (result.outcome === "already_resolved") {
661
+ return textResult(formatAlreadyResolved(result.status, "accept"));
662
+ }
663
+ const node = result.request?.counterparty;
664
+ const who = node ? counterpartyName(node) : "that Node";
548
665
  const handle = node?.handle ? ` (@${node.handle})` : "";
549
666
  const kindLabel = formatKindLabel(node?.kind);
667
+ if (result.idempotent) {
668
+ return textResult(`Already connected with ${kindLabel}${who}${handle} — you accepted this request earlier and nothing changed now. You can send messages using masons_send_message.`);
669
+ }
550
670
  return textResult(`Connected with ${kindLabel}${who}${handle}! You can now send messages using masons_send_message.`);
551
671
  }
552
672
  catch (err) {
553
673
  if (err instanceof PlatformApiError && err.status === 404) {
554
- return textResult("Request not found or already processed. Use masons_list_requests to see current requests.");
674
+ return textResult(formatVerb404(err, "accept"));
675
+ }
676
+ if (err instanceof PlatformNetworkError) {
677
+ return textResult(networkErrorText(err));
678
+ }
679
+ throw err;
680
+ }
681
+ }),
682
+ });
683
+ api.registerTool({
684
+ name: "masons_ignore_request",
685
+ description: "Set aside a connection request awaiting your decision. " +
686
+ "This resolves the whole request on your side, silently: the sender is " +
687
+ "not notified and sees no change — there is no refusal they can observe.",
688
+ parameters: Type.Object({
689
+ requestId: Type.String({
690
+ description: "ID of the connection request to set aside, as shown by masons_list_requests",
691
+ }),
692
+ }),
693
+ execute: withUpdateNotice(async (_id, params) => {
694
+ const cfg = requirePlatformConfig();
695
+ const apiKey = requireApiKey();
696
+ const requestId = params.requestId;
697
+ try {
698
+ const result = await ignoreRequest(cfg, apiKey, requestId);
699
+ if (result.outcome === "already_resolved") {
700
+ return textResult(formatAlreadyResolved(result.status, "set aside"));
701
+ }
702
+ if (result.idempotent) {
703
+ return textResult("This request was already set aside earlier; nothing changed.");
704
+ }
705
+ return textResult("Request set aside. The whole request is resolved on your side. " +
706
+ "The sender is not notified and sees no change.");
707
+ }
708
+ catch (err) {
709
+ if (err instanceof PlatformApiError && err.status === 404) {
710
+ return textResult(formatVerb404(err, "ignore"));
555
711
  }
556
712
  if (err instanceof PlatformNetworkError) {
557
713
  return textResult(networkErrorText(err));
@@ -561,11 +717,14 @@ export function registerTools(api) {
561
717
  }),
562
718
  });
563
719
  api.registerTool({
564
- name: "masons_decline_request",
565
- description: "Decline an incoming connection request from another agent.",
720
+ name: "masons_withdraw_request",
721
+ description: "Withdraw a connection request YOU sent — one awaiting the counterparty. " +
722
+ "(masons_accept_request and masons_ignore_request act on requests " +
723
+ "awaiting your decision.) Takes the ID masons_list_requests shows for " +
724
+ "that request; the whole request is resolved in one act.",
566
725
  parameters: Type.Object({
567
726
  requestId: Type.String({
568
- description: "ID of the connection request to decline",
727
+ description: "ID of the outgoing connection request to withdraw, as shown by masons_list_requests",
569
728
  }),
570
729
  }),
571
730
  execute: withUpdateNotice(async (_id, params) => {
@@ -573,12 +732,21 @@ export function registerTools(api) {
573
732
  const apiKey = requireApiKey();
574
733
  const requestId = params.requestId;
575
734
  try {
576
- await declineRequest(cfg, apiKey, requestId);
577
- return textResult("Request declined.");
735
+ const result = await withdrawRequest(cfg, apiKey, requestId);
736
+ if (result.outcome === "already_resolved") {
737
+ return textResult(formatAlreadyResolved(result.status, "withdraw"));
738
+ }
739
+ if (result.idempotent) {
740
+ return textResult("This request was already withdrawn earlier; nothing changed.");
741
+ }
742
+ return textResult("Request withdrawn. A new request to the same agent can be sent later. " +
743
+ "If the recipient had not acted on it yet, the pending request " +
744
+ "disappears from their view; if they had already set it aside, " +
745
+ "nothing changes for them.");
578
746
  }
579
747
  catch (err) {
580
748
  if (err instanceof PlatformApiError && err.status === 404) {
581
- return textResult("Request not found or already processed. Use masons_list_requests to see current requests.");
749
+ return textResult(formatVerb404(err, "withdraw"));
582
750
  }
583
751
  if (err instanceof PlatformNetworkError) {
584
752
  return textResult(networkErrorText(err));
@@ -727,36 +895,50 @@ export function registerTools(api) {
727
895
  execute: async () => {
728
896
  const pendingNotes = ownerNotesQueue.size();
729
897
  if (pendingNotes > 0) {
730
- return textResult(`You have ${pendingNotes} pending note(s) for your owner that haven't been delivered yet. ` +
731
- `Report them to your owner first, then call masons_upgrade again.`);
898
+ return textResult(withRoster(`You have ${pendingNotes} pending note(s) for your owner that haven't been delivered yet. ` +
899
+ `Report them to your owner first, then call masons_upgrade again.`));
732
900
  }
733
901
  const currentVersion = getPluginVersion();
734
902
  const latestVersion = await getLatestPublishedVersion();
735
903
  if (!latestVersion) {
736
- return textResult("Could not check for updates (network issue). Try again later.");
904
+ return textResult(withRoster("Could not check for updates (network issue). Try again later."));
737
905
  }
738
906
  if (latestVersion === currentVersion) {
739
- return textResult(`You're on the latest version (v${currentVersion}). No update needed.`);
907
+ return textResult(withRoster(`You're on the latest version (v${currentVersion}). No update needed.`));
740
908
  }
741
909
  if (!SEMVER_RE.test(latestVersion)) {
742
- return textResult(`Registry returned an unexpected version format: "${latestVersion}". Try again later.`);
910
+ return textResult(withRoster(`Registry returned an unexpected version format: "${latestVersion}". Try again later.`));
743
911
  }
744
912
  const cmd = upgradeCmd(latestVersion);
745
- return textResult([
913
+ return textResult(withRoster([
746
914
  `Update available: v${currentVersion} → v${latestVersion}`,
747
915
  "",
748
916
  "Run this command using the exec tool:",
749
917
  cmd,
750
918
  "",
919
+ "After the install finishes and BEFORE restarting: read " +
920
+ "`contracts.tools` from the newly installed package's " +
921
+ "`openclaw.plugin.json` (its installed package directory under " +
922
+ "`~/.openclaw/`) and compare it with the roster below. Tell your " +
923
+ "owner about every tool the new version adds. If their " +
924
+ "`openclaw.json` allows plugin tools by individual name under " +
925
+ "`tools.alsoAllow`, the new names must be added there — or the " +
926
+ "entry replaced with `group:plugins` — or the new tools will be " +
927
+ "missing after the restart with no error reported.",
928
+ "",
751
929
  "Then restart the gateway using the gateway tool.",
752
- "If you cannot run commands, tell the user to run in Terminal:",
930
+ "If you cannot run commands, tell the user to run in Terminal — and " +
931
+ "compare the new package's `openclaw.plugin.json` with the roster " +
932
+ "below before that restart:",
753
933
  `${cmd} && ${GATEWAY_RESTART_CMD}`,
754
- ].join("\n"));
934
+ ].join("\n")));
755
935
  },
756
936
  });
757
937
  api.registerTool({
758
938
  name: "masons_link_identity",
759
939
  description: "Link your owner's identity on the current channel to their Passport identity. " +
940
+ "Not to be confused with masons_link: that tool binds this Runtime to " +
941
+ "an Agent Node. This one aliases owner identities across channels. " +
760
942
  "Provide only the current channel's entry in 'channel:peerId' format " +
761
943
  "(e.g., 'telegram:5099353300', 'feishu:ou_abc123'). " +
762
944
  "The canonical name and Passport entry are added automatically. " +