a2acalling 0.6.36 → 0.6.37

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/bin/cli.js CHANGED
@@ -18,6 +18,7 @@
18
18
  const fs = require('fs');
19
19
  const os = require('os');
20
20
  const path = require('path');
21
+ const crypto = require('crypto');
21
22
  const { spawn } = require('child_process');
22
23
  const { TokenStore } = require('../src/lib/tokens');
23
24
  const { A2AClient } = require('../src/lib/client');
@@ -296,6 +297,166 @@ function summarizePortResults(portResults) {
296
297
  });
297
298
  }
298
299
 
300
+ /**
301
+ * Identify what process is using a given port.
302
+ * Returns { pid, name } or null if detection fails.
303
+ */
304
+ function identifyPortProcess(port) {
305
+ const p = Number(port);
306
+ if (!Number.isFinite(p) || p <= 0) return null;
307
+ const { execSync } = require('child_process');
308
+ // Try lsof first (most common on Linux/macOS)
309
+ try {
310
+ const out = execSync(`lsof -i :${p} -sTCP:LISTEN -t 2>/dev/null`, { encoding: 'utf8', timeout: 5000 }).trim();
311
+ if (out) {
312
+ const pid = out.split('\n')[0].trim();
313
+ let name = 'unknown';
314
+ try {
315
+ name = execSync(`ps -p ${pid} -o comm= 2>/dev/null`, { encoding: 'utf8', timeout: 3000 }).trim();
316
+ } catch (e) { /* best-effort */ }
317
+ return { pid: Number(pid), name };
318
+ }
319
+ } catch (e) { /* lsof not available or failed */ }
320
+
321
+ // Fallback: ss (Linux)
322
+ try {
323
+ const out = execSync(`ss -tlnp 'sport = :${p}' 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
324
+ const pidMatch = out.match(/pid=(\d+)/);
325
+ const nameMatch = out.match(/\("([^"]+)"/);
326
+ if (pidMatch) {
327
+ return { pid: Number(pidMatch[1]), name: nameMatch ? nameMatch[1] : 'unknown' };
328
+ }
329
+ } catch (e) { /* ss not available */ }
330
+
331
+ return null;
332
+ }
333
+
334
+ /**
335
+ * When port 80 is unavailable, prompt the user with fallback options.
336
+ * Returns { strategy: 'kill' | 'proxy' | 'continue', port: number }
337
+ *
338
+ * Non-interactive: auto-returns 'continue' with a printed warning.
339
+ */
340
+ async function promptPortFallbackStrategy(fallbackPort, interactive) {
341
+ const processInfo = identifyPortProcess(80);
342
+
343
+ console.log('\n ┌─────────────────────────────────────────────────────────────────┐');
344
+ console.log(' │ ⚠ PORT 80 IS UNAVAILABLE │');
345
+ console.log(' └─────────────────────────────────────────────────────────────────┘');
346
+ console.log('');
347
+ if (processInfo) {
348
+ console.log(` Port 80 is held by: ${processInfo.name} (PID ${processInfo.pid})`);
349
+ } else {
350
+ console.log(' Port 80 is in use by another process (could not identify).');
351
+ }
352
+ console.log('');
353
+ console.log(' Why this matters:');
354
+ console.log(' - Port 80 is the default HTTP port — no firewall config needed');
355
+ console.log(` - Fallback port ${fallbackPort} may be blocked by the caller's firewall`);
356
+ console.log(` - If the server restarts on a different port, all invite URLs break`);
357
+ console.log(` - Invite URLs with non-standard ports look like: a2a://host:${fallbackPort}/token`);
358
+ console.log('');
359
+
360
+ if (!interactive) {
361
+ console.log(` Non-interactive mode: continuing on port ${fallbackPort}.`);
362
+ console.log(' Set up a reverse proxy (port 80 → ' + fallbackPort + ') for production use.\n');
363
+ return { strategy: 'continue', port: fallbackPort };
364
+ }
365
+
366
+ console.log(' Options:');
367
+ if (processInfo) {
368
+ console.log(` 1) Kill ${processInfo.name} (PID ${processInfo.pid}) and use port 80`);
369
+ } else {
370
+ console.log(' 1) Kill the process on port 80 and retry');
371
+ }
372
+ console.log(` 2) Set up a reverse proxy (port 80 → ${fallbackPort})`);
373
+ console.log(` 3) Continue on port ${fallbackPort} (not recommended for production)`);
374
+ console.log('');
375
+
376
+ const choice = await promptText(' Choose [1/2/3]: ', '2');
377
+ const normalized = String(choice).trim();
378
+
379
+ if (normalized === '1') {
380
+ return { strategy: 'kill', port: 80, processInfo };
381
+ } else if (normalized === '3') {
382
+ console.log(`\n ⚠ Continuing on port ${fallbackPort}.`);
383
+ console.log(` Invite URLs will include :${fallbackPort} and may not be reachable externally.`);
384
+ console.log(' You can set up a reverse proxy later with: a2a config --help\n');
385
+ return { strategy: 'continue', port: fallbackPort };
386
+ } else {
387
+ // Default: reverse proxy (option 2)
388
+ return { strategy: 'proxy', port: fallbackPort };
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Attempt to kill the process on a given port.
394
+ * Returns true if kill succeeded and port is now available.
395
+ */
396
+ async function killPortProcess(processInfo) {
397
+ if (!processInfo || !Number.isFinite(processInfo.pid)) return false;
398
+ const { execSync } = require('child_process');
399
+ try {
400
+ console.log(` Killing ${processInfo.name} (PID ${processInfo.pid})...`);
401
+ execSync(`kill ${processInfo.pid}`, { timeout: 5000 });
402
+ // Wait briefly for the port to free up
403
+ await new Promise(r => setTimeout(r, 1000));
404
+ const { tryBindPort } = require('../src/lib/port-scanner');
405
+ const result = await tryBindPort(80);
406
+ if (result.ok) {
407
+ console.log(' ✅ Port 80 is now available.');
408
+ return true;
409
+ }
410
+ console.log(' Port 80 is still in use after kill. The process may require sudo to stop.');
411
+ return false;
412
+ } catch (e) {
413
+ console.log(` Could not kill process: ${e.message}`);
414
+ console.log(' You may need to run: sudo kill ' + processInfo.pid);
415
+ return false;
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Detect installed web servers and generate reverse proxy config.
421
+ * Returns { hasNginx, hasCaddy, nginxConfig, caddyConfig }.
422
+ */
423
+ function generateProxyConfig(backendPort) {
424
+ const { spawnSync } = require('child_process');
425
+ const hasNginx = spawnSync('which', ['nginx'], { encoding: 'utf8' }).status === 0;
426
+ const hasCaddy = spawnSync('which', ['caddy'], { encoding: 'utf8' }).status === 0;
427
+
428
+ const nginxConfig = [
429
+ '# ══════════════════════════════════════════════════════════════',
430
+ '# A2A (Agent-to-Agent) Protocol Proxy',
431
+ '# ══════════════════════════════════════════════════════════════',
432
+ '# Routes federation requests from port 80 to the local',
433
+ `# A2A server on port ${backendPort}.`,
434
+ '#',
435
+ '# Protocol: https://github.com/onthegonow/a2a_calling',
436
+ '# All requests to /api/a2a/* are agent-to-agent API calls.',
437
+ '# ══════════════════════════════════════════════════════════════',
438
+ 'location /api/a2a/ {',
439
+ ` proxy_pass http://127.0.0.1:${backendPort}/api/a2a/;`,
440
+ ' proxy_http_version 1.1;',
441
+ ' proxy_set_header Host $host;',
442
+ ' proxy_set_header X-Real-IP $remote_addr;',
443
+ ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;',
444
+ ' proxy_set_header X-Forwarded-Proto $scheme;',
445
+ '}'
446
+ ].join('\n');
447
+
448
+ const caddyConfig = [
449
+ '# A2A (Agent-to-Agent) Protocol Proxy',
450
+ `# Routes federation requests to local A2A server on port ${backendPort}`,
451
+ '# Protocol: https://github.com/onthegonow/a2a_calling',
452
+ 'handle /api/a2a/* {',
453
+ ` reverse_proxy 127.0.0.1:${backendPort}`,
454
+ '}'
455
+ ].join('\n');
456
+
457
+ return { hasNginx, hasCaddy, nginxConfig, caddyConfig };
458
+ }
459
+
299
460
  async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
300
461
  const submitRaw = args.flags.submit;
301
462
  if (!submitRaw) return false;
@@ -1204,28 +1365,45 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1204
1365
 
1205
1366
  // Persist conversation locally
1206
1367
  const cs = getConvStore();
1207
- if (cs && response.conversation_id) {
1368
+ if (cs) {
1208
1369
  try {
1209
- cs.startConversation({
1210
- id: response.conversation_id,
1370
+ // Use remote conversation ID if provided, otherwise generate a local one
1371
+ const convId = response.conversation_id || `conv_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
1372
+
1373
+ const convResult = cs.startConversation({
1374
+ id: convId,
1211
1375
  contactId: contactName || null,
1212
1376
  contactName: contactName || null,
1213
1377
  direction: 'outbound'
1214
1378
  });
1215
- cs.addMessage(response.conversation_id, {
1216
- direction: 'outbound',
1217
- role: 'user',
1218
- content: message
1219
- });
1220
- if (response.response) {
1221
- cs.addMessage(response.conversation_id, {
1222
- direction: 'inbound',
1223
- role: 'assistant',
1224
- content: response.response
1379
+ if (convResult.success === false) {
1380
+ console.error(`⚠️ Failed to save conversation: ${convResult.error}`);
1381
+ } else {
1382
+ const outMsg = cs.addMessage(convId, {
1383
+ direction: 'outbound',
1384
+ role: 'user',
1385
+ content: message
1225
1386
  });
1387
+ if (outMsg.success === false) {
1388
+ console.error(`⚠️ Failed to save outbound message: ${outMsg.error}`);
1389
+ }
1390
+ if (response.response) {
1391
+ const inMsg = cs.addMessage(convId, {
1392
+ direction: 'inbound',
1393
+ role: 'assistant',
1394
+ content: response.response
1395
+ });
1396
+ if (inMsg.success === false) {
1397
+ console.error(`⚠️ Failed to save inbound message: ${inMsg.error}`);
1398
+ }
1399
+ }
1400
+ // Update response to include conversation ID for display
1401
+ if (!response.conversation_id) {
1402
+ response.conversation_id = convId;
1403
+ }
1226
1404
  }
1227
1405
  } catch (err) {
1228
- // Best effort — don't fail the call if persistence fails
1406
+ console.error(`⚠️ Error persisting conversation: ${err.message}`);
1229
1407
  }
1230
1408
  }
1231
1409
 
@@ -1474,11 +1652,8 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1474
1652
  let recommendedPort;
1475
1653
  if (port80Available) {
1476
1654
  recommendedPort = 80;
1477
- console.log(' Port 80 is available — using it for easiest external access.');
1478
1655
  } else if (availableCandidates.length) {
1479
1656
  recommendedPort = availableCandidates[0].port;
1480
- console.log(` Port 80 is in use. Using fallback port ${recommendedPort}.`);
1481
- console.log(' (Reverse proxy or firewall config will be needed for external access.)');
1482
1657
  } else {
1483
1658
  recommendedPort = null;
1484
1659
  }
@@ -1494,49 +1669,76 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1494
1669
  }
1495
1670
 
1496
1671
  let serverPort = recommendedPort;
1497
-
1498
- // If we got port 80, just confirm briefly. Otherwise allow override.
1499
- const portPrompt = port80Available
1500
- ? `Use port 80? [Y/n]: `
1501
- : `Use port ${recommendedPort}? [Y/n/custom]: `;
1502
- const portChoice = await promptText(portPrompt, 'y');
1503
-
1504
- if (!interactive) {
1505
- serverPort = recommendedPort;
1506
- } else if (!['', 'y', 'Y', 'yes', 'YES', 'ye'].includes(String(portChoice).trim())) {
1507
- if (/^(n|no|custom|c)$/i.test(String(portChoice).trim())) {
1508
- let customPort = null;
1509
- while (customPort === null) {
1510
- const raw = await promptText('Enter a custom port number: ', String(recommendedPort));
1511
- const parsed = parsePort(raw, null);
1512
- if (!parsed) {
1513
- console.log(' Invalid port. Enter a value between 1 and 65535.');
1514
- continue;
1672
+ let proxyStrategy = false; // true if user chose reverse proxy option
1673
+
1674
+ if (port80Available) {
1675
+ // Port 80 available — simple confirm
1676
+ console.log(' Port 80 is available using it for easiest external access.');
1677
+ const portPrompt = `Use port 80? [Y/n]: `;
1678
+ const portChoice = await promptText(portPrompt, 'y');
1679
+
1680
+ if (!interactive) {
1681
+ serverPort = 80;
1682
+ } else if (!['', 'y', 'Y', 'yes', 'YES', 'ye'].includes(String(portChoice).trim())) {
1683
+ if (/^(n|no|custom|c)$/i.test(String(portChoice).trim())) {
1684
+ let customPort = null;
1685
+ while (customPort === null) {
1686
+ const raw = await promptText('Enter a custom port number: ', String(recommendedPort));
1687
+ const parsed = parsePort(raw, null);
1688
+ if (!parsed) {
1689
+ console.log(' Invalid port. Enter a value between 1 and 65535.');
1690
+ continue;
1691
+ }
1692
+ const checked = await (async () => {
1693
+ const scan = await inspectPorts(parsed);
1694
+ return scan[0];
1695
+ })();
1696
+ if (!checked.available) {
1697
+ console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
1698
+ continue;
1699
+ }
1700
+ customPort = parsed;
1515
1701
  }
1516
- const checked = await (async () => {
1517
- const scan = await inspectPorts(parsed);
1518
- return scan[0];
1519
- })();
1520
- if (!checked.available) {
1521
- console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
1522
- continue;
1702
+ serverPort = customPort;
1703
+ } else {
1704
+ const parsed = parsePort(portChoice, null);
1705
+ if (parsed) {
1706
+ const checked = await (async () => {
1707
+ const scan = await inspectPorts(parsed);
1708
+ return scan[0];
1709
+ })();
1710
+ if (!checked.available) {
1711
+ console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
1712
+ } else {
1713
+ serverPort = parsed;
1714
+ }
1523
1715
  }
1524
- customPort = parsed;
1525
1716
  }
1526
- serverPort = customPort;
1527
- } else {
1528
- const parsed = parsePort(portChoice, null);
1529
- if (parsed) {
1530
- const checked = await (async () => {
1531
- const scan = await inspectPorts(parsed);
1532
- return scan[0];
1533
- })();
1534
- if (!checked.available) {
1535
- console.log(` Port ${parsed} is unavailable (${checked.code || 'in use'}).`);
1717
+ }
1718
+ } else {
1719
+ // Port 80 NOT available — show the fallback strategy prompt
1720
+ const fallback = await promptPortFallbackStrategy(recommendedPort, interactive);
1721
+
1722
+ if (fallback.strategy === 'kill') {
1723
+ const confirmKill = await promptYesNo(` Kill ${(fallback.processInfo && fallback.processInfo.name) || 'process'} (PID ${(fallback.processInfo && fallback.processInfo.pid) || '?'})? (y/N) `);
1724
+ if (confirmKill) {
1725
+ const killed = await killPortProcess(fallback.processInfo);
1726
+ if (killed) {
1727
+ serverPort = 80;
1536
1728
  } else {
1537
- serverPort = parsed;
1729
+ console.log(`\n Falling back to port ${recommendedPort}.`);
1730
+ console.log(' You can set up a reverse proxy after setup completes.\n');
1731
+ serverPort = recommendedPort;
1538
1732
  }
1733
+ } else {
1734
+ console.log(` Skipped. Using port ${recommendedPort}.\n`);
1735
+ serverPort = recommendedPort;
1539
1736
  }
1737
+ } else if (fallback.strategy === 'proxy') {
1738
+ serverPort = fallback.port;
1739
+ proxyStrategy = true;
1740
+ } else {
1741
+ serverPort = fallback.port;
1540
1742
  }
1541
1743
  }
1542
1744
 
@@ -1651,81 +1853,60 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
1651
1853
  // Port 80 — optimal setup, no extra config needed
1652
1854
  console.log(`\n ✅ Running on port 80 — external agents can reach you directly.`);
1653
1855
  console.log(` Invite hostname: ${externalIp}`);
1654
- // Update publicHost to not include port since 80 is default
1655
1856
  publicHost = externalIp;
1656
- } else {
1657
- // Not on port 80 need reverse proxy or firewall config
1658
- const { spawnSync } = require('child_process');
1659
- const hasNginx = spawnSync('which', ['nginx'], { encoding: 'utf8' }).status === 0;
1660
- const hasCaddy = spawnSync('which', ['caddy'], { encoding: 'utf8' }).status === 0;
1661
-
1662
- console.log(`\n ━━━ IMPORTANT: External Access Configuration ━━━`);
1663
- console.log(`\n A2A server is on port ${serverPort}, but external callers expect port 80.`);
1664
- console.log(` Port 80 is in use by ${hasNginx ? 'nginx' : hasCaddy ? 'Caddy' : 'another web server'}.`);
1665
- console.log(`\n RECOMMENDED: Configure reverse proxy to route /api/a2a/* to port ${serverPort}`);
1666
-
1667
- if (hasNginx) {
1668
- console.log(`\n ┌─────────────────────────────────────────────────────────────────┐`);
1669
- console.log(` │ nginx config — add inside your server {} block │`);
1670
- console.log(` │ File: /etc/nginx/sites-available/default │`);
1671
- console.log(` └─────────────────────────────────────────────────────────────────┘`);
1672
- console.log(``);
1673
- console.log(` # ══════════════════════════════════════════════════════════════`);
1674
- console.log(` # A2A (Agent-to-Agent) Protocol Proxy`);
1675
- console.log(` # ══════════════════════════════════════════════════════════════`);
1676
- console.log(` # A2A enables AI agents to communicate across different instances.`);
1677
- console.log(` # This proxy routes federation requests from port 80 to the local`);
1678
- console.log(` # A2A server on port ${serverPort}.`);
1679
- console.log(` #`);
1680
- console.log(` # Protocol: https://github.com/onthegonow/a2a_calling`);
1681
- console.log(` # All requests to /api/a2a/* are agent-to-agent API calls.`);
1682
- console.log(` # ══════════════════════════════════════════════════════════════`);
1683
- console.log(` location /api/a2a/ {`);
1684
- console.log(` proxy_pass http://127.0.0.1:${serverPort}/api/a2a/;`);
1685
- console.log(` proxy_http_version 1.1;`);
1686
- console.log(` proxy_set_header Host $host;`);
1687
- console.log(` proxy_set_header X-Real-IP $remote_addr;`);
1688
- console.log(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`);
1689
- console.log(` proxy_set_header X-Forwarded-Proto $scheme;`);
1690
- console.log(` }`);
1691
- console.log(``);
1692
- console.log(` To apply:`);
1693
- console.log(` 1. sudo nano /etc/nginx/sites-available/default`);
1694
- console.log(` 2. Add the config above inside your server { } block`);
1695
- console.log(` 3. sudo nginx -t`);
1696
- console.log(` 4. sudo systemctl reload nginx`);
1857
+ } else if (proxyStrategy) {
1858
+ // User chose to set up a reverse proxy
1859
+ const proxy = generateProxyConfig(serverPort);
1860
+
1861
+ console.log(`\n ━━━ Reverse Proxy Configuration ━━━`);
1862
+ console.log(`\n A2A server running on port ${serverPort}. Configure your web server`);
1863
+ console.log(` to proxy port 80 ${serverPort} so invite URLs work without a port number.\n`);
1864
+
1865
+ if (proxy.hasNginx) {
1866
+ console.log(' ┌─────────────────────────────────────────────────────────────────┐');
1867
+ console.log(' │ nginx — add inside your server {} block │');
1868
+ console.log(' │ File: /etc/nginx/sites-available/default │');
1869
+ console.log(' └─────────────────────────────────────────────────────────────────┘');
1870
+ console.log('');
1871
+ proxy.nginxConfig.split('\n').forEach(line => console.log(` ${line}`));
1872
+ console.log('');
1873
+ console.log(' To apply:');
1874
+ console.log(' 1. sudo nano /etc/nginx/sites-available/default');
1875
+ console.log(' 2. Add the config above inside your server { } block');
1876
+ console.log(' 3. sudo nginx -t');
1877
+ console.log(' 4. sudo systemctl reload nginx');
1697
1878
  }
1698
-
1699
- if (hasCaddy) {
1700
- console.log(`\n ┌─────────────────────────────────────────────────────────────────┐`);
1701
- console.log(` │ Caddy config │`);
1702
- console.log(` └─────────────────────────────────────────────────────────────────┘`);
1703
- console.log(``);
1704
- console.log(` # A2A (Agent-to-Agent) Protocol Proxy`);
1705
- console.log(` # Routes federation requests to local A2A server on port ${serverPort}`);
1706
- console.log(` # Protocol: https://github.com/onthegonow/a2a_calling`);
1707
- console.log(` handle /api/a2a/* {`);
1708
- console.log(` reverse_proxy 127.0.0.1:${serverPort}`);
1709
- console.log(` }`);
1879
+
1880
+ if (proxy.hasCaddy) {
1881
+ console.log('');
1882
+ console.log(' ┌─────────────────────────────────────────────────────────────────┐');
1883
+ console.log(' │ Caddy config │');
1884
+ console.log(' └─────────────────────────────────────────────────────────────────┘');
1885
+ console.log('');
1886
+ proxy.caddyConfig.split('\n').forEach(line => console.log(` ${line}`));
1710
1887
  }
1711
-
1712
- console.log(`\n After reverse proxy is configured:`);
1713
- console.log(` Invite hostname will be: ${externalIp} (no port needed)`);
1714
-
1715
- console.log(`\n ── Alternative: Direct firewall access ──`);
1716
- console.log(` Open port ${serverPort} directly (less recommended):`);
1717
- console.log(` sudo ufw allow ${serverPort}/tcp # Ubuntu/Debian`);
1718
- console.log(` Invite hostname will then be: ${publicHost}`);
1719
-
1720
- console.log(`\n ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
1721
- console.log(`\n AGENT ACTION REQUIRED:`);
1722
- console.log(` 1. Configure reverse proxy (recommended) add nginx/caddy config above`);
1723
- console.log(` 2. OR open firewall for port ${serverPort} — run the ufw/firewall-cmd command`);
1724
- console.log(` 3. OR skip (local-only, external agents cannot call)`);
1725
- console.log(`\n Ask your owner which option to use, then proceed.`);
1726
- console.log(` If reverse proxy is configured, update hostname with: a2a config --hostname ${externalIp}`);
1888
+
1889
+ if (!proxy.hasNginx && !proxy.hasCaddy) {
1890
+ console.log(' No nginx or Caddy detected. Install one:');
1891
+ console.log(' sudo apt install nginx # Debian/Ubuntu');
1892
+ console.log(' sudo yum install nginx # RHEL/CentOS');
1893
+ console.log('');
1894
+ console.log(' Then add this proxy config:');
1895
+ proxy.nginxConfig.split('\n').forEach(line => console.log(` ${line}`));
1896
+ }
1897
+
1898
+ // With reverse proxy, invite URLs use port 80 (no port in URL)
1899
+ console.log(`\n After applying, invite hostname will be: ${externalIp} (no port needed)`);
1900
+ publicHost = externalIp;
1901
+ } else {
1902
+ // User chose 'continue' on non-standard port brief reminder
1903
+ console.log(`\n ⚠ Running on port ${serverPort} (non-standard).`);
1904
+ console.log(` Invite hostname: ${publicHost}`);
1905
+ console.log(`\n To set up a reverse proxy later:`);
1906
+ console.log(` a2a config --hostname ${externalIp}`);
1907
+ console.log(` Then configure nginx/caddy to proxy port 80 → ${serverPort}.`);
1727
1908
  }
1728
-
1909
+
1729
1910
  const verifyUrl = `http://${publicHost}/api/a2a/ping`;
1730
1911
  console.log(`\n Verify: curl -s ${verifyUrl}`);
1731
1912
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.36",
3
+ "version": "0.6.37",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -128,6 +128,7 @@ Be concise but specific. No filler.`;
128
128
  async run(openingMessage) {
129
129
  const transcript = [];
130
130
  let conversationId = null;
131
+ let dbConversationStarted = false;
131
132
 
132
133
  const collabState = {
133
134
  phase: 'handshake',
@@ -140,13 +141,8 @@ Be concise but specific. No filler.`;
140
141
  confidence: 0.25
141
142
  };
142
143
 
143
- // Start conversation in DB if available
144
- if (this.convStore) {
145
- const convResult = this.convStore.startConversation({ direction: 'outbound' });
146
- conversationId = convResult.id;
147
- } else {
148
- conversationId = `conv_${Date.now()}_local`;
149
- }
144
+ // Don't start conversation in DB yet - wait for remote's conversation ID
145
+ conversationId = `conv_${Date.now()}_local`;
150
146
 
151
147
  let nextMessage = openingMessage;
152
148
 
@@ -166,26 +162,54 @@ Be concise but specific. No filler.`;
166
162
  break;
167
163
  }
168
164
 
169
- // Update conversation ID from remote if first turn
165
+ // Update conversation ID from remote if first turn and start DB conversation
170
166
  if (turn === 0 && remoteResponse.conversation_id) {
171
167
  conversationId = remoteResponse.conversation_id;
168
+
169
+ // Now start the conversation in DB with the remote's ID
170
+ if (this.convStore) {
171
+ const convResult = this.convStore.startConversation({
172
+ id: conversationId,
173
+ direction: 'outbound'
174
+ });
175
+ if (convResult.success === false) {
176
+ logger.warn('Failed to start conversation in DB', {
177
+ event: 'driver_start_conversation_failed',
178
+ error: convResult.error
179
+ });
180
+ } else {
181
+ dbConversationStarted = true;
182
+ }
183
+ }
172
184
  }
173
185
 
174
186
  const remoteText = remoteResponse.response || '';
175
187
  const remoteContinue = remoteResponse.can_continue !== false;
176
188
 
177
- // 2. Store messages in DB
178
- if (this.convStore) {
179
- this.convStore.addMessage(conversationId, {
189
+ // 2. Store messages in DB (only if conversation was started in DB)
190
+ if (this.convStore && dbConversationStarted) {
191
+ const outMsg = this.convStore.addMessage(conversationId, {
180
192
  direction: 'outbound',
181
193
  role: 'user',
182
194
  content: nextMessage
183
195
  });
184
- this.convStore.addMessage(conversationId, {
196
+ if (outMsg.success === false) {
197
+ logger.warn('Failed to save outbound message', {
198
+ event: 'driver_save_message_failed',
199
+ error: outMsg.error
200
+ });
201
+ }
202
+ const inMsg = this.convStore.addMessage(conversationId, {
185
203
  direction: 'inbound',
186
204
  role: 'assistant',
187
205
  content: remoteText
188
206
  });
207
+ if (inMsg.success === false) {
208
+ logger.warn('Failed to save inbound message', {
209
+ event: 'driver_save_message_failed',
210
+ error: inMsg.error
211
+ });
212
+ }
189
213
  }
190
214
 
191
215
  transcript.push(