a2acalling 0.6.50 → 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.
- package/bin/cli.js +172 -0
- package/native/macos/index.html +5 -10
- package/native/macos/src-tauri/Cargo.lock +5875 -0
- package/native/macos/src-tauri/Cargo.toml +3 -2
- package/native/macos/src-tauri/icons/128x128.png +0 -0
- package/native/macos/src-tauri/icons/128x128@2x.png +0 -0
- package/native/macos/src-tauri/icons/32x32.png +0 -0
- package/native/macos/src-tauri/icons/tray-connected.png +0 -0
- package/native/macos/src-tauri/icons/tray-disconnected.png +0 -0
- package/native/macos/src-tauri/src/lib.rs +2 -2
- package/native/macos/src-tauri/src/notifications.rs +147 -68
- package/native/macos/src-tauri/tauri.conf.json +2 -9
- package/package.json +1 -1
- package/scripts/postinstall.js +0 -49
- package/src/dashboard/public/app.js +147 -1
- package/src/lib/conversations.js +55 -1
- package/src/lib/dashboard-events.js +205 -0
- package/src/routes/a2a.js +25 -4
- package/src/routes/dashboard.js +114 -1
- package/src/server.js +4 -0
|
@@ -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
|
+
};
|
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
|
-
|
|
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,6 +374,17 @@ 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) {
|
|
@@ -563,7 +581,10 @@ function createRoutes(options = {}) {
|
|
|
563
581
|
}));
|
|
564
582
|
}
|
|
565
583
|
|
|
566
|
-
const convStore = getConversationStore(
|
|
584
|
+
const convStore = getConversationStore({
|
|
585
|
+
eventStore,
|
|
586
|
+
configDir: tokenStore.configDir
|
|
587
|
+
});
|
|
567
588
|
if (!convStore) {
|
|
568
589
|
return res.json(withTracePayload({ success: true, message: 'Conversation storage not enabled' }));
|
|
569
590
|
}
|
package/src/routes/dashboard.js
CHANGED
|
@@ -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,
|
|
@@ -833,6 +835,7 @@ app.use('/api/a2a/dashboard', createDashboardApiRouter({
|
|
|
833
835
|
tokenStore,
|
|
834
836
|
agentContext,
|
|
835
837
|
config,
|
|
838
|
+
eventStore,
|
|
836
839
|
getUpdateManager: () => updateManager,
|
|
837
840
|
logger: logger.child({ component: 'a2a.dashboard' })
|
|
838
841
|
}));
|
|
@@ -858,6 +861,7 @@ app.use('/callbook', createCallbookRouter());
|
|
|
858
861
|
|
|
859
862
|
app.use('/api/a2a', createRoutes({
|
|
860
863
|
tokenStore,
|
|
864
|
+
eventStore,
|
|
861
865
|
logger: logger.child({ component: 'a2a.routes' }),
|
|
862
866
|
onCallMonitor: (monitor) => {
|
|
863
867
|
activeCallMonitor = monitor;
|