a2acalling 0.6.49 → 0.6.51

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.
@@ -18,6 +18,7 @@ const { A2AConfig } = require('../lib/config');
18
18
  const { loadManifest, saveManifest } = require('../lib/disclosure');
19
19
  const { resolveInviteHost } = require('../lib/invite-host');
20
20
  const { CallbookStore } = require('../lib/callbook');
21
+ const { DashboardEventStore } = require('../lib/dashboard-events');
21
22
  const { createLogger } = require('../lib/logger');
22
23
 
23
24
  const DASHBOARD_STATIC_DIR = path.join(__dirname, '..', 'dashboard', 'public');
@@ -192,11 +193,12 @@ function buildContext(options = {}) {
192
193
  const config = options.config || new A2AConfig();
193
194
  const logger = options.logger || createLogger({ component: 'a2a.dashboard' });
194
195
  const callbookStore = options.callbookStore || new CallbookStore(tokenStore.configDir);
196
+ const eventStore = options.eventStore || new DashboardEventStore(tokenStore.configDir);
195
197
  const agentContext = resolveAgentContext(options);
196
198
  let convStore = options.convStore || null;
197
199
  if (!convStore) {
198
200
  try {
199
- convStore = new ConversationStore();
201
+ convStore = new ConversationStore(tokenStore.configDir, { eventStore });
200
202
  if (!convStore.isAvailable()) {
201
203
  convStore = null;
202
204
  }
@@ -210,6 +212,7 @@ function buildContext(options = {}) {
210
212
  config,
211
213
  convStore,
212
214
  callbookStore,
215
+ eventStore,
213
216
  getUpdateManager: typeof options.getUpdateManager === 'function'
214
217
  ? options.getUpdateManager
215
218
  : (() => null),
@@ -444,6 +447,23 @@ function createDashboardApiRouter(options = {}) {
444
447
  const context = buildContext(options);
445
448
  router.use(express.json());
446
449
  const ensureDashboardAccess = makeEnsureDashboardAccess(context);
450
+ const writeSseEvent = (res, event) => {
451
+ const eventName = sanitizeString(event?.type || 'message', 80) || 'message';
452
+ const eventId = Number.parseInt(String(event?.id || ''), 10);
453
+ const payload = {
454
+ id: eventId || null,
455
+ type: eventName,
456
+ created_at: event?.created_at || new Date().toISOString(),
457
+ conversation_id: event?.conversation_id || null,
458
+ contact_id: event?.contact_id || null,
459
+ payload: event?.payload || {}
460
+ };
461
+ if (Number.isFinite(eventId) && eventId > 0) {
462
+ res.write(`id: ${eventId}\n`);
463
+ }
464
+ res.write(`event: ${eventName}\n`);
465
+ res.write(`data: ${JSON.stringify(payload)}\n\n`);
466
+ };
447
467
 
448
468
  // Callbook Remote: exchange a short-lived provisioning code for a long-lived session cookie.
449
469
  // This route must be reachable BEFORE dashboard access is established.
@@ -481,6 +501,14 @@ function createDashboardApiRouter(options = {}) {
481
501
  });
482
502
  res.setHeader('Set-Cookie', cookie);
483
503
 
504
+ if (context.eventStore && context.eventStore.isAvailable()) {
505
+ context.eventStore.emitEvent('invite.used', {
506
+ device_id: result.device?.id || null,
507
+ device_label: result.device?.label || null,
508
+ source: 'callbook_exchange'
509
+ });
510
+ }
511
+
484
512
  return res.json({
485
513
  success: true,
486
514
  device: result.device,
@@ -491,6 +519,72 @@ function createDashboardApiRouter(options = {}) {
491
519
  // All other dashboard API routes require owner access.
492
520
  router.use(ensureDashboardAccess);
493
521
 
522
+ router.get('/events', (req, res) => {
523
+ if (!context.eventStore || !context.eventStore.isAvailable()) {
524
+ return res.status(503).json({
525
+ success: false,
526
+ error: 'event_stream_unavailable',
527
+ message: context.eventStore ? context.eventStore.getDbError() : 'missing_event_store'
528
+ });
529
+ }
530
+
531
+ const lastIdHeader = req.headers['last-event-id'];
532
+ const since = sanitizeString(
533
+ req.query.since || lastIdHeader || req.query.last_event_id || '',
534
+ 32
535
+ );
536
+ const replayLimit = Math.min(500, Math.max(1, Number.parseInt(req.query.replay || '200', 10) || 200));
537
+
538
+ res.status(200);
539
+ res.setHeader('Content-Type', 'text/event-stream');
540
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
541
+ res.setHeader('Connection', 'keep-alive');
542
+ res.flushHeaders?.();
543
+
544
+ let lastSentId = Number.parseInt(String(since || '0'), 10);
545
+ if (!Number.isFinite(lastSentId) || lastSentId < 0) {
546
+ lastSentId = 0;
547
+ }
548
+
549
+ const pendingLive = [];
550
+ const unsubscribe = context.eventStore.subscribe((event) => {
551
+ if (!event || !Number.isFinite(event.id)) return;
552
+ if (event.id <= lastSentId) return;
553
+ pendingLive.push(event);
554
+ });
555
+
556
+ const replay = context.eventStore.listSince(since, { limit: replayLimit });
557
+ for (const row of replay) {
558
+ writeSseEvent(res, row);
559
+ if (Number.isFinite(row.id) && row.id > lastSentId) {
560
+ lastSentId = row.id;
561
+ }
562
+ }
563
+ while (pendingLive.length > 0) {
564
+ const row = pendingLive.shift();
565
+ if (!row || !Number.isFinite(row.id) || row.id <= lastSentId) continue;
566
+ writeSseEvent(res, row);
567
+ lastSentId = row.id;
568
+ }
569
+ res.write(': connected\n\n');
570
+
571
+ const liveUnsubscribe = context.eventStore.subscribe((event) => {
572
+ if (!event || !Number.isFinite(event.id) || event.id <= lastSentId) return;
573
+ writeSseEvent(res, event);
574
+ lastSentId = event.id;
575
+ });
576
+ const heartbeat = setInterval(() => {
577
+ res.write(`: heartbeat ${Date.now()}\n\n`);
578
+ }, 15000);
579
+
580
+ req.on('close', () => {
581
+ clearInterval(heartbeat);
582
+ unsubscribe();
583
+ liveUnsubscribe();
584
+ res.end();
585
+ });
586
+ });
587
+
494
588
  router.get('/status', async (req, res) => {
495
589
  context.logger.debug('Dashboard status requested', { event: 'dashboard_status' });
496
590
  const refreshIp = String(req.query.refresh_ip || 'false') === 'true';
@@ -1028,7 +1122,16 @@ function createDashboardApiRouter(options = {}) {
1028
1122
  const url = `a2a://${contact.host}/${contact.token}`;
1029
1123
  try {
1030
1124
  const result = await client.call(url, message, { conversationId, timeoutSeconds });
1125
+ const previousStatus = String(contact.status || '');
1031
1126
  context.tokenStore.updateContactStatus(contact.id, 'online');
1127
+ if (context.eventStore && context.eventStore.isAvailable() && previousStatus !== 'online') {
1128
+ context.eventStore.emitEvent('contact.status.changed', {
1129
+ contact_id: contact.id,
1130
+ contact_name: contact.name || contact.host || null,
1131
+ previous_status: previousStatus || null,
1132
+ status: 'online'
1133
+ }, { contactId: contact.id });
1134
+ }
1032
1135
 
1033
1136
  if (context.convStore) {
1034
1137
  try {
@@ -1053,7 +1156,17 @@ function createDashboardApiRouter(options = {}) {
1053
1156
  can_continue: result?.can_continue !== false
1054
1157
  });
1055
1158
  } catch (err) {
1159
+ const previousStatus = String(contact.status || '');
1056
1160
  context.tokenStore.updateContactStatus(contact.id, 'offline', err.message);
1161
+ if (context.eventStore && context.eventStore.isAvailable() && previousStatus !== 'offline') {
1162
+ context.eventStore.emitEvent('contact.status.changed', {
1163
+ contact_id: contact.id,
1164
+ contact_name: contact.name || contact.host || null,
1165
+ previous_status: previousStatus || null,
1166
+ status: 'offline',
1167
+ reason: err.message || null
1168
+ }, { contactId: contact.id });
1169
+ }
1057
1170
  return res.status(502).json({
1058
1171
  success: false,
1059
1172
  error: 'contact_call_failed',
package/src/server.js CHANGED
@@ -26,7 +26,9 @@ const { writePidFile, removePidFile } = require('./lib/pid-file');
26
26
  const { buildUnifiedSummaryPrompt } = require('./lib/summary-prompt');
27
27
  const { A2AConfig } = require('./lib/config');
28
28
  const { UpdateManager } = require('./lib/update-manager');
29
+ const { DashboardEventStore } = require('./lib/dashboard-events');
29
30
  const { spawn } = require('child_process');
31
+ const { resolveTurnTimeoutMs } = require('./lib/turn-timeout');
30
32
 
31
33
  const DEFAULT_PORTS = [80, 3001, 8080, 8443, 9001];
32
34
  const requestedPort = process.env.PORT ? parseInt(process.env.PORT, 10)
@@ -66,6 +68,7 @@ function loadAgentContext() {
66
68
  const agentContext = loadAgentContext();
67
69
  const tokenStore = new TokenStore();
68
70
  const config = new A2AConfig();
71
+ const eventStore = new DashboardEventStore(tokenStore.configDir);
69
72
  const runtime = createRuntimeAdapter({
70
73
  workspaceDir,
71
74
  agentContext,
@@ -120,6 +123,15 @@ function readPositiveIntEnv(name, fallback) {
120
123
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
121
124
  }
122
125
 
126
+ function resolveConfiguredTurnTimeoutMs() {
127
+ try {
128
+ const defaults = config.getDefaults?.() || {};
129
+ return defaults.turnTimeoutMs ?? defaults.turn_timeout_ms ?? null;
130
+ } catch (err) {
131
+ return null;
132
+ }
133
+ }
134
+
123
135
  function resolveCollabMode() {
124
136
  const raw = String(process.env.A2A_COLLAB_MODE || 'adaptive').trim().toLowerCase();
125
137
  if (raw === 'deep_dive' || raw === 'deep-dive') {
@@ -583,6 +595,10 @@ async function callAgent(message, a2aContext) {
583
595
  : buildConnectionPrompt(promptOptions);
584
596
 
585
597
  const sessionId = `a2a-${conversationId}`;
598
+ const claudeTurnTimeoutMs = resolveTurnTimeoutMs({
599
+ tokenTimeoutMs: a2aContext.timeout_ms,
600
+ configTimeoutMs: resolveConfiguredTurnTimeoutMs()
601
+ });
586
602
 
587
603
  try {
588
604
  callLogger.info('Handling inbound call turn', {
@@ -600,12 +616,13 @@ async function callAgent(message, a2aContext) {
600
616
  prompt,
601
617
  message,
602
618
  caller: a2aContext.caller || {},
603
- timeoutMs: 65000,
619
+ timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
604
620
  context: {
605
621
  conversationId,
606
622
  tier: tierInfo,
607
623
  ownerName: agentContext.owner,
608
624
  allowedTopics: a2aContext.allowed_topics || [],
625
+ timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
609
626
  traceId,
610
627
  requestId
611
628
  }
@@ -818,6 +835,7 @@ app.use('/api/a2a/dashboard', createDashboardApiRouter({
818
835
  tokenStore,
819
836
  agentContext,
820
837
  config,
838
+ eventStore,
821
839
  getUpdateManager: () => updateManager,
822
840
  logger: logger.child({ component: 'a2a.dashboard' })
823
841
  }));
@@ -843,6 +861,7 @@ app.use('/callbook', createCallbookRouter());
843
861
 
844
862
  app.use('/api/a2a', createRoutes({
845
863
  tokenStore,
864
+ eventStore,
846
865
  logger: logger.child({ component: 'a2a.routes' }),
847
866
  onCallMonitor: (monitor) => {
848
867
  activeCallMonitor = monitor;