@marshell/cli 0.6.0 → 0.6.1
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/dist/bridge.js +204 -14
- package/dist/cli.js +33 -6
- package/dist/network.js +35 -1
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -4,23 +4,144 @@ exports.tryFastReply = tryFastReply;
|
|
|
4
4
|
exports.handleInbound = handleInbound;
|
|
5
5
|
exports.runBridge = runBridge;
|
|
6
6
|
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
7
9
|
const network_1 = require("./network");
|
|
8
10
|
const DEFAULT_REPLY_TIMEOUT_MS = 120_000;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
const GREETING_COOLDOWN_MS = 120_000;
|
|
12
|
+
const ECHO_WINDOW_MS = 60_000;
|
|
13
|
+
const peers = new Map();
|
|
14
|
+
function peerState(name) {
|
|
15
|
+
const key = name.toLowerCase();
|
|
16
|
+
let state = peers.get(key);
|
|
17
|
+
if (!state) {
|
|
18
|
+
state = { lastGreetingAt: 0, recentOutbound: [], inflight: false };
|
|
19
|
+
peers.set(key, state);
|
|
20
|
+
}
|
|
21
|
+
return state;
|
|
22
|
+
}
|
|
23
|
+
function rememberOutbound(peer, text) {
|
|
24
|
+
const state = peerState(peer);
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
state.recentOutbound.push({ text: text.trim().toLowerCase(), at: now });
|
|
27
|
+
state.recentOutbound = state.recentOutbound.filter((item) => now - item.at < ECHO_WINDOW_MS);
|
|
28
|
+
}
|
|
29
|
+
function isEcho(peer, text) {
|
|
30
|
+
const normalized = text.trim().toLowerCase();
|
|
31
|
+
const state = peerState(peer);
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
return state.recentOutbound.some((item) => {
|
|
34
|
+
if (now - item.at >= ECHO_WINDOW_MS)
|
|
35
|
+
return false;
|
|
36
|
+
if (item.text === normalized)
|
|
37
|
+
return true;
|
|
38
|
+
// Peer quoting/acking our last reply ("got it — acknowledged").
|
|
39
|
+
if (normalized.includes(item.text) || item.text.includes(normalized)) {
|
|
40
|
+
return item.text.length >= 4 && normalized.length <= item.text.length + 40;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function isGreeting(text) {
|
|
46
|
+
return /^(hi|hello|hey|yo|sup|picun)[!.?\s]*$/i.test(text.trim());
|
|
47
|
+
}
|
|
48
|
+
/** Short acks / reactions — deliver only, never auto-reply (stops chat spam). */
|
|
49
|
+
function isAckOnly(text) {
|
|
50
|
+
const t = text.trim();
|
|
51
|
+
if (!t)
|
|
52
|
+
return true;
|
|
53
|
+
if (/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\s\p{P}]+$/u.test(t)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (/^(got it|ok|okay|k|kk|thanks|thank you|thx|ty|cool|nice|great|ack|acknowledged|roger|copy|noted|np|no problem|sounds good|sg|lgtm|sure|yep|yeah|yes|no|nah|👍|✅)[.!*]*$/i.test(t)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// Very short non-questions are almost always reactions.
|
|
60
|
+
if (t.length <= 16 && !/[?]/.test(t) && !/\bwho\b|\bwhat\b|\bhow\b/i.test(t)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/** Only spend LLM budget on real questions / tasks. */
|
|
66
|
+
function needsReply(text) {
|
|
67
|
+
const t = text.trim();
|
|
68
|
+
if (!t || isGreeting(t) || isAckOnly(t) || /^pong$/i.test(t)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (/[?]/.test(t))
|
|
72
|
+
return true;
|
|
73
|
+
if (/\b(who are you|who're you|what are you|and you are)\b/i.test(t)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (/^(who|what|where|when|why|how|can|could|would|please|tell|ask|explain|list|show|find|read|check|run|write|fix|deploy|summarize)\b/i.test(t)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
// Longer messages are likely real tasks.
|
|
80
|
+
return t.length >= 48;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Instant replies — no LLM. Never reply in a way that loops with another bridge.
|
|
84
|
+
* ping → pong (pong alone is ignored)
|
|
85
|
+
* greetings → one-shot per peer, non-greeting text
|
|
86
|
+
* echo:… → body
|
|
87
|
+
*/
|
|
88
|
+
function tryFastReply(msg, options) {
|
|
11
89
|
const text = msg.text.trim();
|
|
12
|
-
if (/^
|
|
90
|
+
if (/^ping$/i.test(text)) {
|
|
13
91
|
return "pong";
|
|
14
92
|
}
|
|
15
|
-
|
|
16
|
-
|
|
93
|
+
// Never auto-reply to "pong" — breaks ping/pong loops.
|
|
94
|
+
if (/^pong$/i.test(text)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (isGreeting(text)) {
|
|
98
|
+
if (options?.allowGreeting === false) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const state = peerState(msg.from);
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if (now - state.lastGreetingAt < GREETING_COOLDOWN_MS) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
state.lastGreetingAt = now;
|
|
107
|
+
// Reply must NOT match isGreeting / ping, or peer bridge will loop.
|
|
108
|
+
return `hey ${msg.from} — here. send a question anytime.`;
|
|
17
109
|
}
|
|
18
110
|
if (text.toLowerCase().startsWith("echo:")) {
|
|
19
111
|
const body = text.slice(5).trim();
|
|
20
112
|
return body || "(empty)";
|
|
21
113
|
}
|
|
114
|
+
// Identity questions — answer without spawning LLM (reliable + instant).
|
|
115
|
+
if (/\bwho are you\b|\bwho're you\b|\bwhat are you\b|\band you are\??\s*$/i.test(text)) {
|
|
116
|
+
return "I'm cursor — Marshell agent on this machine (codebase workspace).";
|
|
117
|
+
}
|
|
22
118
|
return null;
|
|
23
119
|
}
|
|
120
|
+
/** Resolve a CLI binary so Windows .cmd wrappers work under spawn(shell:false). */
|
|
121
|
+
function resolveRuntimeCommand(command) {
|
|
122
|
+
if (process.platform === "win32" && command === "agent") {
|
|
123
|
+
const base = (0, node_path_1.join)(process.env.LOCALAPPDATA ?? "", "cursor-agent");
|
|
124
|
+
const versionsDir = (0, node_path_1.join)(base, "versions");
|
|
125
|
+
if ((0, node_fs_1.existsSync)(versionsDir)) {
|
|
126
|
+
const versions = (0, node_fs_1.readdirSync)(versionsDir)
|
|
127
|
+
.filter((name) => /^\d{4}\./.test(name))
|
|
128
|
+
.sort()
|
|
129
|
+
.reverse();
|
|
130
|
+
for (const version of versions) {
|
|
131
|
+
const nodePath = (0, node_path_1.join)(versionsDir, version, "node.exe");
|
|
132
|
+
const entry = (0, node_path_1.join)(versionsDir, version, "index.js");
|
|
133
|
+
if ((0, node_fs_1.existsSync)(nodePath) && (0, node_fs_1.existsSync)(entry)) {
|
|
134
|
+
return { command: nodePath, argsPrefix: [entry] };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const agentCmd = (0, node_path_1.join)(base, "agent.cmd");
|
|
139
|
+
if ((0, node_fs_1.existsSync)(agentCmd)) {
|
|
140
|
+
return { command: agentCmd, argsPrefix: [] };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { command, argsPrefix: [] };
|
|
144
|
+
}
|
|
24
145
|
function killProcessTree(pid) {
|
|
25
146
|
if (!pid)
|
|
26
147
|
return;
|
|
@@ -96,13 +217,19 @@ function spawnCommand(command, args, options) {
|
|
|
96
217
|
}
|
|
97
218
|
async function generateRuntimeReply(msg, runtime, workspace, timeoutMs) {
|
|
98
219
|
const prompt = [
|
|
99
|
-
`Marshell agent
|
|
100
|
-
msg.
|
|
220
|
+
`You are Marshell agent listening on this machine.`,
|
|
221
|
+
`Peer agent "${msg.from}" sent you a message.`,
|
|
222
|
+
"Answer using the local workspace when relevant.",
|
|
223
|
+
"If they ask who you are, say you are the cursor agent on this Marshell subnet.",
|
|
224
|
+
"Reply with ONLY the answer text. No tools, no marshell CLI, no meta commentary.",
|
|
101
225
|
"",
|
|
102
|
-
"
|
|
226
|
+
"Message:",
|
|
227
|
+
msg.text,
|
|
103
228
|
].join("\n");
|
|
104
229
|
if (runtime === "cursor") {
|
|
105
|
-
const
|
|
230
|
+
const resolved = resolveRuntimeCommand("agent");
|
|
231
|
+
const result = await spawnCommand(resolved.command, [
|
|
232
|
+
...resolved.argsPrefix,
|
|
106
233
|
"-p",
|
|
107
234
|
"--trust",
|
|
108
235
|
"--mode",
|
|
@@ -171,6 +298,9 @@ function emitEvent(json, event) {
|
|
|
171
298
|
else if (kind === "reply") {
|
|
172
299
|
process.stdout.write(`[reply] to=${event.to} id=${event.id}\n`);
|
|
173
300
|
}
|
|
301
|
+
else if (kind === "skip") {
|
|
302
|
+
process.stdout.write(`[skip] from=${event.from} reason=${event.reason}\n`);
|
|
303
|
+
}
|
|
174
304
|
else if (kind === "error") {
|
|
175
305
|
process.stderr.write(`[bridge] ${event.message}\n`);
|
|
176
306
|
}
|
|
@@ -180,6 +310,7 @@ async function deliverReply(networkUrl, msg, reply, json) {
|
|
|
180
310
|
if (sent.kind !== "sent") {
|
|
181
311
|
throw new Error(sent.message);
|
|
182
312
|
}
|
|
313
|
+
rememberOutbound(msg.from, reply);
|
|
183
314
|
emitEvent(json, {
|
|
184
315
|
type: "reply",
|
|
185
316
|
to: msg.from,
|
|
@@ -189,6 +320,17 @@ async function deliverReply(networkUrl, msg, reply, json) {
|
|
|
189
320
|
});
|
|
190
321
|
}
|
|
191
322
|
async function processReplyAsync(networkUrl, msg, options) {
|
|
323
|
+
const state = peerState(msg.from);
|
|
324
|
+
if (state.inflight) {
|
|
325
|
+
emitEvent(options.json, {
|
|
326
|
+
type: "skip",
|
|
327
|
+
from: msg.from,
|
|
328
|
+
reason: "peer already has an inflight reply",
|
|
329
|
+
id: msg.id,
|
|
330
|
+
});
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
state.inflight = true;
|
|
192
334
|
try {
|
|
193
335
|
let reply = tryFastReply(msg);
|
|
194
336
|
if (!reply && options.hook) {
|
|
@@ -208,7 +350,16 @@ async function processReplyAsync(networkUrl, msg, options) {
|
|
|
208
350
|
catch (error) {
|
|
209
351
|
const message = error instanceof Error ? error.message : String(error);
|
|
210
352
|
emitEvent(options.json, { type: "error", message, in_reply_to: msg.id });
|
|
211
|
-
|
|
353
|
+
// Short failure notice so the peer is not left hanging (not a greeting/pong).
|
|
354
|
+
try {
|
|
355
|
+
await deliverReply(networkUrl, msg, `cursor runtime error: ${message}`, options.json);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// ignore secondary send failure
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
state.inflight = false;
|
|
212
363
|
}
|
|
213
364
|
}
|
|
214
365
|
async function handleInbound(networkUrl, msg, options) {
|
|
@@ -219,21 +370,52 @@ async function handleInbound(networkUrl, msg, options) {
|
|
|
219
370
|
text: msg.text,
|
|
220
371
|
created_at: msg.created_at,
|
|
221
372
|
});
|
|
373
|
+
// Always ack first so backlog never sticks.
|
|
374
|
+
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
375
|
+
// Drop echoes of our own recent outbound (hi↔hi loops).
|
|
376
|
+
if (isEcho(msg.from, msg.text)) {
|
|
377
|
+
emitEvent(options.json, {
|
|
378
|
+
type: "skip",
|
|
379
|
+
from: msg.from,
|
|
380
|
+
reason: "echo of our recent outbound",
|
|
381
|
+
id: msg.id,
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
222
385
|
const fast = tryFastReply(msg);
|
|
223
386
|
if (fast) {
|
|
224
387
|
await deliverReply(networkUrl, msg, fast, options.json);
|
|
225
|
-
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
// Greetings / acks / reactions: deliver only, no reply spam.
|
|
391
|
+
if (!needsReply(msg.text)) {
|
|
392
|
+
emitEvent(options.json, {
|
|
393
|
+
type: "skip",
|
|
394
|
+
from: msg.from,
|
|
395
|
+
reason: "no-reply (ack/greeting/trivial)",
|
|
396
|
+
id: msg.id,
|
|
397
|
+
});
|
|
226
398
|
return;
|
|
227
399
|
}
|
|
228
400
|
const willReply = Boolean(options.hook) ||
|
|
229
401
|
(options.autoReply && options.runtime !== "fast");
|
|
230
|
-
// Ack immediately so transport never blocks on slow LLM / hook.
|
|
231
|
-
await (0, network_1.ackMessages)(networkUrl, [msg.id]);
|
|
232
402
|
if (!willReply) {
|
|
233
403
|
return;
|
|
234
404
|
}
|
|
235
405
|
void processReplyAsync(networkUrl, msg, options);
|
|
236
406
|
}
|
|
407
|
+
async function drainBacklog(networkUrl, json) {
|
|
408
|
+
const result = await (0, network_1.fetchInbox)(networkUrl, { peek: true });
|
|
409
|
+
if (result.kind !== "ok" || result.messages.length === 0) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
await (0, network_1.ackMessages)(networkUrl, result.messages.map((m) => m.id));
|
|
413
|
+
emitEvent(json, {
|
|
414
|
+
type: "skip",
|
|
415
|
+
from: "*",
|
|
416
|
+
reason: `drained ${result.messages.length} backlog message(s) on startup`,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
237
419
|
async function runBridge(options) {
|
|
238
420
|
const label = options.agentName ?? "agent";
|
|
239
421
|
const mode = options.hook
|
|
@@ -244,6 +426,8 @@ async function runBridge(options) {
|
|
|
244
426
|
: `auto-reply (${options.runtime})`
|
|
245
427
|
: "deliver-only";
|
|
246
428
|
process.stdout.write(`Bridge listening as '${label}' (${mode}).\n`);
|
|
429
|
+
// Drop stale inbox so we don't replay an old hi storm.
|
|
430
|
+
await drainBacklog(options.networkUrl, options.json);
|
|
247
431
|
let stopped = false;
|
|
248
432
|
const cleanup = () => {
|
|
249
433
|
if (stopped)
|
|
@@ -264,7 +448,13 @@ async function runBridge(options) {
|
|
|
264
448
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
265
449
|
continue;
|
|
266
450
|
}
|
|
267
|
-
|
|
451
|
+
// Newest first — real questions beat greeting backlog.
|
|
452
|
+
const messages = [...result.messages].sort((a, b) => {
|
|
453
|
+
const ta = Date.parse(a.created_at) || 0;
|
|
454
|
+
const tb = Date.parse(b.created_at) || 0;
|
|
455
|
+
return tb - ta;
|
|
456
|
+
});
|
|
457
|
+
for (const msg of messages) {
|
|
268
458
|
await handleInbound(options.networkUrl, msg, options);
|
|
269
459
|
}
|
|
270
460
|
}
|
package/dist/cli.js
CHANGED
|
@@ -20,14 +20,13 @@ function printHelp() {
|
|
|
20
20
|
" marshell send --to <name> --text \"...\" [--json]",
|
|
21
21
|
" marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]",
|
|
22
22
|
" marshell inbox [--json] [--wait <seconds>] [--from <name>]",
|
|
23
|
-
" marshell
|
|
23
|
+
" marshell history [--with <name>] [--limit <n>] [--json]",
|
|
24
|
+
" marshell listen [--json] (deliver-only listener)",
|
|
24
25
|
" marshell --help",
|
|
25
26
|
"",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
" --auto-reply spawns LLM only for non-trivial messages (async, non-blocking).",
|
|
30
|
-
" Prefer: marshell ask --to <peer> --text \"...\" for request/response.",
|
|
27
|
+
"Middleware: send / inbox / history. Agents think; Marshell only delivers.",
|
|
28
|
+
" inbox = unread only (empty after read)",
|
|
29
|
+
" history = last 24h with a peer (use this to recall past chats)",
|
|
31
30
|
"",
|
|
32
31
|
"Environment:",
|
|
33
32
|
" MARSHELL_NETWORK_URL default: https://network.marshell.dev",
|
|
@@ -240,6 +239,30 @@ async function cmdAsk(args) {
|
|
|
240
239
|
}
|
|
241
240
|
printError(result.message);
|
|
242
241
|
}
|
|
242
|
+
async function cmdHistory(args) {
|
|
243
|
+
const json = hasFlag(args, "--json");
|
|
244
|
+
const withPeer = valueForFlag(args, "--with");
|
|
245
|
+
const limitRaw = valueForFlag(args, "--limit");
|
|
246
|
+
const limit = limitRaw ? Number(limitRaw) : 50;
|
|
247
|
+
const config = await (0, config_1.readConfig)();
|
|
248
|
+
const networkUrl = (0, config_1.getNetworkUrl)(config);
|
|
249
|
+
const result = await (0, network_1.fetchHistory)(networkUrl, { with: withPeer, limit });
|
|
250
|
+
if (result.kind === "error") {
|
|
251
|
+
printError(result.message);
|
|
252
|
+
}
|
|
253
|
+
if (json) {
|
|
254
|
+
printJson({ agent: result.agent, messages: result.messages });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (result.messages.length === 0) {
|
|
258
|
+
process.stdout.write("No messages in the last 24h.\n");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
for (const msg of [...result.messages].reverse()) {
|
|
262
|
+
const arrow = msg.direction === "out" ? "→" : "←";
|
|
263
|
+
process.stdout.write(`${msg.created_at} ${arrow} ${msg.peer}: ${msg.text}\n`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
243
266
|
async function cmdInbox(args) {
|
|
244
267
|
const json = hasFlag(args, "--json");
|
|
245
268
|
const from = valueForFlag(args, "--from");
|
|
@@ -362,6 +385,10 @@ async function main() {
|
|
|
362
385
|
await cmdInbox(args.slice(1));
|
|
363
386
|
return;
|
|
364
387
|
}
|
|
388
|
+
if (args[0] === "history") {
|
|
389
|
+
await cmdHistory(args.slice(1));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
365
392
|
if (args[0] === "listen") {
|
|
366
393
|
await cmdListen(args.slice(1));
|
|
367
394
|
return;
|
package/dist/network.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.sendMessage = sendMessage;
|
|
|
7
7
|
exports.fetchInbox = fetchInbox;
|
|
8
8
|
exports.ackMessages = ackMessages;
|
|
9
9
|
exports.waitForInbox = waitForInbox;
|
|
10
|
+
exports.fetchHistory = fetchHistory;
|
|
10
11
|
exports.askAgent = askAgent;
|
|
11
12
|
exports.toWsUrl = toWsUrl;
|
|
12
13
|
const config_1 = require("./config");
|
|
@@ -124,8 +125,11 @@ async function joinAgent(baseUrl, name) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
async function discoverPeers(baseUrl) {
|
|
127
|
-
|
|
128
|
+
// Prefer agent_key — survives join-token rotation.
|
|
129
|
+
const config = await (0, config_1.readConfig)();
|
|
130
|
+
const kind = config.agentKey ? "agent" : "token";
|
|
128
131
|
try {
|
|
132
|
+
const headers = await authHeaders(kind);
|
|
129
133
|
const response = await fetch(withPath(baseUrl, "/v1/peers"), {
|
|
130
134
|
method: "GET",
|
|
131
135
|
headers,
|
|
@@ -244,6 +248,36 @@ async function waitForInbox(baseUrl, options) {
|
|
|
244
248
|
}
|
|
245
249
|
return { kind: "timeout" };
|
|
246
250
|
}
|
|
251
|
+
async function fetchHistory(baseUrl, options) {
|
|
252
|
+
try {
|
|
253
|
+
const headers = await authHeaders("agent");
|
|
254
|
+
const params = new URLSearchParams();
|
|
255
|
+
if (options?.with) {
|
|
256
|
+
params.set("with", options.with);
|
|
257
|
+
}
|
|
258
|
+
if (options?.limit && options.limit > 0) {
|
|
259
|
+
params.set("limit", String(options.limit));
|
|
260
|
+
}
|
|
261
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
262
|
+
const response = await fetch(withPath(baseUrl, `/v1/messages/history${qs}`), { method: "GET", headers });
|
|
263
|
+
const data = (await response.json());
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
return {
|
|
266
|
+
kind: "error",
|
|
267
|
+
status: response.status,
|
|
268
|
+
message: data.error ?? `History failed with HTTP ${response.status}.`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
kind: "ok",
|
|
273
|
+
agent: data.agent ?? "",
|
|
274
|
+
messages: data.messages ?? [],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
return { kind: "error", message: error.message };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
247
281
|
async function askAgent(baseUrl, to, text, waitSeconds) {
|
|
248
282
|
// Drain any stale messages from this peer before asking.
|
|
249
283
|
const stale = await fetchInbox(baseUrl, { peek: true });
|