a2acalling 0.6.48 → 0.6.50

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,313 @@
1
+ const { execFile } = require('child_process');
2
+ const { EventEmitter } = require('events');
3
+ const { createLogger } = require('./logger');
4
+ const { checkForUpdate, isSameMajor } = require('./update-checker');
5
+
6
+ const DEFAULT_INTERVAL_MS = 60 * 60 * 1000;
7
+ const DEFAULT_NPM_TIMEOUT_MS = 2 * 60 * 1000;
8
+
9
+ function isUpdateSafe(callMonitor) {
10
+ if (!callMonitor || typeof callMonitor.getActiveCount !== 'function') return true;
11
+ return Number(callMonitor.getActiveCount()) === 0;
12
+ }
13
+
14
+ function shouldApplyUpdate(currentVersion, latestVersion, options = {}) {
15
+ if (!latestVersion) return false;
16
+ if (options.allowMajor) return true;
17
+ return isSameMajor(currentVersion, latestVersion);
18
+ }
19
+
20
+ function autoUpdateDisabledByEnv() {
21
+ if (String(process.env.CI || '').toLowerCase() === 'true') return true;
22
+ if (String(process.env.A2A_AUTO_UPDATE || '').trim() === '0') return true;
23
+ if (String(process.env.NO_AUTO_UPDATE || '').trim() === '1') return true;
24
+ return false;
25
+ }
26
+
27
+ function execFilePromise(cmd, args, options = {}) {
28
+ return new Promise((resolve, reject) => {
29
+ execFile(cmd, args, options, (err, stdout, stderr) => {
30
+ if (err) {
31
+ err.stdout = stdout;
32
+ err.stderr = stderr;
33
+ reject(err);
34
+ return;
35
+ }
36
+ resolve({ stdout, stderr });
37
+ });
38
+ });
39
+ }
40
+
41
+ class UpdateManager extends EventEmitter {
42
+ constructor(options = {}) {
43
+ super();
44
+ this.currentVersion = options.currentVersion;
45
+ this.config = options.config || null;
46
+ this.logger = options.logger || createLogger({ component: 'a2a.updater' });
47
+ this.intervalMs = Number.isFinite(options.intervalMs) && options.intervalMs > 0
48
+ ? options.intervalMs
49
+ : DEFAULT_INTERVAL_MS;
50
+ this.allowMajor = Boolean(options.allowMajor);
51
+ this.enabled = options.enabled !== false;
52
+ this.getCallMonitor = typeof options.getCallMonitor === 'function'
53
+ ? options.getCallMonitor
54
+ : (() => null);
55
+ this.installTimeoutMs = Number.isFinite(options.installTimeoutMs) && options.installTimeoutMs > 0
56
+ ? options.installTimeoutMs
57
+ : DEFAULT_NPM_TIMEOUT_MS;
58
+ this.restartFn = typeof options.restartFn === 'function' ? options.restartFn : null;
59
+ this._execFile = options.execFile || execFilePromise;
60
+ this._status = {
61
+ state: 'up_to_date',
62
+ enabled: this.enabled,
63
+ current_version: this.currentVersion,
64
+ latest_version: this.currentVersion,
65
+ target_version: null,
66
+ active_calls: 0,
67
+ last_checked_at: null,
68
+ last_success_at: null,
69
+ last_error: null,
70
+ defer_reason: null
71
+ };
72
+ this._timer = null;
73
+ this._running = false;
74
+ this._pendingManualUpdate = false;
75
+
76
+ this._loadConfig();
77
+ if (autoUpdateDisabledByEnv()) {
78
+ this.enabled = false;
79
+ this._status.enabled = false;
80
+ this.logger.info('Auto-update disabled by environment', { event: 'updater_env_disabled' });
81
+ }
82
+ }
83
+
84
+ _loadConfig() {
85
+ if (!this.config || typeof this.config.getAutoUpdate !== 'function') return;
86
+ const cfg = this.config.getAutoUpdate();
87
+ if (cfg && typeof cfg === 'object') {
88
+ if (typeof cfg.enabled === 'boolean') this.enabled = cfg.enabled;
89
+ if (Number.isFinite(cfg.intervalMs) && cfg.intervalMs > 0) this.intervalMs = cfg.intervalMs;
90
+ if (typeof cfg.allowMajor === 'boolean') this.allowMajor = cfg.allowMajor;
91
+ }
92
+ this._status.enabled = this.enabled;
93
+ }
94
+
95
+ _persistConfigPatch(patch) {
96
+ if (!this.config || typeof this.config.setAutoUpdate !== 'function') return;
97
+ try {
98
+ this.config.setAutoUpdate(patch);
99
+ } catch (err) {
100
+ this.logger.warn('Failed to persist auto-update config patch', {
101
+ event: 'updater_config_patch_failed',
102
+ error: err
103
+ });
104
+ }
105
+ }
106
+
107
+ _setStatus(patch) {
108
+ this._status = { ...this._status, ...patch };
109
+ this.emit('status', this.getStatus());
110
+ }
111
+
112
+ getStatus() {
113
+ return {
114
+ ...this._status,
115
+ enabled: this.enabled,
116
+ interval_ms: this.intervalMs,
117
+ allow_major: this.allowMajor
118
+ };
119
+ }
120
+
121
+ async setEnabled(enabled) {
122
+ this.enabled = Boolean(enabled);
123
+ this._setStatus({ enabled: this.enabled });
124
+ this._persistConfigPatch({ enabled: this.enabled });
125
+ if (this.enabled && !this._timer) {
126
+ this.start();
127
+ await this.triggerCheck({ reason: 'manual_enable' });
128
+ }
129
+ if (!this.enabled && this._timer) {
130
+ this.stop();
131
+ }
132
+ return this.getStatus();
133
+ }
134
+
135
+ start() {
136
+ if (this._timer) return;
137
+ if (!this.enabled) {
138
+ this.logger.info('Auto-updater disabled', { event: 'updater_disabled' });
139
+ return;
140
+ }
141
+
142
+ this._timer = setInterval(() => {
143
+ this.triggerCheck({ reason: 'interval' }).catch((err) => {
144
+ this.logger.warn('Auto-update interval check failed', {
145
+ event: 'updater_interval_failed',
146
+ error: err
147
+ });
148
+ });
149
+ }, this.intervalMs);
150
+
151
+ this.logger.info('Auto-updater started', {
152
+ event: 'updater_started',
153
+ data: {
154
+ interval_ms: this.intervalMs,
155
+ allow_major: this.allowMajor
156
+ }
157
+ });
158
+ }
159
+
160
+ stop() {
161
+ if (!this._timer) return;
162
+ clearInterval(this._timer);
163
+ this._timer = null;
164
+ this.logger.info('Auto-updater stopped', { event: 'updater_stopped' });
165
+ }
166
+
167
+ async triggerCheck(options = {}) {
168
+ return this._runCycle({ ...options, manualUpdate: false, forceCheck: Boolean(options.forceCheck) });
169
+ }
170
+
171
+ async triggerUpdate(options = {}) {
172
+ this._pendingManualUpdate = true;
173
+ return this._runCycle({ ...options, manualUpdate: true });
174
+ }
175
+
176
+ async _runCycle(options = {}) {
177
+ if (this._running) {
178
+ return this.getStatus();
179
+ }
180
+ if (!this.enabled && !options.manualUpdate && !options.forceCheck) {
181
+ return this.getStatus();
182
+ }
183
+
184
+ this._running = true;
185
+ const nowIso = new Date().toISOString();
186
+ this._setStatus({
187
+ state: 'checking',
188
+ last_checked_at: nowIso,
189
+ last_error: null,
190
+ defer_reason: null
191
+ });
192
+
193
+ try {
194
+ const result = await checkForUpdate(this.currentVersion);
195
+ if (result.error) {
196
+ this._setStatus({
197
+ state: 'failed',
198
+ latest_version: this.currentVersion,
199
+ last_error: result.error
200
+ });
201
+ return this.getStatus();
202
+ }
203
+
204
+ const latest = result.latest || this.currentVersion;
205
+ if (!result.available) {
206
+ this._setStatus({
207
+ state: 'up_to_date',
208
+ latest_version: latest,
209
+ target_version: null
210
+ });
211
+ return this.getStatus();
212
+ }
213
+
214
+ if (!shouldApplyUpdate(this.currentVersion, latest, { allowMajor: this.allowMajor })) {
215
+ this._setStatus({
216
+ state: 'up_to_date',
217
+ latest_version: latest,
218
+ target_version: null,
219
+ defer_reason: 'cross_major_blocked'
220
+ });
221
+ return this.getStatus();
222
+ }
223
+
224
+ const monitor = this.getCallMonitor();
225
+ const activeCalls = monitor && typeof monitor.getActiveCount === 'function'
226
+ ? Number(monitor.getActiveCount()) || 0
227
+ : 0;
228
+ if (!isUpdateSafe(monitor) && !options.force) {
229
+ this._setStatus({
230
+ state: 'waiting_for_safe_restart',
231
+ latest_version: latest,
232
+ target_version: latest,
233
+ active_calls: activeCalls,
234
+ defer_reason: 'active_calls'
235
+ });
236
+ return this.getStatus();
237
+ }
238
+
239
+ this._setStatus({
240
+ state: 'downloading',
241
+ latest_version: latest,
242
+ target_version: latest,
243
+ active_calls: activeCalls
244
+ });
245
+
246
+ await this._installVersion(latest);
247
+ this.currentVersion = latest;
248
+ this._persistConfigPatch({ lastGoodVersion: latest });
249
+
250
+ this._setStatus({
251
+ state: 'restarting',
252
+ current_version: latest,
253
+ latest_version: latest,
254
+ target_version: latest,
255
+ last_success_at: new Date().toISOString(),
256
+ last_error: null,
257
+ defer_reason: null,
258
+ active_calls: 0
259
+ });
260
+
261
+ if (this.restartFn) {
262
+ await this.restartFn(latest);
263
+ } else {
264
+ this._setStatus({
265
+ state: 'up_to_date',
266
+ current_version: latest,
267
+ latest_version: latest,
268
+ target_version: null
269
+ });
270
+ }
271
+
272
+ return this.getStatus();
273
+ } catch (err) {
274
+ const message = err && err.message ? err.message : 'update_failed';
275
+ this._setStatus({
276
+ state: 'failed',
277
+ last_error: message
278
+ });
279
+ this.logger.error('Auto-update failed', {
280
+ event: 'updater_failed',
281
+ error: err
282
+ });
283
+ return this.getStatus();
284
+ } finally {
285
+ this._running = false;
286
+ this._pendingManualUpdate = false;
287
+ }
288
+ }
289
+
290
+ async _installVersion(version) {
291
+ const args = ['install', '-g', `a2acalling@${version}`];
292
+ this._setStatus({ state: 'applying' });
293
+ this.logger.info('Installing auto-update target', {
294
+ event: 'updater_install_start',
295
+ data: { version }
296
+ });
297
+ await this._execFile('npm', args, {
298
+ timeout: this.installTimeoutMs,
299
+ env: process.env
300
+ });
301
+ this.logger.info('Auto-update install complete', {
302
+ event: 'updater_install_done',
303
+ data: { version }
304
+ });
305
+ }
306
+ }
307
+
308
+ module.exports = {
309
+ DEFAULT_INTERVAL_MS,
310
+ isUpdateSafe,
311
+ shouldApplyUpdate,
312
+ UpdateManager
313
+ };
package/src/routes/a2a.js CHANGED
@@ -174,17 +174,24 @@ function createRoutes(options = {}) {
174
174
  maxDurationMs: options.maxDurationMs || 300000,
175
175
  logger: logger.child({ component: 'a2a.call-monitor' })
176
176
  });
177
+ if (typeof options.onCallMonitor === 'function') {
178
+ try {
179
+ options.onCallMonitor(monitor);
180
+ } catch (_) {}
181
+ }
177
182
 
178
183
  /**
179
184
  * GET /status
180
185
  * Check if A2A is enabled
181
186
  */
182
187
  router.get('/status', (req, res) => {
188
+ const activeCalls = monitor ? monitor.getActiveCount() : 0;
183
189
  res.json({
184
190
  a2a: true,
185
191
  version: require('../../package.json').version,
186
192
  capabilities: ['invoke', 'multi-turn'],
187
- rate_limits: limits
193
+ rate_limits: limits,
194
+ active_calls: activeCalls
188
195
  });
189
196
  });
190
197
 
@@ -333,6 +340,7 @@ function createRoutes(options = {}) {
333
340
  tier: validation.tier,
334
341
  capabilities: validation.capabilities,
335
342
  allowed_topics: validation.allowed_topics,
343
+ timeout_ms: validation.timeout_ms,
336
344
  disclosure: validation.disclosure,
337
345
  caller: sanitizedCaller,
338
346
  conversation_id: conversation_id || `conv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
@@ -210,12 +210,45 @@ function buildContext(options = {}) {
210
210
  config,
211
211
  convStore,
212
212
  callbookStore,
213
+ getUpdateManager: typeof options.getUpdateManager === 'function'
214
+ ? options.getUpdateManager
215
+ : (() => null),
213
216
  logger,
214
217
  agentContext,
215
218
  staticDir: DASHBOARD_STATIC_DIR
216
219
  };
217
220
  }
218
221
 
222
+ function resolveAutoUpdateStatus(context) {
223
+ const manager = context.getUpdateManager ? context.getUpdateManager() : null;
224
+ const config = context.config && typeof context.config.getAutoUpdate === 'function'
225
+ ? context.config.getAutoUpdate()
226
+ : {
227
+ enabled: true,
228
+ intervalMs: 60 * 60 * 1000,
229
+ allowMajor: false,
230
+ lastGoodVersion: null
231
+ };
232
+ const runtime = manager && typeof manager.getStatus === 'function'
233
+ ? manager.getStatus()
234
+ : null;
235
+ return {
236
+ enabled: runtime ? Boolean(runtime.enabled) : Boolean(config.enabled),
237
+ interval_ms: runtime && Number.isFinite(runtime.interval_ms) ? runtime.interval_ms : config.intervalMs,
238
+ allow_major: runtime ? Boolean(runtime.allow_major) : Boolean(config.allowMajor),
239
+ state: runtime ? runtime.state : 'up_to_date',
240
+ current_version: runtime ? runtime.current_version : require('../../package.json').version,
241
+ latest_version: runtime ? runtime.latest_version : null,
242
+ target_version: runtime ? runtime.target_version : null,
243
+ active_calls: runtime ? runtime.active_calls : 0,
244
+ last_checked_at: runtime ? runtime.last_checked_at : null,
245
+ last_success_at: runtime ? runtime.last_success_at : null,
246
+ last_error: runtime ? runtime.last_error : null,
247
+ defer_reason: runtime ? runtime.defer_reason : null,
248
+ last_good_version: config.lastGoodVersion || null
249
+ };
250
+ }
251
+
219
252
  function buildContactIndex(contacts) {
220
253
  const byName = new Map();
221
254
  const byId = new Map();
@@ -537,7 +570,76 @@ function createDashboardApiRouter(options = {}) {
537
570
  callbook: {
538
571
  enabled: Boolean(context.callbookStore && context.callbookStore.isAvailable()),
539
572
  device_count: Array.isArray(devices) ? devices.length : 0
540
- }
573
+ },
574
+ auto_update: resolveAutoUpdateStatus(context)
575
+ });
576
+ });
577
+
578
+ router.get('/update/status', (req, res) => {
579
+ return res.json({
580
+ success: true,
581
+ auto_update: resolveAutoUpdateStatus(context)
582
+ });
583
+ });
584
+
585
+ router.post('/update/check', async (req, res) => {
586
+ const manager = context.getUpdateManager ? context.getUpdateManager() : null;
587
+ if (!manager || typeof manager.triggerCheck !== 'function') {
588
+ return res.status(503).json({
589
+ success: false,
590
+ error: 'updater_unavailable',
591
+ message: 'Auto-updater is not initialized for this server.'
592
+ });
593
+ }
594
+ await manager.triggerCheck({ reason: 'dashboard_manual_check', forceCheck: true });
595
+ return res.json({
596
+ success: true,
597
+ auto_update: resolveAutoUpdateStatus(context)
598
+ });
599
+ });
600
+
601
+ router.post('/update/now', async (req, res) => {
602
+ const manager = context.getUpdateManager ? context.getUpdateManager() : null;
603
+ if (!manager || typeof manager.triggerUpdate !== 'function') {
604
+ return res.status(503).json({
605
+ success: false,
606
+ error: 'updater_unavailable',
607
+ message: 'Auto-updater is not initialized for this server.'
608
+ });
609
+ }
610
+ const force = parseBoolean(req.body && (req.body.force !== undefined ? req.body.force : req.body.force_update));
611
+ await manager.triggerUpdate({
612
+ reason: 'dashboard_manual_update',
613
+ force
614
+ });
615
+ return res.json({
616
+ success: true,
617
+ auto_update: resolveAutoUpdateStatus(context)
618
+ });
619
+ });
620
+
621
+ router.put('/update/config', async (req, res) => {
622
+ const manager = context.getUpdateManager ? context.getUpdateManager() : null;
623
+ const body = req.body || {};
624
+ const enabled = body.enabled !== undefined ? parseBoolean(body.enabled) : undefined;
625
+
626
+ if (enabled === undefined) {
627
+ return res.status(400).json({
628
+ success: false,
629
+ error: 'enabled_required',
630
+ message: 'Pass { enabled: true|false }.'
631
+ });
632
+ }
633
+
634
+ if (manager && typeof manager.setEnabled === 'function') {
635
+ await manager.setEnabled(enabled);
636
+ } else if (context.config && typeof context.config.setAutoUpdate === 'function') {
637
+ context.config.setAutoUpdate({ enabled });
638
+ }
639
+
640
+ return res.json({
641
+ success: true,
642
+ auto_update: resolveAutoUpdateStatus(context)
541
643
  });
542
644
  });
543
645