a2acalling 0.6.50 → 0.6.52

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.
@@ -0,0 +1,205 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { EventEmitter } = require('events');
4
+
5
+ const DEFAULT_CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
6
+ process.env.OPENCLAW_CONFIG_DIR ||
7
+ path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
8
+
9
+ const DB_FILENAME = 'a2a-events.db';
10
+ const DEFAULT_RETENTION_COUNT = 5000;
11
+
12
+ function nowIso() {
13
+ return new Date().toISOString();
14
+ }
15
+
16
+ class DashboardEventStore {
17
+ constructor(configDir = DEFAULT_CONFIG_DIR, options = {}) {
18
+ this.configDir = configDir;
19
+ this.dbPath = options.dbPath || path.join(configDir, DB_FILENAME);
20
+ this.retentionCount = Number.isFinite(options.retentionCount)
21
+ ? Math.max(100, Math.floor(options.retentionCount))
22
+ : DEFAULT_RETENTION_COUNT;
23
+ this.db = null;
24
+ this._dbError = null;
25
+ this._stmts = null;
26
+ this._emitter = new EventEmitter();
27
+ this._ensureDir();
28
+ }
29
+
30
+ _ensureDir() {
31
+ if (!fs.existsSync(this.configDir)) {
32
+ fs.mkdirSync(this.configDir, { recursive: true });
33
+ }
34
+ }
35
+
36
+ _initDb() {
37
+ if (this.db) return this.db;
38
+ if (this._dbError) return null;
39
+ try {
40
+ const Database = require('better-sqlite3');
41
+ this.db = new Database(this.dbPath);
42
+ try {
43
+ fs.chmodSync(this.dbPath, 0o600);
44
+ } catch (_) {
45
+ // Best effort.
46
+ }
47
+ this._migrate();
48
+ this._prepareStatements();
49
+ return this.db;
50
+ } catch (err) {
51
+ this._dbError = err && err.message ? err.message : 'failed_to_initialize_event_db';
52
+ return null;
53
+ }
54
+ }
55
+
56
+ _migrate() {
57
+ this.db.exec(`
58
+ PRAGMA journal_mode = WAL;
59
+
60
+ CREATE TABLE IF NOT EXISTS dashboard_events (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ event_type TEXT NOT NULL,
63
+ created_at TEXT NOT NULL,
64
+ payload_json TEXT NOT NULL,
65
+ conversation_id TEXT,
66
+ contact_id TEXT
67
+ );
68
+
69
+ CREATE INDEX IF NOT EXISTS idx_dashboard_events_created ON dashboard_events(created_at);
70
+ CREATE INDEX IF NOT EXISTS idx_dashboard_events_type ON dashboard_events(event_type);
71
+ CREATE INDEX IF NOT EXISTS idx_dashboard_events_conversation ON dashboard_events(conversation_id);
72
+ CREATE INDEX IF NOT EXISTS idx_dashboard_events_contact ON dashboard_events(contact_id);
73
+ `);
74
+ }
75
+
76
+ _prepareStatements() {
77
+ this._stmts = {
78
+ insertEvent: this.db.prepare(`
79
+ INSERT INTO dashboard_events (event_type, created_at, payload_json, conversation_id, contact_id)
80
+ VALUES (@event_type, @created_at, @payload_json, @conversation_id, @contact_id)
81
+ `),
82
+ listSince: this.db.prepare(`
83
+ SELECT id, event_type, created_at, payload_json, conversation_id, contact_id
84
+ FROM dashboard_events
85
+ WHERE id > @since_id
86
+ ORDER BY id ASC
87
+ LIMIT @limit
88
+ `),
89
+ listLatest: this.db.prepare(`
90
+ SELECT id, event_type, created_at, payload_json, conversation_id, contact_id
91
+ FROM dashboard_events
92
+ ORDER BY id DESC
93
+ LIMIT @limit
94
+ `),
95
+ prune: this.db.prepare(`
96
+ DELETE FROM dashboard_events
97
+ WHERE id NOT IN (
98
+ SELECT id FROM dashboard_events
99
+ ORDER BY id DESC
100
+ LIMIT @retention
101
+ )
102
+ `)
103
+ };
104
+ }
105
+
106
+ isAvailable() {
107
+ return Boolean(this._initDb());
108
+ }
109
+
110
+ getDbError() {
111
+ this._initDb();
112
+ return this._dbError;
113
+ }
114
+
115
+ emitEvent(eventType, payload = {}, meta = {}) {
116
+ const db = this._initDb();
117
+ if (!db) {
118
+ return { success: false, error: 'event_storage_unavailable', message: this._dbError };
119
+ }
120
+
121
+ const event_type = String(eventType || '').trim().slice(0, 80);
122
+ if (!event_type) {
123
+ return { success: false, error: 'event_type_required' };
124
+ }
125
+
126
+ const created_at = nowIso();
127
+ const row = {
128
+ event_type,
129
+ created_at,
130
+ payload_json: JSON.stringify(payload || {}),
131
+ conversation_id: meta && meta.conversationId ? String(meta.conversationId).slice(0, 120) : null,
132
+ contact_id: meta && meta.contactId ? String(meta.contactId).slice(0, 120) : null
133
+ };
134
+
135
+ const info = this._stmts.insertEvent.run(row);
136
+ const id = Number(info.lastInsertRowid);
137
+ const event = {
138
+ id,
139
+ type: event_type,
140
+ created_at,
141
+ conversation_id: row.conversation_id,
142
+ contact_id: row.contact_id,
143
+ payload: payload || {}
144
+ };
145
+
146
+ if (id % 100 === 0) {
147
+ try {
148
+ this._stmts.prune.run({ retention: this.retentionCount });
149
+ } catch (_) {
150
+ // Best effort.
151
+ }
152
+ }
153
+
154
+ this._emitter.emit('event', event);
155
+ return { success: true, event };
156
+ }
157
+
158
+ listSince(sinceId, options = {}) {
159
+ const db = this._initDb();
160
+ if (!db) return [];
161
+ const limit = Math.min(500, Math.max(1, Number.parseInt(String(options.limit || '200'), 10) || 200));
162
+ const parsedSince = Number.parseInt(String(sinceId || '0'), 10);
163
+ if (Number.isFinite(parsedSince) && parsedSince > 0) {
164
+ const rows = this._stmts.listSince.all({ since_id: parsedSince, limit });
165
+ return rows.map((row) => this._toEvent(row));
166
+ }
167
+ const rows = this._stmts.listLatest.all({ limit }).reverse();
168
+ return rows.map((row) => this._toEvent(row));
169
+ }
170
+
171
+ subscribe(listener) {
172
+ const safe = (event) => {
173
+ try {
174
+ listener(event);
175
+ } catch (_) {
176
+ // Keep stream robust.
177
+ }
178
+ };
179
+ this._emitter.on('event', safe);
180
+ return () => {
181
+ this._emitter.off('event', safe);
182
+ };
183
+ }
184
+
185
+ _toEvent(row) {
186
+ let payload = {};
187
+ try {
188
+ payload = row.payload_json ? JSON.parse(row.payload_json) : {};
189
+ } catch (_) {
190
+ payload = {};
191
+ }
192
+ return {
193
+ id: row.id,
194
+ type: row.event_type,
195
+ created_at: row.created_at,
196
+ conversation_id: row.conversation_id || null,
197
+ contact_id: row.contact_id || null,
198
+ payload
199
+ };
200
+ }
201
+ }
202
+
203
+ module.exports = {
204
+ DashboardEventStore
205
+ };
@@ -149,7 +149,9 @@ function createRuntimeAdapter(options = {}) {
149
149
  }
150
150
  });
151
151
 
152
- // Claude subagent session tracking
152
+ // Claude state tracking.
153
+ // Design decision (A2A-29): we keep per-conversation state for prompt/metadata
154
+ // continuity, but Claude execution itself is stateless (no `--resume`).
153
155
  const claudeSessions = new Map();
154
156
 
155
157
  async function runClaudeTurnAdapter({ sessionId, message, caller, context, timeoutMs }) {
@@ -180,7 +182,18 @@ function createRuntimeAdapter(options = {}) {
180
182
  roleContext: context?.roleContext || ''
181
183
  });
182
184
 
183
- session = { claudeSessionId: null, systemPrompt, turnCount: 0, lastMeta: null };
185
+ session = {
186
+ systemPrompt,
187
+ turnCount: 0,
188
+ lastMeta: null,
189
+ // Keep a permission snapshot so summary runs with the same policy envelope.
190
+ permissionSnapshot: {
191
+ capabilities: Array.isArray(context?.capabilities) ? context.capabilities : [],
192
+ allowedTopics: Array.isArray(context?.allowedTopics || context?.allowed_topics)
193
+ ? (context?.allowedTopics || context?.allowed_topics)
194
+ : []
195
+ }
196
+ };
184
197
  claudeSessions.set(sessionId, session);
185
198
  }
186
199
 
@@ -199,7 +212,6 @@ function createRuntimeAdapter(options = {}) {
199
212
  });
200
213
 
201
214
  const result = await invokeClaudeTurn({
202
- sessionId: session.claudeSessionId,
203
215
  systemPrompt: session.systemPrompt,
204
216
  turnMessage: message,
205
217
  turn: session.turnCount,
@@ -209,12 +221,21 @@ function createRuntimeAdapter(options = {}) {
209
221
  activeThreads: context?.activeThreads || [],
210
222
  candidateCollaborations: context?.candidateCollaborations || [],
211
223
  closeSignal: context?.closeSignal || false,
224
+ capabilities: Array.isArray(context?.capabilities)
225
+ ? context.capabilities
226
+ : (session.permissionSnapshot?.capabilities || []),
227
+ allowedTopics: Array.isArray(context?.allowedTopics || context?.allowed_topics)
228
+ ? (context?.allowedTopics || context?.allowed_topics)
229
+ : (session.permissionSnapshot?.allowedTopics || []),
212
230
  timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
213
231
  });
214
232
 
215
- // Store session ID from first turn for subsequent --resume
216
- if (result.sessionId) {
217
- session.claudeSessionId = result.sessionId;
233
+ // Update permission snapshot if the caller supplied explicit context this turn.
234
+ if (Array.isArray(context?.capabilities)) {
235
+ session.permissionSnapshot.capabilities = context.capabilities;
236
+ }
237
+ if (Array.isArray(context?.allowedTopics || context?.allowed_topics)) {
238
+ session.permissionSnapshot.allowedTopics = context?.allowedTopics || context?.allowed_topics;
218
239
  }
219
240
 
220
241
  // Store flags/state for retrieval via getLastTurnMeta
@@ -385,20 +406,28 @@ function createRuntimeAdapter(options = {}) {
385
406
  const requestId = callerInfo?.request_id || callerInfo?.requestId;
386
407
  const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
387
408
 
388
- // Claude mode: use the subagent session for summarization
409
+ // Claude mode: stateless summary invocation (no session restore dependency).
389
410
  if (modeInfo.mode === 'claude') {
390
411
  const session = claudeSessions.get(sessionId);
391
- if (session?.claudeSessionId) {
392
- const result = await runClaudeSummary(
393
- session.claudeSessionId,
394
- 'conversation ended',
395
- timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
396
- );
397
- if (result && result.summary) {
398
- return result;
399
- }
412
+ const capabilities = session?.permissionSnapshot?.capabilities
413
+ || callerInfo?.capabilities
414
+ || [];
415
+ const allowedTopics = session?.permissionSnapshot?.allowedTopics
416
+ || callerInfo?.allowedTopics
417
+ || callerInfo?.allowed_topics
418
+ || [];
419
+
420
+ const result = await runClaudeSummary({
421
+ prompt,
422
+ reason: 'conversation ended',
423
+ capabilities,
424
+ allowedTopics,
425
+ timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
426
+ });
427
+ if (result && result.summary) {
428
+ return result;
400
429
  }
401
- throw new Error('Claude summary session not available or returned empty result');
430
+ throw new Error('Claude summary returned empty result');
402
431
  }
403
432
 
404
433
  if (modeInfo.mode !== 'openclaw') {
package/src/routes/a2a.js CHANGED
@@ -15,11 +15,14 @@ const { createLogger, createTraceId } = require('../lib/logger');
15
15
  // Lazy-load conversation store (optional dependency)
16
16
  let ConversationStore = null;
17
17
  let conversationStore = null;
18
- function getConversationStore() {
18
+ function getConversationStore(options = {}) {
19
19
  if (!ConversationStore) {
20
20
  try {
21
21
  ConversationStore = require('../lib/conversations').ConversationStore;
22
- conversationStore = new ConversationStore();
22
+ const configDir = options.configDir || undefined;
23
+ conversationStore = new ConversationStore(configDir, {
24
+ eventStore: options.eventStore || null
25
+ });
23
26
  if (!conversationStore.isAvailable()) {
24
27
  conversationStore = null;
25
28
  }
@@ -162,9 +165,13 @@ function createRoutes(options = {}) {
162
165
  const notifyOwner = options.notifyOwner || (() => Promise.resolve());
163
166
  const limits = options.rateLimits || { minute: 10, hour: 100, day: 1000 };
164
167
  const logger = options.logger || createLogger({ component: 'a2a.routes' });
168
+ const eventStore = options.eventStore || null;
165
169
 
166
170
  // Initialize conversation store and call monitor
167
- const convStore = getConversationStore();
171
+ const convStore = getConversationStore({
172
+ eventStore,
173
+ configDir: tokenStore.configDir
174
+ });
168
175
  const monitor = getCallMonitor({
169
176
  convStore,
170
177
  summarizer: options.summarizer,
@@ -367,11 +374,25 @@ function createRoutes(options = {}) {
367
374
  tokenId: validation.id,
368
375
  direction: 'inbound'
369
376
  });
377
+ if (isNewConversation && eventStore && eventStore.isAvailable && eventStore.isAvailable()) {
378
+ eventStore.emitEvent('call.inbound', {
379
+ conversation_id: a2aContext.conversation_id,
380
+ token_id: validation.id,
381
+ caller_name: sanitizedCaller.name || validation.name || null,
382
+ caller_owner: sanitizedCaller.owner || null
383
+ }, {
384
+ conversationId: a2aContext.conversation_id,
385
+ contactId: ensuredContact?.id || validation.id
386
+ });
387
+ }
370
388
 
371
389
  // Track activity for auto-conclude
372
390
  if (monitor) {
373
391
  monitor.trackActivity(a2aContext.conversation_id, {
374
392
  ...sanitizedCaller,
393
+ tier: validation.tier,
394
+ capabilities: validation.capabilities,
395
+ allowed_topics: validation.allowed_topics,
375
396
  trace_id: traceId,
376
397
  request_id: requestId
377
398
  });
@@ -563,7 +584,10 @@ function createRoutes(options = {}) {
563
584
  }));
564
585
  }
565
586
 
566
- const convStore = getConversationStore();
587
+ const convStore = getConversationStore({
588
+ eventStore,
589
+ configDir: tokenStore.configDir
590
+ });
567
591
  if (!convStore) {
568
592
  return res.json(withTracePayload({ success: true, message: 'Conversation storage not enabled' }));
569
593
  }
@@ -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,6 +26,7 @@ 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');
30
31
  const { resolveTurnTimeoutMs } = require('./lib/turn-timeout');
31
32
 
@@ -67,6 +68,7 @@ function loadAgentContext() {
67
68
  const agentContext = loadAgentContext();
68
69
  const tokenStore = new TokenStore();
69
70
  const config = new A2AConfig();
71
+ const eventStore = new DashboardEventStore(tokenStore.configDir);
70
72
  const runtime = createRuntimeAdapter({
71
73
  workspaceDir,
72
74
  agentContext,
@@ -619,7 +621,9 @@ async function callAgent(message, a2aContext) {
619
621
  conversationId,
620
622
  tier: tierInfo,
621
623
  ownerName: agentContext.owner,
624
+ capabilities: Array.isArray(a2aContext.capabilities) ? a2aContext.capabilities : [],
622
625
  allowedTopics: a2aContext.allowed_topics || [],
626
+ allowed_topics: a2aContext.allowed_topics || [],
623
627
  timeoutMs: runtime.mode === 'claude' ? claudeTurnTimeoutMs : 65000,
624
628
  traceId,
625
629
  requestId
@@ -833,6 +837,7 @@ app.use('/api/a2a/dashboard', createDashboardApiRouter({
833
837
  tokenStore,
834
838
  agentContext,
835
839
  config,
840
+ eventStore,
836
841
  getUpdateManager: () => updateManager,
837
842
  logger: logger.child({ component: 'a2a.dashboard' })
838
843
  }));
@@ -858,6 +863,7 @@ app.use('/callbook', createCallbookRouter());
858
863
 
859
864
  app.use('/api/a2a', createRoutes({
860
865
  tokenStore,
866
+ eventStore,
861
867
  logger: logger.child({ component: 'a2a.routes' }),
862
868
  onCallMonitor: (monitor) => {
863
869
  activeCallMonitor = monitor;