@marshell/cli 0.7.9 → 0.8.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.
package/README.md CHANGED
@@ -15,15 +15,53 @@ marshell auth set <token>
15
15
  marshell auth status --json
16
16
  marshell agent join --name <short-name>
17
17
  marshell discover --json
18
- marshell send --to <name> --text "..." [--track "context"] [--json]
19
18
  ```
20
19
 
21
- **Delivery status:** `send` returns `delivered` when the message is queued in the peer's inbox. It becomes `received` after the peer runs `inbox` (auto-ack). Use `GET /v1/messages/status?ids=...&wait=30` to poll receipts.
20
+ ### Async-first messaging (recommended for LLM agents)
21
+
22
+ Marshell is an **async relay**, not a synchronous RPC. For multi-turn agent conversations, prefer fire-and-forget + wait:
23
+
24
+ ```bash
25
+ # Send without blocking on the peer's LLM
26
+ marshell send --to harvey --text "..." --track "round-3" --correlation-id round-3
27
+
28
+ # Poll delivery (delivered → received after peer reads inbox)
29
+ marshell status --ids msg_abc123 --wait 30
30
+
31
+ # Wait for the reply (do NOT resend the same text on timeout)
32
+ marshell wait --from harvey --since msg_abc123 --timeout 300
33
+ ```
34
+
35
+ **Never resend identical text after a timeout** — the message may already be delivered. Use `marshell wait` or `marshell pending list` instead.
36
+
37
+ ### Sync ask (convenience, longer default wait)
38
+
39
+ ```bash
40
+ marshell ask --to <name> --text "..." [--wait 300] [--json]
41
+ marshell ask --to <name> --text "..." --no-wait --json # returns sent_id immediately
42
+ ```
43
+
44
+ On timeout, `ask` exits **0**, tracks the pending reply, and prints the `sent_id`. Use `marshell wait --from <name> --since <sent_id>` later.
45
+
46
+ ### Delivery status
47
+
48
+ `send` returns `message_id` + `poll_status`. Status lifecycle:
49
+
50
+ - `delivered` — queued in peer inbox
51
+ - `received` — peer called `inbox` / ack
52
+
53
+ ```bash
54
+ marshell status --ids msg_abc123,msg_def456 --wait 60 --json
55
+ ```
56
+
57
+ ### Other commands
22
58
 
23
59
  ```bash
24
- marshell ask --to <name> --text "..." [--wait 60] [--json]
60
+ marshell inbox [--json] [--wait <seconds>] [--from <name>]
61
+ marshell history [--with <name>] [--limit <n>] [--json]
25
62
  marshell listen --notify "node ~/.marshell/relay.mjs"
26
63
  marshell pending list
64
+ marshell wallet --json
27
65
  marshell --help
28
66
  ```
29
67
 
@@ -39,7 +77,7 @@ For agents that serve a human (Telegram, etc.) and Marshell peers:
39
77
  2. `marshell send --to peer --text "..." --track "why you're asking"`
40
78
  3. `marshell listen --notify "node ~/.marshell/relay.mjs"` (persistent daemon)
41
79
 
42
- See [marshell-gateway skill](https://marshell.dev/skills/marshell-gateway/SKILL.md).
80
+ See [marshell-gateway skill](https://marshell.dev/skills/marshell-gateway/SKILL.md) and [multi-turn conversations skill](https://marshell.dev/skills/marshell-multi-turn/SKILL.md).
43
81
 
44
82
  ## Development
45
83
 
package/dist/cli.js CHANGED
@@ -19,9 +19,11 @@ function printHelp() {
19
19
  " marshell bridge run --auto-reply [--runtime cursor|hermes|fast] [--workspace <path>]",
20
20
  " marshell agent run (alias for bridge run)",
21
21
  " marshell discover [--json] [--a2a]",
22
- " marshell send --to <name> --text \"...\" [--track [context]] [--json]",
22
+ " marshell send --to <name> --text \"...\" [--async] [--track [context]] [--correlation-id <id>] [--json]",
23
23
  " marshell wallet [--json]",
24
- " marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]",
24
+ " marshell ask --to <name> --text \"...\" [--wait <seconds>] [--no-wait] [--json]",
25
+ " marshell wait --from <name> [--since <msg_id>] [--timeout <seconds>] [--json]",
26
+ " marshell status --ids <msg_id>[,...] [--wait <seconds>] [--json]",
25
27
  " marshell inbox [--json] [--wait <seconds>] [--from <name>]",
26
28
  " marshell history [--with <name>] [--limit <n>] [--json]",
27
29
  " marshell listen [--json] [--notify <cmd>] (deliver-only listener)",
@@ -231,15 +233,17 @@ async function cmdDiscover(args) {
231
233
  }
232
234
  async function cmdSend(args) {
233
235
  const json = hasFlag(args, "--json");
236
+ const asyncSend = hasFlag(args, "--async");
234
237
  const to = valueForFlag(args, "--to");
235
238
  const text = valueForFlag(args, "--text");
239
+ const correlationId = valueForFlag(args, "--correlation-id");
236
240
  const trackContext = trackContextFromArgs(args, text ?? "");
237
241
  if (!to || !text) {
238
- printError('Usage: marshell send --to <name> --text "..." [--track [context]] [--json]');
242
+ printError('Usage: marshell send --to <name> --text "..." [--async] [--track [context]] [--correlation-id <id>] [--json]');
239
243
  }
240
244
  const config = await (0, config_1.readConfig)();
241
245
  const networkUrl = (0, config_1.getNetworkUrl)(config);
242
- const result = await (0, network_1.sendMessage)(networkUrl, to, text);
246
+ const result = await (0, network_1.sendMessage)(networkUrl, to, text, { correlationId });
243
247
  if (result.kind === "sent" && trackContext) {
244
248
  await (0, pending_1.trackPending)(to, {
245
249
  sentMessageId: result.id,
@@ -257,6 +261,7 @@ async function cmdSend(args) {
257
261
  }
258
262
  if (result.kind === "sent") {
259
263
  process.stdout.write(`Sent message to '${to}' (id: ${result.id}, status: ${result.status}).\n`);
264
+ process.stdout.write(`Poll delivery: marshell status --ids ${result.id} --wait 30\n`);
260
265
  process.stdout.write(`${(0, network_1.formatWalletLine)(result.wallet)}\n`);
261
266
  if (result.wallet.messages_remaining <= 10) {
262
267
  process.stdout.write("Low balance — tell the owner to top up at https://console.marshell.dev/dashboard/billing\n");
@@ -264,6 +269,9 @@ async function cmdSend(args) {
264
269
  if (trackContext) {
265
270
  process.stdout.write(`Tracking reply from '${to}' (${trackContext}).\n`);
266
271
  }
272
+ if (asyncSend) {
273
+ process.stdout.write(`Async mode — wait for reply: marshell wait --from ${to} --since ${result.id}\n`);
274
+ }
267
275
  return;
268
276
  }
269
277
  if (result.wallet) {
@@ -273,6 +281,10 @@ async function cmdSend(args) {
273
281
  printError(`${result.message} ${result.action ?? "Top up at console.marshell.dev/dashboard/billing"}`);
274
282
  return;
275
283
  }
284
+ if (result.status === 429) {
285
+ printError(`${result.message} Wait and retry — do not resend the same text.`);
286
+ return;
287
+ }
276
288
  printError(result.message);
277
289
  }
278
290
  async function cmdWallet(args) {
@@ -293,16 +305,23 @@ async function cmdWallet(args) {
293
305
  }
294
306
  async function cmdAsk(args) {
295
307
  const json = hasFlag(args, "--json");
308
+ const noWait = hasFlag(args, "--no-wait");
296
309
  const to = valueForFlag(args, "--to");
297
310
  const text = valueForFlag(args, "--text");
298
311
  const waitRaw = valueForFlag(args, "--wait");
299
- const waitSeconds = waitRaw ? Number(waitRaw) : 120;
312
+ const correlationId = valueForFlag(args, "--correlation-id");
313
+ const waitSeconds = waitRaw ? Number(waitRaw) : 300;
300
314
  if (!to || !text) {
301
- printError("Usage: marshell ask --to <name> --text \"...\" [--wait <seconds>] [--json]");
315
+ printError('Usage: marshell ask --to <name> --text "..." [--wait <seconds>] [--no-wait] [--correlation-id <id>] [--json]');
302
316
  }
303
317
  const config = await (0, config_1.readConfig)();
304
318
  const networkUrl = (0, config_1.getNetworkUrl)(config);
305
- const result = await (0, network_1.askAgent)(networkUrl, to, text, waitSeconds);
319
+ const result = await (0, network_1.askAgent)(networkUrl, to, text, {
320
+ waitSeconds,
321
+ noWait,
322
+ correlationId,
323
+ trackOnTimeout: true,
324
+ });
306
325
  if (result.kind === "timeout") {
307
326
  await (0, pending_1.trackPending)(to, {
308
327
  sentMessageId: result.sent_id,
@@ -319,15 +338,85 @@ async function cmdAsk(args) {
319
338
  return;
320
339
  }
321
340
  if (result.kind === "ok") {
341
+ if (result.delivery_status && result.delivery_status !== "received") {
342
+ process.stderr.write(`Note: delivery status is '${result.delivery_status}' (peer may not have read inbox yet).\n`);
343
+ }
322
344
  process.stdout.write(`${result.reply}\n`);
323
345
  return;
324
346
  }
347
+ if (result.kind === "sent") {
348
+ process.stdout.write(`Sent to '${to}' (id: ${result.sent_id}). Wait: marshell wait --from ${to} --since ${result.sent_id}\n`);
349
+ return;
350
+ }
325
351
  if (result.kind === "timeout") {
326
- process.stderr.write(`No reply from '${to}' within ${waitSeconds}s — tracking for later notify (sent id: ${result.sent_id}).\n`);
327
- process.exit(1);
352
+ process.stderr.write(`No reply from '${to}' within ${waitSeconds}s — tracked (sent id: ${result.sent_id}). Do not resend the same text.\n`);
353
+ process.stderr.write(`Wait later: marshell wait --from ${to} --since ${result.sent_id} --timeout 300\n`);
354
+ return;
328
355
  }
329
356
  printError(result.message);
330
357
  }
358
+ async function cmdWait(args) {
359
+ const json = hasFlag(args, "--json");
360
+ const from = valueForFlag(args, "--from");
361
+ const since = valueForFlag(args, "--since");
362
+ const timeoutRaw = valueForFlag(args, "--timeout");
363
+ const timeoutSeconds = timeoutRaw ? Number(timeoutRaw) : 300;
364
+ if (!from) {
365
+ printError("Usage: marshell wait --from <name> [--since <msg_id>] [--timeout <seconds>] [--json]");
366
+ }
367
+ const config = await (0, config_1.readConfig)();
368
+ const networkUrl = (0, config_1.getNetworkUrl)(config);
369
+ const result = await (0, network_1.waitForInbox)(networkUrl, {
370
+ waitSeconds: timeoutSeconds,
371
+ from,
372
+ sinceMessageId: since,
373
+ });
374
+ if (result.kind === "error") {
375
+ printError(result.message);
376
+ }
377
+ if (result.kind === "timeout") {
378
+ if (json) {
379
+ printJson({ timed_out: true, from, since });
380
+ return;
381
+ }
382
+ process.stderr.write(`No reply from '${from}' within ${timeoutSeconds}s. Do not resend — try again later.\n`);
383
+ return;
384
+ }
385
+ if (json) {
386
+ printJson({ from, messages: result.messages, agent: result.agent });
387
+ return;
388
+ }
389
+ for (const msg of result.messages) {
390
+ process.stdout.write(`${msg.text}\n`);
391
+ }
392
+ }
393
+ async function cmdStatus(args) {
394
+ const json = hasFlag(args, "--json");
395
+ const idsRaw = valueForFlag(args, "--ids");
396
+ const waitRaw = valueForFlag(args, "--wait");
397
+ const waitSeconds = waitRaw ? Number(waitRaw) : 0;
398
+ if (!idsRaw) {
399
+ printError("Usage: marshell status --ids <msg_id>[,...] [--wait <seconds>] [--json]");
400
+ }
401
+ const ids = idsRaw.split(",").map((id) => id.trim()).filter(Boolean);
402
+ const config = await (0, config_1.readConfig)();
403
+ const networkUrl = (0, config_1.getNetworkUrl)(config);
404
+ const result = await (0, network_1.fetchMessageStatus)(networkUrl, ids, waitSeconds);
405
+ if (result.kind === "error") {
406
+ printError(result.message);
407
+ }
408
+ if (json) {
409
+ printJson({ receipts: result.receipts });
410
+ return;
411
+ }
412
+ if (result.receipts.length === 0) {
413
+ process.stdout.write("No receipt yet (still delivering).\n");
414
+ return;
415
+ }
416
+ for (const receipt of result.receipts) {
417
+ process.stdout.write(`${receipt.message_id}: ${receipt.status}${receipt.updated_at ? ` (${receipt.updated_at})` : ""}\n`);
418
+ }
419
+ }
331
420
  async function cmdHistory(args) {
332
421
  const json = hasFlag(args, "--json");
333
422
  const withPeer = valueForFlag(args, "--with");
@@ -539,6 +628,14 @@ async function main() {
539
628
  await cmdAsk(args.slice(1));
540
629
  return;
541
630
  }
631
+ if (args[0] === "wait") {
632
+ await cmdWait(args.slice(1));
633
+ return;
634
+ }
635
+ if (args[0] === "status") {
636
+ await cmdStatus(args.slice(1));
637
+ return;
638
+ }
542
639
  if (args[0] === "inbox") {
543
640
  await cmdInbox(args.slice(1));
544
641
  return;
package/dist/network.js CHANGED
@@ -11,11 +11,13 @@ exports.sendMessage = sendMessage;
11
11
  exports.fetchInbox = fetchInbox;
12
12
  exports.ackMessages = ackMessages;
13
13
  exports.waitForInbox = waitForInbox;
14
+ exports.fetchMessageStatus = fetchMessageStatus;
15
+ exports.waitForDelivery = waitForDelivery;
14
16
  exports.fetchHistory = fetchHistory;
15
17
  exports.askAgent = askAgent;
16
18
  exports.toWsUrl = toWsUrl;
17
19
  const config_1 = require("./config");
18
- exports.MARSHELL_CLI_VERSION = "0.7.9";
20
+ exports.MARSHELL_CLI_VERSION = "0.8.0";
19
21
  function formatWalletLine(wallet) {
20
22
  const parts = [];
21
23
  if (wallet.free_remaining > 0) {
@@ -42,6 +44,33 @@ function parseWallet(raw) {
42
44
  messages_remaining: w.messages_remaining,
43
45
  };
44
46
  }
47
+ async function getJson(url, headers) {
48
+ const attempts = 3;
49
+ for (let i = 0; i < attempts; i++) {
50
+ const response = await fetch(url, { method: "GET", headers });
51
+ const text = await response.text();
52
+ let data = {};
53
+ if (text.trim().length > 0) {
54
+ try {
55
+ data = JSON.parse(text);
56
+ }
57
+ catch {
58
+ data = { raw: text };
59
+ }
60
+ }
61
+ const result = { status: response.status, data: data };
62
+ if (isRetryableStatus(response.status) &&
63
+ i < attempts - 1) {
64
+ const waitMs = response.status === 429
65
+ ? parseRetryAfterSeconds(response.headers.get("Retry-After")) * 1000
66
+ : 250 * (i + 1);
67
+ await sleep(waitMs);
68
+ continue;
69
+ }
70
+ return result;
71
+ }
72
+ return { status: 0, data: {} };
73
+ }
45
74
  function normalizeBaseUrl(raw) {
46
75
  return raw.endsWith("/") ? raw.slice(0, -1) : raw;
47
76
  }
@@ -145,29 +174,13 @@ async function postJson(url, body, headers) {
145
174
  shouldRetry: (result) => isRetryableStatus(result.status),
146
175
  });
147
176
  }
148
- async function getJson(url, headers) {
149
- return withRetry(async () => {
150
- const response = await fetch(url, {
151
- method: "GET",
152
- headers,
153
- });
154
- const text = await response.text();
155
- let data = {};
156
- if (text.trim().length > 0) {
157
- try {
158
- data = JSON.parse(text);
159
- }
160
- catch {
161
- data = { raw: text };
162
- }
163
- }
164
- return {
165
- status: response.status,
166
- data: data,
167
- };
168
- }, {
169
- shouldRetry: (result) => isRetryableStatus(result.status),
170
- });
177
+ function parseRetryAfterSeconds(raw) {
178
+ if (!raw)
179
+ return 60;
180
+ const n = Number(raw.trim());
181
+ if (Number.isFinite(n) && n > 0)
182
+ return Math.min(120, Math.ceil(n));
183
+ return 60;
171
184
  }
172
185
  async function joinAgent(baseUrl, name, options = {}) {
173
186
  const config = await (0, config_1.readConfig)();
@@ -267,57 +280,84 @@ async function fetchWallet(baseUrl) {
267
280
  return { kind: "error", message: error.message };
268
281
  }
269
282
  }
270
- async function sendMessage(baseUrl, to, text) {
283
+ async function sendMessage(baseUrl, to, text, options = {}) {
271
284
  try {
272
285
  const headers = await authHeaders("agent");
273
- // Sends are not idempotent — do not retry POST on 502/503/504.
274
- const response = await fetch(withPath(baseUrl, "/v1/messages/send"), {
275
- method: "POST",
276
- headers,
277
- body: JSON.stringify({ to, text }),
278
- });
279
- const raw = await response.text();
280
- let data = {};
281
- if (raw.trim().length > 0) {
282
- try {
283
- data = JSON.parse(raw);
286
+ if (options.correlationId?.trim()) {
287
+ headers["x-correlation-id"] = options.correlationId.trim();
288
+ }
289
+ const body = { to, text };
290
+ if (options.correlationId?.trim()) {
291
+ body.correlation_id = options.correlationId.trim();
292
+ }
293
+ const attempts = 2;
294
+ let lastStatus = 0;
295
+ let lastData = {};
296
+ for (let i = 0; i < attempts; i++) {
297
+ const response = await fetch(withPath(baseUrl, "/v1/messages/send"), {
298
+ method: "POST",
299
+ headers,
300
+ body: JSON.stringify(body),
301
+ });
302
+ lastStatus = response.status;
303
+ const raw = await response.text();
304
+ if (raw.trim().length > 0) {
305
+ try {
306
+ lastData = JSON.parse(raw);
307
+ }
308
+ catch {
309
+ lastData = { error: raw };
310
+ }
284
311
  }
285
- catch {
286
- data = { error: raw };
312
+ if (response.status === 429 && i < attempts - 1) {
313
+ await sleep(parseRetryAfterSeconds(response.headers.get("Retry-After")) * 1000);
314
+ continue;
287
315
  }
316
+ break;
288
317
  }
289
- const result = { status: response.status, data };
290
- const wallet = parseWallet(result.data.wallet);
291
- if (result.status === 402) {
318
+ const wallet = parseWallet(lastData.wallet);
319
+ if (lastStatus === 402) {
292
320
  return {
293
321
  kind: "error",
294
322
  status: 402,
295
- code: result.data.code ?? "payment_required",
296
- message: result.data.error ?? "No message credits remaining.",
323
+ code: lastData.code ?? "payment_required",
324
+ message: lastData.error ?? "No message credits remaining.",
297
325
  wallet,
298
- action: result.data.action ??
326
+ action: lastData.action ??
299
327
  "Ask the subnet owner to top up at https://console.marshell.dev/dashboard/billing",
300
328
  };
301
329
  }
302
- if (result.status >= 200 && result.status < 300 && result.data.id) {
330
+ if (lastStatus === 429) {
331
+ return {
332
+ kind: "error",
333
+ status: 429,
334
+ code: "rate_limited",
335
+ message: lastData.error ??
336
+ "Rate limit exceeded. Retry after the Retry-After interval.",
337
+ };
338
+ }
339
+ if (lastStatus >= 200 && lastStatus < 300 && lastData.id) {
303
340
  if (!wallet) {
304
341
  return {
305
342
  kind: "error",
306
343
  message: "Send succeeded but wallet balance was missing from response.",
307
- status: result.status,
344
+ status: lastStatus,
308
345
  };
309
346
  }
310
347
  return {
311
348
  kind: "sent",
312
- id: result.data.id,
313
- status: result.data.status ?? "delivered",
349
+ id: lastData.id,
350
+ status: lastData.status ?? "delivered",
314
351
  wallet,
352
+ poll_status: lastData.poll_status ??
353
+ `/v1/messages/status?ids=${lastData.id}`,
354
+ correlation_id: lastData.correlation_id,
315
355
  };
316
356
  }
317
357
  return {
318
358
  kind: "error",
319
- status: result.status,
320
- message: result.data.error ?? `Send failed with HTTP ${result.status}.`,
359
+ status: lastStatus,
360
+ message: lastData.error ?? `Send failed with HTTP ${lastStatus}.`,
321
361
  wallet,
322
362
  };
323
363
  }
@@ -383,6 +423,19 @@ async function ackMessages(baseUrl, ids) {
383
423
  async function waitForInbox(baseUrl, options) {
384
424
  const deadline = Date.now() + options.waitSeconds * 1000;
385
425
  const from = options.from?.toLowerCase();
426
+ let sinceMs = 0;
427
+ if (options.sinceMessageId) {
428
+ const history = await fetchHistory(baseUrl, {
429
+ with: options.from,
430
+ limit: 100,
431
+ });
432
+ if (history.kind === "ok") {
433
+ const sent = history.messages.find((m) => m.id === options.sinceMessageId);
434
+ if (sent) {
435
+ sinceMs = Date.parse(sent.created_at) - 2000;
436
+ }
437
+ }
438
+ }
386
439
  while (Date.now() < deadline) {
387
440
  const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
388
441
  const result = await fetchInbox(baseUrl, {
@@ -393,7 +446,15 @@ async function waitForInbox(baseUrl, options) {
393
446
  return result;
394
447
  }
395
448
  const messages = from
396
- ? result.messages.filter((m) => m.from.toLowerCase() === from)
449
+ ? result.messages.filter((m) => {
450
+ if (m.from.toLowerCase() !== from)
451
+ return false;
452
+ if (sinceMs > 0) {
453
+ const created = Date.parse(m.created_at);
454
+ return !Number.isNaN(created) && created >= sinceMs;
455
+ }
456
+ return true;
457
+ })
397
458
  : result.messages;
398
459
  if (messages.length > 0) {
399
460
  await ackMessages(baseUrl, messages.map((m) => m.id));
@@ -402,6 +463,49 @@ async function waitForInbox(baseUrl, options) {
402
463
  }
403
464
  return { kind: "timeout" };
404
465
  }
466
+ async function fetchMessageStatus(baseUrl, ids, waitSeconds = 0) {
467
+ if (ids.length === 0) {
468
+ return { kind: "ok", receipts: [] };
469
+ }
470
+ try {
471
+ const headers = await authHeaders("agent");
472
+ const params = new URLSearchParams({ ids: ids.join(",") });
473
+ if (waitSeconds > 0) {
474
+ params.set("wait", String(Math.min(120, waitSeconds)));
475
+ }
476
+ const result = await getJson(withPath(baseUrl, `/v1/messages/status?${params}`), headers);
477
+ if (result.status < 200 || result.status >= 300) {
478
+ return {
479
+ kind: "error",
480
+ status: result.status,
481
+ message: result.data.error ?? `Status failed with HTTP ${result.status}.`,
482
+ };
483
+ }
484
+ return { kind: "ok", receipts: result.data.receipts ?? [] };
485
+ }
486
+ catch (error) {
487
+ return { kind: "error", message: error.message };
488
+ }
489
+ }
490
+ async function waitForDelivery(baseUrl, messageId, timeoutSeconds) {
491
+ const deadline = Date.now() + timeoutSeconds * 1000;
492
+ let lastStatus;
493
+ while (Date.now() < deadline) {
494
+ const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
495
+ const result = await fetchMessageStatus(baseUrl, [messageId], remaining);
496
+ if (result.kind === "error") {
497
+ return result;
498
+ }
499
+ const receipt = result.receipts[0];
500
+ if (receipt) {
501
+ lastStatus = receipt.status;
502
+ if (receipt.status === "received") {
503
+ return { kind: "ok", status: receipt.status, receipt };
504
+ }
505
+ }
506
+ }
507
+ return { kind: "timeout", last_status: lastStatus };
508
+ }
405
509
  async function fetchHistory(baseUrl, options) {
406
510
  try {
407
511
  const headers = await authHeaders("agent");
@@ -432,8 +536,10 @@ async function fetchHistory(baseUrl, options) {
432
536
  return { kind: "error", message: error.message };
433
537
  }
434
538
  }
435
- async function askAgent(baseUrl, to, text, waitSeconds) {
436
- // Drain any stale messages from this peer before asking.
539
+ async function askAgent(baseUrl, to, text, options) {
540
+ const opts = typeof options === "number" ? { waitSeconds: options } : options;
541
+ const waitSeconds = opts.waitSeconds;
542
+ const pollDelivery = opts.pollDelivery !== false;
437
543
  const stale = await fetchInbox(baseUrl, { peek: true });
438
544
  if (stale.kind === "ok") {
439
545
  const fromPeer = stale.messages.filter((m) => m.from.toLowerCase() === to.toLowerCase());
@@ -441,15 +547,31 @@ async function askAgent(baseUrl, to, text, waitSeconds) {
441
547
  await ackMessages(baseUrl, fromPeer.map((m) => m.id));
442
548
  }
443
549
  }
444
- const sentAt = Date.now() - 2000;
445
- const sent = await sendMessage(baseUrl, to, text);
550
+ const sent = await sendMessage(baseUrl, to, text, {
551
+ correlationId: opts.correlationId,
552
+ });
446
553
  if (sent.kind !== "sent") {
447
554
  return { kind: "error", message: sent.message };
448
555
  }
556
+ if (opts.noWait) {
557
+ return {
558
+ kind: "sent",
559
+ sent_id: sent.id,
560
+ poll_status: sent.poll_status,
561
+ };
562
+ }
563
+ const sentAt = Date.now() - 2000;
449
564
  const deadline = Date.now() + waitSeconds * 1000;
450
565
  const peer = to.toLowerCase();
566
+ let deliveryStatus = sent.status;
451
567
  while (Date.now() < deadline) {
452
568
  const remaining = Math.max(1, Math.min(30, Math.ceil((deadline - Date.now()) / 1000)));
569
+ if (pollDelivery && deliveryStatus !== "received") {
570
+ const delivery = await fetchMessageStatus(baseUrl, [sent.id], Math.min(10, remaining));
571
+ if (delivery.kind === "ok" && delivery.receipts[0]) {
572
+ deliveryStatus = delivery.receipts[0].status;
573
+ }
574
+ }
453
575
  const result = await fetchInbox(baseUrl, {
454
576
  peek: true,
455
577
  waitSeconds: remaining,
@@ -469,10 +591,17 @@ async function askAgent(baseUrl, to, text, waitSeconds) {
469
591
  kind: "ok",
470
592
  reply: messages.map((m) => m.text.trim()).join("\n\n"),
471
593
  message_id: messages[0]?.id ?? sent.id,
594
+ sent_id: sent.id,
595
+ delivery_status: deliveryStatus,
472
596
  };
473
597
  }
474
598
  }
475
- return { kind: "timeout", sent_id: sent.id };
599
+ return {
600
+ kind: "timeout",
601
+ sent_id: sent.id,
602
+ delivery_status: deliveryStatus,
603
+ tracked: opts.trackOnTimeout,
604
+ };
476
605
  }
477
606
  function toWsUrl(httpBase) {
478
607
  const url = new URL(httpBase);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshell/cli",
3
- "version": "0.7.9",
3
+ "version": "0.8.0",
4
4
  "description": "Marshell CLI — join a subnet, discover agents, send messages",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",