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.
@@ -9,6 +9,7 @@
9
9
 
10
10
  const { execSync, spawn } = require('child_process');
11
11
  const { createLogger } = require('./logger');
12
+ const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
12
13
 
13
14
  const logger = createLogger({ component: 'a2a.claude-subagent' });
14
15
 
@@ -216,7 +217,7 @@ function parseSubagentResponse(resultText) {
216
217
  * @param {number} timeoutMs - Timeout in milliseconds
217
218
  * @returns {Promise<{ stdout: string, stderr: string }>}
218
219
  */
219
- function spawnClaude(args, timeoutMs = 180000) {
220
+ function spawnClaude(args, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
220
221
  return new Promise((resolve, reject) => {
221
222
  const proc = spawn('claude', args, {
222
223
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -302,7 +303,7 @@ function extractResultFromJson(stdout) {
302
303
  * @param {Array} options.activeThreads - Active conversation threads
303
304
  * @param {Array} options.candidateCollaborations - Candidate collaboration ideas
304
305
  * @param {boolean} options.closeSignal - Whether close has been signaled
305
- * @param {number} [options.timeoutMs=180000] - Timeout in milliseconds
306
+ * @param {number} [options.timeoutMs=300000] - Timeout in milliseconds
306
307
  * @returns {Promise<{ message: string, statePatch: object|null, flags: array, sessionId: string }>}
307
308
  */
308
309
  async function runClaudeTurn(options) {
@@ -317,7 +318,7 @@ async function runClaudeTurn(options) {
317
318
  activeThreads = [],
318
319
  candidateCollaborations = [],
319
320
  closeSignal = false,
320
- timeoutMs = 180000
321
+ timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS
321
322
  } = options;
322
323
 
323
324
  const turnPrompt = buildTurnPrompt({
@@ -396,10 +397,10 @@ async function runClaudeTurn(options) {
396
397
  *
397
398
  * @param {string} sessionId - Session ID to resume
398
399
  * @param {string} reason - Why the conversation is ending
399
- * @param {number} [timeoutMs=120000] - Timeout in milliseconds
400
+ * @param {number} [timeoutMs=300000] - Timeout in milliseconds
400
401
  * @returns {Promise<{ summary: string, ownerSummary: string, actionItems: array, flags: array }>}
401
402
  */
402
- async function runClaudeSummary(sessionId, reason, timeoutMs = 120000) {
403
+ async function runClaudeSummary(sessionId, reason, timeoutMs = HARD_FALLBACK_TURN_TIMEOUT_MS) {
403
404
  if (!sessionId) {
404
405
  throw new Error('Cannot summarize without a session ID');
405
406
  }
package/src/lib/config.js CHANGED
@@ -224,6 +224,7 @@ const DEFAULT_CONFIG = {
224
224
  perHour: 100,
225
225
  perDay: 1000
226
226
  },
227
+ turnTimeoutMs: 300000, // default Claude turn timeout
227
228
  maxPendingRequests: 5 // max connection requests per hour
228
229
  },
229
230
 
@@ -22,6 +22,7 @@ const {
22
22
  const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./disclosure');
23
23
  const { createLogger } = require('./logger');
24
24
  const { buildUnifiedSummaryPrompt } = require('./summary-prompt');
25
+ const { resolveTokenTimeoutMs, resolveTurnTimeoutMs } = require('./turn-timeout');
25
26
 
26
27
  const logger = createLogger({ component: 'a2a.conversation-driver' });
27
28
 
@@ -130,9 +131,16 @@ class ConversationDriver {
130
131
  this.summarizer = options.summarizer || null;
131
132
  this.ownerContext = options.ownerContext || {};
132
133
  this.claudeMode = options.runtime?.mode === 'claude';
133
- this.claudeTimeoutMs = options.claudeTimeoutMs || 180000;
134
134
 
135
- const clientTimeout = this.claudeMode ? 200000 : 65000;
135
+ const tokenTimeoutMs = options.tokenTimeoutMs
136
+ || options.claudeTimeoutMs
137
+ || resolveTokenTimeoutMs(options.token);
138
+ const configTimeoutMs = options.configTurnTimeoutMs;
139
+ this.claudeTimeoutMs = resolveTurnTimeoutMs({ tokenTimeoutMs, configTimeoutMs });
140
+
141
+ const clientTimeout = this.claudeMode
142
+ ? Math.max(this.claudeTimeoutMs + 20000, 200000)
143
+ : 65000;
136
144
  this.client = new A2AClient({ caller: this.caller, timeout: clientTimeout });
137
145
  }
138
146
 
@@ -221,7 +229,8 @@ class ConversationDriver {
221
229
  sessionId: `summary-${Date.now()}`,
222
230
  prompt,
223
231
  messages,
224
- callerInfo: { name: agentContext.name, owner: agentContext.owner }
232
+ callerInfo: { name: agentContext.name, owner: agentContext.owner },
233
+ timeoutMs: this.claudeMode ? this.claudeTimeoutMs : 35000
225
234
  });
226
235
  } catch (err) {
227
236
  logger.warn('Runtime summarizer failed, using default', {
@@ -18,9 +18,10 @@ const DB_FILENAME = 'a2a-conversations.db';
18
18
  const logger = createLogger({ component: 'a2a.conversations' });
19
19
 
20
20
  class ConversationStore {
21
- constructor(configDir = DEFAULT_CONFIG_DIR) {
21
+ constructor(configDir = DEFAULT_CONFIG_DIR, options = {}) {
22
22
  this.configDir = configDir;
23
23
  this.dbPath = path.join(configDir, DB_FILENAME);
24
+ this.eventStore = options.eventStore || null;
24
25
  this.db = null;
25
26
  this._ensureDir();
26
27
  }
@@ -253,6 +254,19 @@ class ConversationStore {
253
254
  VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
254
255
  `).run(id, contactId, contactName, tokenId, direction, now, now);
255
256
 
257
+ if (this.eventStore && this.eventStore.isAvailable && this.eventStore.isAvailable()) {
258
+ this.eventStore.emitEvent('call.updated', {
259
+ conversation_id: id,
260
+ status: 'active',
261
+ direction,
262
+ contact_id: contactId || null,
263
+ contact_name: contactName || null
264
+ }, {
265
+ conversationId: id,
266
+ contactId: contactId || null
267
+ });
268
+ }
269
+
256
270
  return { id, resumed: false };
257
271
  }
258
272
 
@@ -284,6 +298,14 @@ class ConversationStore {
284
298
  WHERE id = ?
285
299
  `).run(now, conversationId);
286
300
 
301
+ if (this.eventStore && this.eventStore.isAvailable && this.eventStore.isAvailable()) {
302
+ this.eventStore.emitEvent('call.updated', {
303
+ conversation_id: conversationId,
304
+ status: 'active',
305
+ direction
306
+ }, { conversationId });
307
+ }
308
+
287
309
  return { id, timestamp: now };
288
310
  }
289
311
 
@@ -450,6 +472,31 @@ class ConversationStore {
450
472
  `).run(now, conversationId);
451
473
  }
452
474
 
475
+ if (this.eventStore && this.eventStore.isAvailable && this.eventStore.isAvailable()) {
476
+ this.eventStore.emitEvent('call.updated', {
477
+ conversation_id: conversationId,
478
+ status: 'concluded',
479
+ contact_id: conversation.contact_id || null,
480
+ contact_name: conversation.contact_name || null
481
+ }, {
482
+ conversationId,
483
+ contactId: conversation.contact_id || null
484
+ });
485
+
486
+ if (summary || ownerSummary) {
487
+ this.eventStore.emitEvent('summary.completed', {
488
+ conversation_id: conversationId,
489
+ contact_id: conversation.contact_id || null,
490
+ contact_name: conversation.contact_name || null,
491
+ has_summary: Boolean(summary),
492
+ has_owner_summary: Boolean(ownerSummary)
493
+ }, {
494
+ conversationId,
495
+ contactId: conversation.contact_id || null
496
+ });
497
+ }
498
+ }
499
+
453
500
  return {
454
501
  success: true,
455
502
  conversationId,
@@ -472,6 +519,13 @@ class ConversationStore {
472
519
  WHERE id = ?
473
520
  `).run(now, conversationId);
474
521
 
522
+ if (this.eventStore && this.eventStore.isAvailable && this.eventStore.isAvailable()) {
523
+ this.eventStore.emitEvent('call.updated', {
524
+ conversation_id: conversationId,
525
+ status: 'timeout'
526
+ }, { conversationId });
527
+ }
528
+
475
529
  return { success: true };
476
530
  }
477
531
 
@@ -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
+ };
@@ -14,6 +14,7 @@ const { execSync, spawnSync } = require('child_process');
14
14
  const { createLogger } = require('./logger');
15
15
  const { runClaudeTurn: invokeClaudeTurn, buildSubagentSystemPrompt, runClaudeSummary } = require('./claude-subagent');
16
16
  const { getTopicsForTier, formatTopicsForPrompt, loadManifest } = require('./disclosure');
17
+ const { HARD_FALLBACK_TURN_TIMEOUT_MS } = require('./turn-timeout');
17
18
 
18
19
  function commandExists(command) {
19
20
  try {
@@ -208,7 +209,7 @@ function createRuntimeAdapter(options = {}) {
208
209
  activeThreads: context?.activeThreads || [],
209
210
  candidateCollaborations: context?.candidateCollaborations || [],
210
211
  closeSignal: context?.closeSignal || false,
211
- timeoutMs: timeoutMs || 180000
212
+ timeoutMs: timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
212
213
  });
213
214
 
214
215
  // Store session ID from first turn for subsequent --resume
@@ -379,7 +380,7 @@ function createRuntimeAdapter(options = {}) {
379
380
  }
380
381
  }
381
382
 
382
- async function summarize({ sessionId, prompt, messages, callerInfo, traceId, conversationId }) {
383
+ async function summarize({ sessionId, prompt, messages, callerInfo, traceId, conversationId, timeoutMs }) {
383
384
  const effectiveTraceId = traceId || callerInfo?.trace_id || callerInfo?.traceId;
384
385
  const requestId = callerInfo?.request_id || callerInfo?.requestId;
385
386
  const effectiveConversationId = conversationId || callerInfo?.conversation_id || callerInfo?.conversationId;
@@ -388,7 +389,11 @@ function createRuntimeAdapter(options = {}) {
388
389
  if (modeInfo.mode === 'claude') {
389
390
  const session = claudeSessions.get(sessionId);
390
391
  if (session?.claudeSessionId) {
391
- const result = await runClaudeSummary(session.claudeSessionId, 'conversation ended');
392
+ const result = await runClaudeSummary(
393
+ session.claudeSessionId,
394
+ 'conversation ended',
395
+ timeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS
396
+ );
392
397
  if (result && result.summary) {
393
398
  return result;
394
399
  }
package/src/lib/tokens.js CHANGED
@@ -56,6 +56,11 @@ function sanitizeCustomFields(fields, options = {}) {
56
56
  return cleaned;
57
57
  }
58
58
 
59
+ function parsePositiveTimeoutMs(value) {
60
+ const parsed = Number.parseInt(String(value ?? ''), 10);
61
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
62
+ }
63
+
59
64
  class TokenStore {
60
65
  constructor(configDir = DEFAULT_CONFIG_DIR) {
61
66
  this.configDir = configDir;
@@ -196,7 +201,8 @@ class TokenStore {
196
201
  // Snapshot of actual capabilities at creation time
197
202
  allowedTopics = null, // Array of topic strings, e.g. ['chat', 'calendar.read']
198
203
  allowedGoals = null, // Array of goal strings, e.g. ['grow-network', 'find-collaborators']
199
- tierSettings = null // Object with tier-specific settings
204
+ tierSettings = null, // Object with tier-specific settings
205
+ timeoutMs = null
200
206
  } = options;
201
207
 
202
208
  const tier = String(permissions || 'public').trim() || 'public';
@@ -255,6 +261,7 @@ class TokenStore {
255
261
  capabilities: capabilities || defaultCapabilities,
256
262
  allowed_topics: allowedTopics || defaultTopics[tier] || ['chat'],
257
263
  allowed_goals: allowedGoals || defaultGoals[tier] || [],
264
+ timeout_ms: parsePositiveTimeoutMs(timeoutMs),
258
265
  tier_settings: tierSettings || {}, // Snapshot of settings at creation
259
266
  disclosure,
260
267
  notify,
@@ -327,6 +334,10 @@ class TokenStore {
327
334
  || TokenStore.DEFAULT_CAPABILITIES[tier]
328
335
  || ['context-read'];
329
336
 
337
+ const timeoutMs = parsePositiveTimeoutMs(record.timeout_ms)
338
+ || parsePositiveTimeoutMs(record.tier_settings?.timeout_ms)
339
+ || parsePositiveTimeoutMs(record.tier_settings?.timeoutMs);
340
+
330
341
  return {
331
342
  valid: true,
332
343
  id: record.id,
@@ -335,6 +346,7 @@ class TokenStore {
335
346
  capabilities,
336
347
  allowed_topics: record.allowed_topics || ['chat'],
337
348
  allowed_goals: record.allowed_goals || [],
349
+ timeout_ms: timeoutMs,
338
350
  tier_settings: record.tier_settings || {},
339
351
  disclosure: record.disclosure,
340
352
  notify: record.notify,
@@ -0,0 +1,52 @@
1
+ const HARD_FALLBACK_TURN_TIMEOUT_MS = 300000;
2
+
3
+ function parsePositiveInt(value) {
4
+ const parsed = Number.parseInt(String(value ?? ''), 10);
5
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
6
+ }
7
+
8
+ function resolveTokenTimeoutMs(token) {
9
+ if (!token || typeof token !== 'object') {
10
+ return null;
11
+ }
12
+
13
+ const topLevel = parsePositiveInt(token.timeout_ms ?? token.timeoutMs);
14
+ if (topLevel) {
15
+ return topLevel;
16
+ }
17
+
18
+ const tierSettings = token.tier_settings || token.tierSettings;
19
+ if (!tierSettings || typeof tierSettings !== 'object') {
20
+ return null;
21
+ }
22
+ return parsePositiveInt(tierSettings.timeout_ms ?? tierSettings.timeoutMs);
23
+ }
24
+
25
+ function resolveTurnTimeoutMs(options = {}) {
26
+ const tokenTimeoutMs = parsePositiveInt(options.tokenTimeoutMs);
27
+ if (tokenTimeoutMs) {
28
+ return tokenTimeoutMs;
29
+ }
30
+
31
+ const envTimeoutMs = parsePositiveInt(
32
+ options.envTimeoutMs !== undefined ? options.envTimeoutMs : process.env.A2A_TURN_TIMEOUT
33
+ );
34
+ if (envTimeoutMs) {
35
+ return envTimeoutMs;
36
+ }
37
+
38
+ const configTimeoutMs = parsePositiveInt(options.configTimeoutMs);
39
+ if (configTimeoutMs) {
40
+ return configTimeoutMs;
41
+ }
42
+
43
+ const fallbackTimeoutMs = parsePositiveInt(options.hardFallbackMs);
44
+ return fallbackTimeoutMs || HARD_FALLBACK_TURN_TIMEOUT_MS;
45
+ }
46
+
47
+ module.exports = {
48
+ HARD_FALLBACK_TURN_TIMEOUT_MS,
49
+ parsePositiveInt,
50
+ resolveTokenTimeoutMs,
51
+ resolveTurnTimeoutMs
52
+ };
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,
@@ -340,6 +347,7 @@ function createRoutes(options = {}) {
340
347
  tier: validation.tier,
341
348
  capabilities: validation.capabilities,
342
349
  allowed_topics: validation.allowed_topics,
350
+ timeout_ms: validation.timeout_ms,
343
351
  disclosure: validation.disclosure,
344
352
  caller: sanitizedCaller,
345
353
  conversation_id: conversation_id || `conv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
@@ -366,6 +374,17 @@ function createRoutes(options = {}) {
366
374
  tokenId: validation.id,
367
375
  direction: 'inbound'
368
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
+ }
369
388
 
370
389
  // Track activity for auto-conclude
371
390
  if (monitor) {
@@ -562,7 +581,10 @@ function createRoutes(options = {}) {
562
581
  }));
563
582
  }
564
583
 
565
- const convStore = getConversationStore();
584
+ const convStore = getConversationStore({
585
+ eventStore,
586
+ configDir: tokenStore.configDir
587
+ });
566
588
  if (!convStore) {
567
589
  return res.json(withTracePayload({ success: true, message: 'Conversation storage not enabled' }));
568
590
  }