a2acalling 0.5.3 → 0.5.4

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
@@ -208,6 +208,7 @@ Dashboard/API log routes:
208
208
  - `GET /api/a2a/dashboard/logs`
209
209
  - `GET /api/a2a/dashboard/logs/trace/:traceId`
210
210
  - `GET /api/a2a/dashboard/logs/stats`
211
+ - `GET /api/a2a/dashboard/debug/call?trace_id=<id>` (or `conversation_id=<id>`)
211
212
 
212
213
  Useful filters for `/api/a2a/dashboard/logs`:
213
214
 
@@ -221,6 +222,24 @@ Example:
221
222
  curl "http://localhost:3001/api/a2a/dashboard/logs?trace_id=trace_abc123&error_code=TOKEN_INVALID_OR_EXPIRED"
222
223
  ```
223
224
 
225
+ ### Incoming Call Debug
226
+
227
+ Every `/api/a2a/invoke` and `/api/a2a/end` response now returns:
228
+ - `trace_id` (generated when caller does not send one)
229
+ - `request_id` (generated when caller does not send one)
230
+
231
+ To inspect one call, use the dashboard debug endpoint:
232
+
233
+ ```bash
234
+ curl -H "x-admin-token: $A2A_ADMIN_TOKEN" \
235
+ "http://localhost:3001/api/a2a/dashboard/debug/call?trace_id=<trace_id>"
236
+ ```
237
+
238
+ For each call you get:
239
+ - `summary` (event count, first/last seen, duration, and IDs involved)
240
+ - `errors` and `error_codes` for fast triage
241
+ - `logs` (ordered timeline events from that trace)
242
+
224
243
  ## 📡 Protocol
225
244
 
226
245
  Tokens use the `a2a://` URI scheme:
package/docs/protocol.md CHANGED
@@ -56,6 +56,8 @@ Headers:
56
56
  ```
57
57
  Authorization: Bearer fed_abc123xyz
58
58
  Content-Type: application/json
59
+ x-trace-id: trace_... (optional)
60
+ x-request-id: req_... (optional)
59
61
  ```
60
62
 
61
63
  Request body:
@@ -76,6 +78,8 @@ Success response:
76
78
  ```json
77
79
  {
78
80
  "success": true,
81
+ "trace_id": "trace_...",
82
+ "request_id": "req_...",
79
83
  "conversation_id": "conv_123456",
80
84
  "response": "The agent's response text",
81
85
  "can_continue": true,
@@ -114,6 +118,8 @@ Success response:
114
118
  ```json
115
119
  {
116
120
  "success": true,
121
+ "trace_id": "trace_...",
122
+ "request_id": "req_...",
117
123
  "conversation_id": "conv_123456",
118
124
  "status": "concluded",
119
125
  "summary": "Optional summary text"
@@ -143,6 +149,7 @@ Dashboard API endpoints:
143
149
  - `GET /api/a2a/dashboard/logs`
144
150
  - `GET /api/a2a/dashboard/logs/trace/:traceId`
145
151
  - `GET /api/a2a/dashboard/logs/stats`
152
+ - `GET /api/a2a/dashboard/debug/call?trace_id=<id>`
146
153
 
147
154
  Example filters for `/logs`: `trace_id`, `conversation_id`, `token_id`, `error_code`, `status_code`, `component`, `event`, `level`, `search`, `from`, `to`.
148
155
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -77,6 +77,53 @@ function loadOpenClawConfig() {
77
77
  }
78
78
  }
79
79
 
80
+ function normalizeDashboardPluginEntry(rawEntry, backendUrl) {
81
+ const issues = [];
82
+ const normalized = {
83
+ enabled: true,
84
+ config: {}
85
+ };
86
+ const entry = (rawEntry && typeof rawEntry === 'object') ? rawEntry : {};
87
+
88
+ const legacyBackendUrl = (typeof entry.backendUrl === 'string' && entry.backendUrl.trim())
89
+ ? entry.backendUrl.trim()
90
+ : null;
91
+ const rawConfig = (entry.config && typeof entry.config === 'object') ? entry.config : null;
92
+
93
+ if (!entry || typeof entry !== 'object') {
94
+ issues.push('plugin entry is missing or invalid');
95
+ }
96
+
97
+ if (entry && typeof entry.enabled === 'boolean') {
98
+ normalized.enabled = entry.enabled;
99
+ }
100
+
101
+ if (rawConfig) {
102
+ normalized.config = { ...rawConfig };
103
+ } else if (entry.config !== undefined) {
104
+ issues.push('plugin entry has non-object config; replacing with empty object');
105
+ }
106
+
107
+ if (legacyBackendUrl) {
108
+ issues.push(`legacy key detected: plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl (using backendUrl migration)`);
109
+ normalized.config.backendUrl = backendUrl || legacyBackendUrl;
110
+ } else if (typeof normalized.config.backendUrl === 'string' && normalized.config.backendUrl.trim()) {
111
+ normalized.config.backendUrl = normalized.config.backendUrl.trim();
112
+ } else if (typeof backendUrl === 'string' && backendUrl.trim()) {
113
+ normalized.config.backendUrl = backendUrl.trim();
114
+ } else {
115
+ issues.push('backendUrl could not be determined; plugin may fail to route dashboard traffic');
116
+ }
117
+
118
+ return {
119
+ normalized,
120
+ issues,
121
+ changed: issues.length > 0,
122
+ legacyBackendUrl,
123
+ summary: `a2a-dashboard-proxy config => ${normalized.enabled ? 'enabled' : 'disabled'}, backendUrl=${normalized.config.backendUrl || 'missing'}`
124
+ };
125
+ }
126
+
80
127
  function writeOpenClawConfig(config) {
81
128
  const backupPath = `${OPENCLAW_CONFIG}.backup.${Date.now()}`;
82
129
  fs.copyFileSync(OPENCLAW_CONFIG, backupPath);
@@ -573,20 +620,17 @@ async function install() {
573
620
  config.plugins = config.plugins || {};
574
621
  config.plugins.entries = config.plugins.entries || {};
575
622
  const rawEntry = config.plugins.entries[DASHBOARD_PLUGIN_ID];
576
- const existingEntry = (rawEntry && typeof rawEntry === 'object') ? rawEntry : {};
577
- const existingConfig = (existingEntry.config && typeof existingEntry.config === 'object')
578
- ? existingEntry.config
579
- : {};
580
- if (typeof existingEntry.backendUrl === 'string' && existingEntry.backendUrl) {
581
- log(`Migrated legacy plugin key plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl -> plugins.entries.${DASHBOARD_PLUGIN_ID}.config.backendUrl`);
623
+ const audit = normalizeDashboardPluginEntry(rawEntry, backendUrl);
624
+ for (const issue of audit.issues) {
625
+ warn(`a2a-dashboard-proxy config issue: ${issue}`);
582
626
  }
583
- config.plugins.entries[DASHBOARD_PLUGIN_ID] = {
584
- enabled: true,
585
- config: {
586
- ...existingConfig,
587
- backendUrl
588
- }
589
- };
627
+ if (audit.legacyBackendUrl) {
628
+ warn(`Auto-fixing legacy key: plugins.entries.${DASHBOARD_PLUGIN_ID}.backendUrl`);
629
+ }
630
+ if (audit.changed) {
631
+ log(`Migrated dashboard plugin config: ${audit.summary}`);
632
+ }
633
+ config.plugins.entries[DASHBOARD_PLUGIN_ID] = audit.normalized;
590
634
  configUpdated = true;
591
635
  log(`Configured gateway plugin entry: ${DASHBOARD_PLUGIN_ID}`);
592
636
  }
@@ -34,6 +34,15 @@ function cleanText(value, maxLength = 300) {
34
34
  .slice(0, maxLength);
35
35
  }
36
36
 
37
+ function normalizeCommandText(command) {
38
+ return String(command || '').trim().slice(0, 160);
39
+ }
40
+
41
+ function payloadAuditLength(payload) {
42
+ const raw = JSON.stringify(payload || {});
43
+ return Number.isFinite(raw?.length) ? raw.length : 0;
44
+ }
45
+
37
46
  function toBool(value, fallback = true) {
38
47
  if (value === undefined || value === null || value === '') {
39
48
  return fallback;
@@ -326,6 +335,21 @@ function createRuntimeAdapter(options = {}) {
326
335
  caller: caller || {},
327
336
  context: context || {}
328
337
  };
338
+ const traceId = context?.traceId || context?.trace_id;
339
+ const requestId = context?.requestId || context?.request_id;
340
+ const conversationId = context?.conversationId || context?.conversation_id;
341
+ const startAt = Date.now();
342
+
343
+ logger.debug('Invoking generic agent command', {
344
+ event: 'generic_agent_command_start',
345
+ traceId,
346
+ requestId,
347
+ conversationId,
348
+ data: {
349
+ command: normalizeCommandText(genericAgentCommand),
350
+ payload_bytes: payloadAuditLength(payload)
351
+ }
352
+ });
329
353
 
330
354
  if (genericAgentCommand) {
331
355
  try {
@@ -333,6 +357,17 @@ function createRuntimeAdapter(options = {}) {
333
357
  timeoutMs: context?.timeoutMs || 65000
334
358
  });
335
359
  const text = parseCommandTextOutput(output);
360
+ logger.debug('Generic agent command completed', {
361
+ event: 'generic_agent_command_complete',
362
+ traceId,
363
+ requestId,
364
+ conversationId,
365
+ data: {
366
+ command: normalizeCommandText(genericAgentCommand),
367
+ duration_ms: Date.now() - startAt,
368
+ output_length: String(output || '').length
369
+ }
370
+ });
336
371
  if (text) {
337
372
  return text;
338
373
  }
@@ -340,13 +375,17 @@ function createRuntimeAdapter(options = {}) {
340
375
  runtimeError = err.message;
341
376
  logger.error('Generic agent command failed', {
342
377
  event: 'generic_agent_command_failed',
343
- traceId: context?.traceId,
344
- conversationId: context?.conversationId,
378
+ traceId,
379
+ requestId,
380
+ conversationId,
345
381
  error_code: 'GENERIC_AGENT_COMMAND_FAILED',
346
382
  hint: 'Verify A2A_AGENT_COMMAND exits 0 and returns valid text/JSON response.',
347
383
  error: err,
348
384
  data: {
349
- command_present: Boolean(genericAgentCommand)
385
+ command_present: Boolean(genericAgentCommand),
386
+ command: normalizeCommandText(genericAgentCommand),
387
+ payload_bytes: payloadAuditLength(payload),
388
+ duration_ms: Date.now() - startAt
350
389
  }
351
390
  });
352
391
  }
@@ -366,11 +405,26 @@ function createRuntimeAdapter(options = {}) {
366
405
  messages,
367
406
  caller: callerInfo || {}
368
407
  };
408
+ const traceId = callerInfo?.trace_id || callerInfo?.traceId;
409
+ const requestId = callerInfo?.request_id || callerInfo?.requestId;
410
+ const conversationId = callerInfo?.conversation_id || callerInfo?.conversationId;
411
+ const startAt = Date.now();
369
412
 
370
413
  if (genericSummaryCommand) {
371
414
  try {
372
415
  const output = runCommand(genericSummaryCommand, payload, { timeoutMs: 35000 });
373
416
  const parsed = parseSummaryOutput(output);
417
+ logger.debug('Generic summary command completed', {
418
+ event: 'generic_summary_command_complete',
419
+ traceId,
420
+ requestId,
421
+ conversationId,
422
+ data: {
423
+ command: normalizeCommandText(genericSummaryCommand),
424
+ payload_bytes: payloadAuditLength(payload),
425
+ output_length: String(output || '').length
426
+ }
427
+ });
374
428
  if (parsed && parsed.summary) {
375
429
  return parsed;
376
430
  }
@@ -378,13 +432,17 @@ function createRuntimeAdapter(options = {}) {
378
432
  reason = err.message;
379
433
  logger.error('Generic summary command failed', {
380
434
  event: 'generic_summary_command_failed',
381
- traceId: callerInfo?.trace_id || callerInfo?.traceId,
382
- conversationId: callerInfo?.conversation_id || callerInfo?.conversationId,
435
+ traceId,
436
+ requestId,
437
+ conversationId,
383
438
  error_code: 'GENERIC_SUMMARY_COMMAND_FAILED',
384
439
  hint: 'Verify A2A_SUMMARY_COMMAND returns JSON with summary field or plain text.',
385
440
  error: err,
386
441
  data: {
387
- command_present: Boolean(genericSummaryCommand)
442
+ command_present: Boolean(genericSummaryCommand),
443
+ command: normalizeCommandText(genericSummaryCommand),
444
+ payload_bytes: payloadAuditLength(payload),
445
+ duration_ms: Date.now() - startAt
388
446
  }
389
447
  });
390
448
  }
@@ -397,19 +455,47 @@ function createRuntimeAdapter(options = {}) {
397
455
  if (!genericNotifyCommand) {
398
456
  return;
399
457
  }
458
+ const traceId = payload?.trace_id || payload?.traceId;
459
+ const requestId = payload?.request_id || payload?.requestId;
460
+ const conversationId = payload?.conversationId;
461
+ const startAt = Date.now();
462
+ logger.debug('Invoking generic notify command', {
463
+ event: 'generic_notify_command_start',
464
+ traceId,
465
+ requestId,
466
+ conversationId,
467
+ data: {
468
+ command: normalizeCommandText(genericNotifyCommand),
469
+ payload_bytes: payloadAuditLength(payload)
470
+ }
471
+ });
400
472
  try {
401
473
  runCommand(genericNotifyCommand, payload, { timeoutMs: 10000 });
474
+ logger.debug('Generic notify command completed', {
475
+ event: 'generic_notify_command_complete',
476
+ traceId,
477
+ requestId,
478
+ conversationId,
479
+ data: {
480
+ command: normalizeCommandText(genericNotifyCommand),
481
+ duration_ms: Date.now() - startAt
482
+ }
483
+ });
402
484
  } catch (err) {
403
485
  logger.error('Generic notify command failed', {
404
486
  event: 'generic_notify_command_failed',
405
- traceId: payload?.trace_id || payload?.traceId,
406
- conversationId: payload?.conversationId,
487
+ traceId,
488
+ requestId,
489
+ conversationId,
407
490
  tokenId: payload?.token?.id,
408
491
  error_code: 'GENERIC_NOTIFY_COMMAND_FAILED',
409
492
  hint: 'Validate A2A_NOTIFY_COMMAND and downstream notifier transport availability.',
410
493
  error: err,
411
494
  data: {
412
- command_present: Boolean(genericNotifyCommand)
495
+ command_present: Boolean(genericNotifyCommand),
496
+ command: normalizeCommandText(genericNotifyCommand),
497
+ payload_bytes: payloadAuditLength(payload),
498
+ duration_ms: Date.now() - startAt
413
499
  }
414
500
  });
415
501
  }
@@ -417,24 +503,65 @@ function createRuntimeAdapter(options = {}) {
417
503
 
418
504
  async function runTurn({ sessionId, prompt, message, caller, context = {}, timeoutMs }) {
419
505
  const traceId = context?.traceId || context?.trace_id;
506
+ const requestId = context?.requestId || context?.request_id;
507
+ const conversationId = context?.conversationId || context?.conversation_id;
420
508
  if (modeInfo.mode !== 'openclaw') {
421
509
  return runGenericTurn({ message, caller, context });
422
510
  }
423
511
 
512
+ const startAt = Date.now();
513
+ logger.debug('Invoking openclaw turn', {
514
+ event: 'openclaw_turn_start',
515
+ traceId,
516
+ requestId,
517
+ conversationId,
518
+ data: {
519
+ session_id: sessionId,
520
+ timeout_ms: timeoutMs
521
+ }
522
+ });
523
+
424
524
  try {
425
- return await runOpenClawTurn({ sessionId, prompt, timeoutMs });
525
+ const response = await runOpenClawTurn({ sessionId, prompt, timeoutMs });
526
+ logger.debug('OpenClaw turn completed', {
527
+ event: 'openclaw_turn_complete',
528
+ traceId,
529
+ requestId,
530
+ conversationId,
531
+ data: {
532
+ session_id: sessionId,
533
+ duration_ms: Date.now() - startAt
534
+ }
535
+ });
536
+ return response;
426
537
  } catch (err) {
427
538
  if (!failoverEnabled) {
539
+ logger.error('OpenClaw turn failed', {
540
+ event: 'openclaw_turn_failed',
541
+ traceId,
542
+ requestId,
543
+ conversationId,
544
+ error_code: 'OPENCLAW_TURN_FAILED',
545
+ hint: 'Inspect OpenClaw CLI output, timeout settings, and environment PATH.',
546
+ error: err,
547
+ data: {
548
+ session_id: sessionId,
549
+ timeout_ms: timeoutMs,
550
+ duration_ms: Date.now() - startAt
551
+ }
552
+ });
428
553
  throw err;
429
554
  }
430
555
  logger.warn('OpenClaw runtime failed, switching to generic fallback', {
431
556
  event: 'openclaw_turn_failed_fallback',
432
557
  traceId,
433
- conversationId: context?.conversationId,
558
+ requestId,
559
+ conversationId,
434
560
  error_code: 'OPENCLAW_TURN_FAILED_FALLBACK',
435
561
  hint: 'Inspect OpenClaw CLI health or set A2A_RUNTIME=generic for explicit fallback mode.',
436
562
  error: err,
437
563
  data: {
564
+ duration_ms: Date.now() - startAt,
438
565
  failover_enabled: failoverEnabled
439
566
  }
440
567
  });
@@ -448,9 +575,23 @@ function createRuntimeAdapter(options = {}) {
448
575
  }
449
576
 
450
577
  async function summarize({ sessionId, prompt, messages, callerInfo, traceId, conversationId }) {
578
+ const effectiveTraceId = traceId || callerInfo?.trace_id || callerInfo?.traceId;
579
+ const requestId = callerInfo?.request_id || callerInfo?.requestId;
580
+ const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
451
581
  if (modeInfo.mode !== 'openclaw') {
452
582
  return runGenericSummary({ messages, callerInfo });
453
583
  }
584
+ const startAt = Date.now();
585
+ logger.debug('Invoking openclaw summary', {
586
+ event: 'openclaw_summary_start',
587
+ traceId: effectiveTraceId,
588
+ requestId,
589
+ conversationId: effectiveConversationId,
590
+ data: {
591
+ session_id: sessionId,
592
+ message_count: Array.isArray(messages) ? messages.length : 0
593
+ }
594
+ });
454
595
 
455
596
  try {
456
597
  const result = await runOpenClawSummary({
@@ -459,8 +600,28 @@ function createRuntimeAdapter(options = {}) {
459
600
  timeoutMs: 35000
460
601
  });
461
602
  if (result && result.summary) {
603
+ logger.debug('OpenClaw summary completed', {
604
+ event: 'openclaw_summary_complete',
605
+ traceId: effectiveTraceId,
606
+ requestId,
607
+ conversationId: effectiveConversationId,
608
+ data: {
609
+ session_id: sessionId,
610
+ duration_ms: Date.now() - startAt
611
+ }
612
+ });
462
613
  return result;
463
614
  }
615
+ logger.warn('OpenClaw summary returned empty output; using generic fallback', {
616
+ event: 'openclaw_summary_empty',
617
+ traceId: effectiveTraceId,
618
+ requestId,
619
+ conversationId: effectiveConversationId,
620
+ data: {
621
+ session_id: sessionId,
622
+ duration_ms: Date.now() - startAt
623
+ }
624
+ });
464
625
  return runGenericSummary({
465
626
  messages,
466
627
  callerInfo,
@@ -468,16 +629,32 @@ function createRuntimeAdapter(options = {}) {
468
629
  });
469
630
  } catch (err) {
470
631
  if (!failoverEnabled) {
632
+ logger.error('OpenClaw summary failed', {
633
+ event: 'openclaw_summary_failed',
634
+ traceId: effectiveTraceId,
635
+ requestId,
636
+ conversationId: effectiveConversationId,
637
+ error_code: 'OPENCLAW_SUMMARY_FAILED',
638
+ hint: 'Inspect summary message length, timeout configuration, and CLI stderr output.',
639
+ error: err,
640
+ data: {
641
+ session_id: sessionId,
642
+ duration_ms: Date.now() - startAt
643
+ }
644
+ });
471
645
  throw err;
472
646
  }
473
647
  logger.warn('OpenClaw summary failed, using generic fallback', {
474
648
  event: 'openclaw_summary_failed_fallback',
475
- traceId: traceId || callerInfo?.trace_id || callerInfo?.traceId,
476
- conversationId: conversationId || callerInfo?.conversation_id || callerInfo?.conversationId,
649
+ traceId: effectiveTraceId,
650
+ requestId,
651
+ conversationId: effectiveConversationId,
477
652
  error_code: 'OPENCLAW_SUMMARY_FAILED_FALLBACK',
478
653
  hint: 'Inspect OpenClaw summary session output and summarizer prompt input.',
479
654
  error: err,
480
655
  data: {
656
+ session_id: sessionId,
657
+ duration_ms: Date.now() - startAt,
481
658
  failover_enabled: failoverEnabled
482
659
  }
483
660
  });
@@ -490,6 +667,7 @@ function createRuntimeAdapter(options = {}) {
490
667
  }
491
668
 
492
669
  async function notify({ level, token, caller, message, conversationId, traceId }) {
670
+ const requestId = token?.request_id || token?.requestId || null;
493
671
  const payload = {
494
672
  mode: 'a2a-notify',
495
673
  level,
@@ -497,9 +675,19 @@ function createRuntimeAdapter(options = {}) {
497
675
  caller: caller || null,
498
676
  message,
499
677
  conversationId,
500
- traceId
678
+ traceId,
679
+ requestId
501
680
  };
502
681
 
682
+ logger.debug('Owner notify requested', {
683
+ event: 'notify_requested',
684
+ traceId,
685
+ requestId,
686
+ conversationId,
687
+ tokenId: token?.id,
688
+ data: { level }
689
+ });
690
+
503
691
  if (modeInfo.mode !== 'openclaw') {
504
692
  return runGenericNotify(payload);
505
693
  }
@@ -510,9 +698,20 @@ function createRuntimeAdapter(options = {}) {
510
698
 
511
699
  const callerName = caller?.name || 'Unknown';
512
700
  const callerOwner = caller?.owner ? ` (${caller.owner})` : '';
701
+ const notifyStart = Date.now();
513
702
 
514
703
  try {
515
704
  await runOpenClawNotify({ callerName, callerOwner, message: message || '' });
705
+ logger.debug('OpenClaw notify completed', {
706
+ event: 'openclaw_notify_complete',
707
+ traceId,
708
+ requestId,
709
+ conversationId,
710
+ tokenId: token?.id,
711
+ data: {
712
+ duration_ms: Date.now() - notifyStart
713
+ }
714
+ });
516
715
  } catch (err) {
517
716
  if (!failoverEnabled) {
518
717
  throw err;
@@ -520,15 +719,24 @@ function createRuntimeAdapter(options = {}) {
520
719
  logger.warn('OpenClaw notify failed, running generic notifier', {
521
720
  event: 'openclaw_notify_failed_fallback',
522
721
  traceId,
722
+ requestId,
523
723
  conversationId,
524
724
  tokenId: token?.id,
525
725
  error_code: 'OPENCLAW_NOTIFY_FAILED_FALLBACK',
526
726
  hint: 'Check OpenClaw messaging channel config and notify permissions.',
527
727
  error: err,
528
728
  data: {
529
- failover_enabled: failoverEnabled
729
+ failover_enabled: failoverEnabled,
730
+ duration_ms: Date.now() - notifyStart
530
731
  }
531
732
  });
733
+ logger.debug('OpenClaw notify fallback to generic notifier', {
734
+ event: 'openclaw_notify_generic_fallback',
735
+ traceId,
736
+ requestId,
737
+ conversationId,
738
+ tokenId: token?.id
739
+ });
532
740
  await runGenericNotify(payload);
533
741
  }
534
742
  }
package/src/routes/a2a.js CHANGED
@@ -74,6 +74,32 @@ function resolveTraceId(req) {
74
74
  return createTraceId('a2a');
75
75
  }
76
76
 
77
+ function resolveRequestId(req) {
78
+ const headerRequestId = req.headers['x-request-id'];
79
+ if (typeof headerRequestId === 'string' && headerRequestId.trim()) {
80
+ return headerRequestId.trim().slice(0, 120);
81
+ }
82
+ return createTraceId('req');
83
+ }
84
+
85
+ function extractClientHost(req) {
86
+ const forwarded = req.headers['x-forwarded-for'];
87
+ if (typeof forwarded === 'string' && forwarded.trim()) {
88
+ return forwarded.split(',')[0].trim();
89
+ }
90
+ return req.ip || null;
91
+ }
92
+
93
+ function normalizeRequestMetadata(req) {
94
+ const body = req && typeof req.body === 'object' && req.body ? req.body : {};
95
+ return {
96
+ has_message: typeof body.message === 'string',
97
+ has_caller: Boolean(body.caller && typeof body.caller === 'object'),
98
+ has_context: Boolean(body.context && typeof body.context === 'object'),
99
+ timeout_seconds: body.timeout_seconds
100
+ };
101
+ }
102
+
77
103
  function checkRateLimit(tokenId, limits = { minute: 10, hour: 100, day: 1000 }) {
78
104
  const now = Date.now();
79
105
  const minute = Math.floor(now / 60000);
@@ -177,14 +203,25 @@ function createRoutes(options = {}) {
177
203
  router.post('/invoke', async (req, res) => {
178
204
  const startedAt = Date.now();
179
205
  const traceId = resolveTraceId(req);
180
- const reqLogger = logger.child({ traceId, event: 'invoke' });
206
+ const requestId = resolveRequestId(req);
207
+ const reqLogger = logger.child({ traceId, requestId, event: 'invoke' });
208
+ const withTracePayload = (payload) => ({ ...payload, trace_id: traceId, request_id: requestId });
181
209
  res.set('x-trace-id', traceId);
210
+ res.set('x-request-id', requestId);
182
211
  reqLogger.info('Received invoke request', {
183
212
  data: {
184
213
  ip: req.ip,
214
+ request_id: requestId,
215
+ client_host: extractClientHost(req),
216
+ forwarded_for: req.headers['x-forwarded-for'] || null,
217
+ user_agent: req.headers['user-agent'] || null,
185
218
  has_auth_header: Boolean(req.headers.authorization)
186
219
  }
187
220
  });
221
+ reqLogger.debug('Invoke request metadata', {
222
+ event: 'invoke_request_metadata',
223
+ data: normalizeRequestMetadata(req)
224
+ });
188
225
 
189
226
  // Extract token
190
227
  const authHeader = req.headers.authorization;
@@ -194,11 +231,11 @@ function createRoutes(options = {}) {
194
231
  status_code: 401,
195
232
  hint: 'Send Authorization: Bearer <a2a_token>.'
196
233
  });
197
- return res.status(401).json({
234
+ return res.status(401).json(withTracePayload({
198
235
  success: false,
199
236
  error: 'missing_token',
200
237
  message: 'Authorization header required'
201
- });
238
+ }));
202
239
  }
203
240
 
204
241
  const token = authHeader.slice(7);
@@ -213,11 +250,11 @@ function createRoutes(options = {}) {
213
250
  status_code: 401,
214
251
  hint: 'Create a fresh invite token and retry with the new bearer token.'
215
252
  });
216
- return res.status(401).json({
253
+ return res.status(401).json(withTracePayload({
217
254
  success: false,
218
255
  error: 'unauthorized',
219
256
  message: 'Invalid or expired token'
220
- });
257
+ }));
221
258
  }
222
259
 
223
260
  // Check rate limit
@@ -233,11 +270,11 @@ function createRoutes(options = {}) {
233
270
  }
234
271
  });
235
272
  res.set('Retry-After', rateCheck.retryAfter);
236
- return res.status(429).json({
273
+ return res.status(429).json(withTracePayload({
237
274
  success: false,
238
275
  error: rateCheck.error,
239
276
  message: rateCheck.message
240
- });
277
+ }));
241
278
  }
242
279
 
243
280
  // Extract and validate request
@@ -250,11 +287,11 @@ function createRoutes(options = {}) {
250
287
  status_code: 400,
251
288
  hint: 'Include a non-empty string field `message` in the request body.'
252
289
  });
253
- return res.status(400).json({
290
+ return res.status(400).json(withTracePayload({
254
291
  success: false,
255
292
  error: 'missing_message',
256
293
  message: 'Message is required'
257
- });
294
+ }));
258
295
  }
259
296
 
260
297
  // Validate message length
@@ -269,11 +306,11 @@ function createRoutes(options = {}) {
269
306
  message_length: typeof message === 'string' ? message.length : null
270
307
  }
271
308
  });
272
- return res.status(400).json({
309
+ return res.status(400).json(withTracePayload({
273
310
  success: false,
274
311
  error: 'invalid_message',
275
312
  message: `Message must be a string under ${MAX_MESSAGE_LENGTH} characters`
276
- });
313
+ }));
277
314
  }
278
315
 
279
316
  // Validate and bound timeout
@@ -299,7 +336,8 @@ function createRoutes(options = {}) {
299
336
  disclosure: validation.disclosure,
300
337
  caller: sanitizedCaller,
301
338
  conversation_id: conversation_id || `conv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
302
- trace_id: traceId
339
+ trace_id: traceId,
340
+ request_id: requestId
303
341
  };
304
342
 
305
343
  // Track conversation if store available
@@ -317,7 +355,8 @@ function createRoutes(options = {}) {
317
355
  if (monitor) {
318
356
  monitor.trackActivity(a2aContext.conversation_id, {
319
357
  ...sanitizedCaller,
320
- trace_id: traceId
358
+ trace_id: traceId,
359
+ request_id: requestId
321
360
  });
322
361
  }
323
362
 
@@ -377,7 +416,8 @@ function createRoutes(options = {}) {
377
416
  message,
378
417
  response: response.text,
379
418
  conversation_id: a2aContext.conversation_id,
380
- trace_id: traceId
419
+ trace_id: traceId,
420
+ request_id: requestId
381
421
  }).catch(err => {
382
422
  reqLogger.error('Failed to notify owner', {
383
423
  conversationId: a2aContext.conversation_id,
@@ -395,6 +435,7 @@ function createRoutes(options = {}) {
395
435
  reqLogger.info('Invoke request completed', {
396
436
  conversationId: a2aContext.conversation_id,
397
437
  tokenId: validation.id,
438
+ requestId,
398
439
  data: {
399
440
  duration_ms: Date.now() - startedAt,
400
441
  message_length: message.length,
@@ -404,6 +445,8 @@ function createRoutes(options = {}) {
404
445
 
405
446
  res.json({
406
447
  success: true,
448
+ trace_id: traceId,
449
+ request_id: requestId,
407
450
  conversation_id: a2aContext.conversation_id,
408
451
  response: response.text,
409
452
  can_continue: response.canContinue !== false,
@@ -422,11 +465,11 @@ function createRoutes(options = {}) {
422
465
  duration_ms: Date.now() - startedAt
423
466
  }
424
467
  });
425
- res.status(500).json({
468
+ res.status(500).json(withTracePayload({
426
469
  success: false,
427
470
  error: 'internal_error',
428
471
  message: 'Failed to process message'
429
- });
472
+ }));
430
473
  }
431
474
  });
432
475
 
@@ -437,8 +480,20 @@ function createRoutes(options = {}) {
437
480
  router.post('/end', async (req, res) => {
438
481
  const startedAt = Date.now();
439
482
  const traceId = resolveTraceId(req);
440
- const reqLogger = logger.child({ traceId, event: 'end' });
483
+ const requestId = resolveRequestId(req);
484
+ const reqLogger = logger.child({ traceId, requestId, event: 'end' });
485
+ const withTracePayload = (payload) => ({ ...payload, trace_id: traceId, request_id: requestId });
441
486
  res.set('x-trace-id', traceId);
487
+ res.set('x-request-id', requestId);
488
+ reqLogger.info('Received end request', {
489
+ data: {
490
+ request_id: requestId,
491
+ ip: req.ip,
492
+ client_host: extractClientHost(req),
493
+ has_auth_header: Boolean(req.headers.authorization),
494
+ has_conversation_id: Boolean(req.body && req.body.conversation_id)
495
+ }
496
+ });
442
497
 
443
498
  // Extract token
444
499
  const authHeader = req.headers.authorization;
@@ -448,11 +503,11 @@ function createRoutes(options = {}) {
448
503
  status_code: 401,
449
504
  hint: 'Send Authorization: Bearer <a2a_token>.'
450
505
  });
451
- return res.status(401).json({
506
+ return res.status(401).json(withTracePayload({
452
507
  success: false,
453
508
  error: 'unauthorized',
454
509
  message: 'Authorization header required'
455
- });
510
+ }));
456
511
  }
457
512
 
458
513
  const token = authHeader.slice(7);
@@ -463,11 +518,11 @@ function createRoutes(options = {}) {
463
518
  status_code: 401,
464
519
  hint: 'Use a currently valid invite token for conversation end calls.'
465
520
  });
466
- return res.status(401).json({
521
+ return res.status(401).json(withTracePayload({
467
522
  success: false,
468
523
  error: 'unauthorized',
469
524
  message: 'Invalid or expired token'
470
- });
525
+ }));
471
526
  }
472
527
 
473
528
  const { conversation_id } = req.body;
@@ -478,16 +533,16 @@ function createRoutes(options = {}) {
478
533
  status_code: 400,
479
534
  hint: 'Provide `conversation_id` returned from /invoke.'
480
535
  });
481
- return res.status(400).json({
536
+ return res.status(400).json(withTracePayload({
482
537
  success: false,
483
538
  error: 'missing_conversation_id',
484
539
  message: 'conversation_id is required'
485
- });
540
+ }));
486
541
  }
487
542
 
488
543
  const convStore = getConversationStore();
489
544
  if (!convStore) {
490
- return res.json({ success: true, message: 'Conversation storage not enabled' });
545
+ return res.json(withTracePayload({ success: true, message: 'Conversation storage not enabled' }));
491
546
  }
492
547
 
493
548
  try {
@@ -508,7 +563,8 @@ function createRoutes(options = {}) {
508
563
  type: 'conversation_concluded',
509
564
  token: validation,
510
565
  conversation: conv,
511
- trace_id: traceId
566
+ trace_id: traceId,
567
+ request_id: requestId
512
568
  }).catch(err => {
513
569
  reqLogger.error('Failed to notify owner after conversation end', {
514
570
  conversationId: conversation_id,
@@ -526,6 +582,7 @@ function createRoutes(options = {}) {
526
582
  reqLogger.info('End request completed', {
527
583
  conversationId: conversation_id,
528
584
  tokenId: validation.id,
585
+ requestId,
529
586
  data: {
530
587
  duration_ms: Date.now() - startedAt,
531
588
  status: result.success ? 'concluded' : 'unchanged'
@@ -534,6 +591,8 @@ function createRoutes(options = {}) {
534
591
 
535
592
  res.json({
536
593
  success: true,
594
+ trace_id: traceId,
595
+ request_id: requestId,
537
596
  conversation_id,
538
597
  status: 'concluded',
539
598
  summary: result.summary
@@ -550,11 +609,11 @@ function createRoutes(options = {}) {
550
609
  duration_ms: Date.now() - startedAt
551
610
  }
552
611
  });
553
- res.status(500).json({
612
+ res.status(500).json(withTracePayload({
554
613
  success: false,
555
614
  error: 'internal_error',
556
615
  message: 'Failed to end conversation'
557
- });
616
+ }));
558
617
  }
559
618
  });
560
619
 
@@ -185,6 +185,69 @@ function ensureDashboardAccess(req, res, next) {
185
185
  return res.status(401).json({ success: false, error: 'unauthorized', message: 'Admin token required' });
186
186
  }
187
187
 
188
+ function summarizeDebugLogs(logs) {
189
+ if (!Array.isArray(logs) || logs.length === 0) {
190
+ return {
191
+ event_count: 0,
192
+ events: [],
193
+ timestamps: [],
194
+ error_count: 0,
195
+ warning_count: 0
196
+ };
197
+ }
198
+
199
+ const normalized = logs.slice().sort((a, b) => {
200
+ const aTime = Number(new Date(a.timestamp));
201
+ const bTime = Number(new Date(b.timestamp));
202
+ if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
203
+ if (Number.isNaN(aTime)) return 1;
204
+ if (Number.isNaN(bTime)) return -1;
205
+ return aTime - bTime;
206
+ });
207
+
208
+ const firstTimestamp = normalized[0]?.timestamp || null;
209
+ const lastTimestamp = normalized[normalized.length - 1]?.timestamp || null;
210
+ const firstMs = firstTimestamp ? Number(new Date(firstTimestamp)) : null;
211
+ const lastMs = lastTimestamp ? Number(new Date(lastTimestamp)) : null;
212
+
213
+ return {
214
+ event_count: normalized.length,
215
+ events: normalized.map(row => row.event).filter(Boolean),
216
+ timestamps: normalized.map(row => row.timestamp),
217
+ first_seen: firstTimestamp,
218
+ last_seen: lastTimestamp,
219
+ timeline_ms: Number.isFinite(firstMs) && Number.isFinite(lastMs) && lastMs >= firstMs
220
+ ? lastMs - firstMs
221
+ : null,
222
+ trace_ids: [...new Set(normalized.map(row => row.trace_id).filter(Boolean))],
223
+ conversation_ids: [...new Set(normalized.map(row => row.conversation_id).filter(Boolean))],
224
+ token_ids: [...new Set(normalized.map(row => row.token_id).filter(Boolean))],
225
+ request_ids: [...new Set(normalized.map(row => row.request_id).filter(Boolean))],
226
+ error_count: normalized.filter(row => row.level === 'error').length,
227
+ warning_count: normalized.filter(row => row.level === 'warn').length,
228
+ hints: [...new Set(normalized
229
+ .filter(row => row.hint)
230
+ .map(row => String(row.hint)))]
231
+ };
232
+ }
233
+
234
+ function findErrorHints(logs) {
235
+ const hints = new Map();
236
+ for (const row of logs) {
237
+ if (!row.error_code) continue;
238
+ if (!hints.has(row.error_code)) {
239
+ hints.set(row.error_code, {
240
+ error_code: row.error_code,
241
+ status_code: row.status_code,
242
+ count: 0
243
+ });
244
+ }
245
+ const existing = hints.get(row.error_code);
246
+ existing.count += 1;
247
+ }
248
+ return Array.from(hints.values());
249
+ }
250
+
188
251
  function createDashboardApiRouter(options = {}) {
189
252
  const router = express.Router();
190
253
  const context = buildContext(options);
@@ -239,6 +302,65 @@ function createDashboardApiRouter(options = {}) {
239
302
  return res.json({ success: true, stats });
240
303
  });
241
304
 
305
+ router.get('/debug/call', (req, res) => {
306
+ const traceId = sanitizeString(req.query.trace_id || req.query.traceId || '', 120);
307
+ const conversationId = sanitizeString(req.query.conversation_id || req.query.conversationId || '', 120);
308
+ const limit = Math.min(1000, Math.max(1, Number.parseInt(req.query.limit || '500', 10) || 500));
309
+
310
+ if (!traceId && !conversationId) {
311
+ return res.status(400).json({
312
+ success: false,
313
+ error: 'missing_scope',
314
+ message: 'Provide trace_id or conversation_id.'
315
+ });
316
+ }
317
+
318
+ const logs = traceId
319
+ ? context.logger.getTrace(traceId, { limit })
320
+ : context.logger.list({
321
+ limit,
322
+ conversationId,
323
+ sort_desc: false
324
+ });
325
+
326
+ if (!Array.isArray(logs) || logs.length === 0) {
327
+ return res.status(404).json({
328
+ success: false,
329
+ error: 'no_logs_found',
330
+ message: traceId ? 'No logs for trace_id' : 'No logs for conversation_id'
331
+ });
332
+ }
333
+
334
+ const summary = summarizeDebugLogs(logs);
335
+ const errors = logs.filter(row => row.level === 'error' || row.level === 'warn').slice(0, 20);
336
+ const callSignature = {
337
+ trace_ids: summary.trace_ids,
338
+ conversation_ids: summary.conversation_ids,
339
+ token_ids: summary.token_ids,
340
+ request_ids: summary.request_ids
341
+ };
342
+
343
+ return res.json({
344
+ success: true,
345
+ summary: {
346
+ ...summary,
347
+ ...callSignature,
348
+ errors: errors.map(row => ({
349
+ id: row.id,
350
+ timestamp: row.timestamp,
351
+ level: row.level,
352
+ event: row.event,
353
+ message: row.message,
354
+ error_code: row.error_code,
355
+ status_code: row.status_code,
356
+ hint: row.hint
357
+ })),
358
+ error_codes: findErrorHints(logs)
359
+ },
360
+ logs: summary.timeline_ms === null ? logs : logs
361
+ });
362
+ });
363
+
242
364
  router.get('/contacts', (req, res) => {
243
365
  const contacts = context.tokenStore.listRemotes();
244
366
  const contactIndex = buildContactIndex(contacts);
package/src/server.js CHANGED
@@ -454,8 +454,10 @@ async function callAgent(message, a2aContext) {
454
454
  const tierInfo = a2aContext.tier || 'public';
455
455
  const conversationId = a2aContext.conversation_id || `conv_${Date.now()}`;
456
456
  const traceId = a2aContext.trace_id || null;
457
+ const requestId = a2aContext.request_id || null;
457
458
  const callLogger = logger.child({
458
459
  traceId,
460
+ requestId,
459
461
  conversationId,
460
462
  tokenId: a2aContext.token_id
461
463
  });
@@ -528,20 +530,21 @@ async function callAgent(message, a2aContext) {
528
530
  }
529
531
  });
530
532
 
531
- const rawResponse = await runtime.runTurn({
532
- sessionId,
533
- prompt,
534
- message,
535
- caller: a2aContext.caller || {},
536
- timeoutMs: 65000,
537
- context: {
538
- conversationId,
539
- tier: tierInfo,
540
- ownerName: agentContext.owner,
541
- allowedTopics: a2aContext.allowed_topics || [],
542
- traceId
543
- }
544
- });
533
+ const rawResponse = await runtime.runTurn({
534
+ sessionId,
535
+ prompt,
536
+ message,
537
+ caller: a2aContext.caller || {},
538
+ timeoutMs: 65000,
539
+ context: {
540
+ conversationId,
541
+ tier: tierInfo,
542
+ ownerName: agentContext.owner,
543
+ allowedTopics: a2aContext.allowed_topics || [],
544
+ traceId,
545
+ requestId
546
+ }
547
+ });
545
548
 
546
549
  if (collabMode !== 'adaptive') {
547
550
  return rawResponse;
@@ -595,7 +598,7 @@ async function callAgent(message, a2aContext) {
595
598
  }
596
599
  });
597
600
 
598
- return cleanResponse || '[Sub-agent returned empty response]';
601
+ return cleanResponse || '[Sub-agent returned empty response]';
599
602
 
600
603
  } catch (err) {
601
604
  callLogger.error('Runtime turn handling failed; using fallback response', {