@pingagent/sdk 0.1.13 → 0.1.15

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,4825 @@
1
+ // src/client.ts
2
+ import { ERROR_HINTS, ErrorCode as ErrorCode2, SCHEMA_TASK as SCHEMA_TASK2, SCHEMA_CONTACT_REQUEST as SCHEMA_CONTACT_REQUEST2, SCHEMA_RESULT as SCHEMA_RESULT2, SCHEMA_TEXT as SCHEMA_TEXT2 } from "@pingagent/schemas";
3
+ import {
4
+ buildUnsignedEnvelope,
5
+ signEnvelope
6
+ } from "@pingagent/protocol";
7
+
8
+ // src/transport.ts
9
+ import { ErrorCode } from "@pingagent/schemas";
10
+ var HttpTransport = class {
11
+ serverUrl;
12
+ accessToken;
13
+ maxRetries;
14
+ onTokenRefreshed;
15
+ constructor(opts) {
16
+ this.serverUrl = opts.serverUrl.replace(/\/$/, "");
17
+ this.accessToken = opts.accessToken;
18
+ this.maxRetries = opts.maxRetries ?? 3;
19
+ this.onTokenRefreshed = opts.onTokenRefreshed;
20
+ }
21
+ setToken(token) {
22
+ this.accessToken = token;
23
+ }
24
+ /** Fetch a URL with Bearer auth (e.g. artifact upload/download). */
25
+ async fetchWithAuth(url, options = {}) {
26
+ const headers = { Authorization: `Bearer ${this.accessToken}` };
27
+ if (options.contentType) headers["Content-Type"] = options.contentType;
28
+ const res = await fetch(url, {
29
+ method: options.method ?? "GET",
30
+ headers,
31
+ body: options.body ?? void 0
32
+ });
33
+ if (!res.ok) {
34
+ const text = await res.text();
35
+ throw new Error(`Request failed (${res.status}): ${text.slice(0, 200)}`);
36
+ }
37
+ return res.arrayBuffer();
38
+ }
39
+ async request(method, path7, body, skipAuth = false) {
40
+ const url = `${this.serverUrl}${path7}`;
41
+ const headers = {
42
+ "Content-Type": "application/json"
43
+ };
44
+ if (!skipAuth) {
45
+ headers["Authorization"] = `Bearer ${this.accessToken}`;
46
+ }
47
+ let lastError = null;
48
+ const delays = [1e3, 2e3, 4e3];
49
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
50
+ try {
51
+ const res = await fetch(url, {
52
+ method,
53
+ headers,
54
+ body: body ? JSON.stringify(body) : void 0
55
+ });
56
+ const text = await res.text();
57
+ let data;
58
+ try {
59
+ data = text ? JSON.parse(text) : {};
60
+ } catch {
61
+ throw new Error(`Server returned non-JSON (${res.status}): ${text.slice(0, 200)}`);
62
+ }
63
+ if (res.status === 401 && data.error?.code === ErrorCode.TOKEN_EXPIRED && attempt === 0) {
64
+ const refreshed = await this.refreshToken();
65
+ if (refreshed) {
66
+ headers["Authorization"] = `Bearer ${this.accessToken}`;
67
+ continue;
68
+ }
69
+ }
70
+ if (res.status === 429 && data.error?.retry_after_ms) {
71
+ await sleep(data.error.retry_after_ms);
72
+ continue;
73
+ }
74
+ if (res.status >= 500 && attempt < this.maxRetries) {
75
+ await sleep(delays[attempt] ?? 4e3);
76
+ continue;
77
+ }
78
+ return data;
79
+ } catch (e) {
80
+ lastError = e;
81
+ if (attempt < this.maxRetries) {
82
+ await sleep(delays[attempt] ?? 4e3);
83
+ }
84
+ }
85
+ }
86
+ throw lastError ?? new Error("Request failed after retries");
87
+ }
88
+ async refreshToken() {
89
+ try {
90
+ const res = await fetch(`${this.serverUrl}/v1/auth/refresh`, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({ access_token: this.accessToken })
94
+ });
95
+ if (!res.ok) return false;
96
+ const text = await res.text();
97
+ let data;
98
+ try {
99
+ data = text ? JSON.parse(text) : {};
100
+ } catch {
101
+ return false;
102
+ }
103
+ if (!data.ok || !data.data) return false;
104
+ this.accessToken = data.data.access_token;
105
+ const expiresAt = Date.now() + data.data.expires_ms;
106
+ this.onTokenRefreshed?.(data.data.access_token, expiresAt);
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+ };
113
+ function sleep(ms) {
114
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
115
+ }
116
+
117
+ // src/identity.ts
118
+ import * as fs from "fs";
119
+ import * as path2 from "path";
120
+ import { generateKeyPairSync } from "crypto";
121
+ import { generateIdentity as genId } from "@pingagent/protocol";
122
+
123
+ // src/paths.ts
124
+ import * as path from "path";
125
+ import * as os from "os";
126
+ var DEFAULT_ROOT_DIR = path.join(os.homedir(), ".pingagent");
127
+ function getRootDir() {
128
+ return process.env.PINGAGENT_ROOT_DIR || DEFAULT_ROOT_DIR;
129
+ }
130
+ function getProfile() {
131
+ const p = process.env.PINGAGENT_PROFILE;
132
+ if (!p) return void 0;
133
+ return p.trim() || void 0;
134
+ }
135
+ function getIdentityPath(explicitPath) {
136
+ const envPath = process.env.PINGAGENT_IDENTITY_PATH?.trim();
137
+ if (explicitPath) return explicitPath;
138
+ if (envPath) return path.resolve(envPath.replace(/^~(?=\/|$)/, os.homedir()));
139
+ const root = getRootDir();
140
+ const profile = getProfile();
141
+ if (!profile) {
142
+ return path.join(root, "identity.json");
143
+ }
144
+ return path.join(root, "profiles", profile, "identity.json");
145
+ }
146
+ function getStorePath(explicitPath) {
147
+ const envPath = process.env.PINGAGENT_STORE_PATH?.trim();
148
+ if (explicitPath) return explicitPath;
149
+ if (envPath) return path.resolve(envPath.replace(/^~(?=\/|$)/, os.homedir()));
150
+ const root = getRootDir();
151
+ const profile = getProfile();
152
+ if (!profile) {
153
+ return path.join(root, "store.db");
154
+ }
155
+ return path.join(root, "profiles", profile, "store.db");
156
+ }
157
+
158
+ // src/identity.ts
159
+ function toBase64(bytes) {
160
+ let binary = "";
161
+ for (const b of bytes) binary += String.fromCharCode(b);
162
+ return btoa(binary);
163
+ }
164
+ function fromBase64(b64) {
165
+ const binary = atob(b64);
166
+ const bytes = new Uint8Array(binary.length);
167
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
168
+ return bytes;
169
+ }
170
+ function generateEncryptionKeyPair() {
171
+ const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
172
+ const privateJwk = privateKey.export({ format: "jwk" });
173
+ const publicJwk = publicKey.export({ format: "jwk" });
174
+ return {
175
+ encryptionPublicKeyJwk: {
176
+ kty: "EC",
177
+ crv: "P-256",
178
+ x: publicJwk.x,
179
+ y: publicJwk.y
180
+ },
181
+ encryptionPrivateKeyJwk: {
182
+ kty: "EC",
183
+ crv: "P-256",
184
+ x: privateJwk.x,
185
+ y: privateJwk.y,
186
+ d: privateJwk.d
187
+ }
188
+ };
189
+ }
190
+ function ensureIdentityEncryptionKeys(identity) {
191
+ if (identity.encryptionPublicKeyJwk && identity.encryptionPrivateKeyJwk) {
192
+ return {
193
+ encryptionPublicKeyJwk: identity.encryptionPublicKeyJwk,
194
+ encryptionPrivateKeyJwk: identity.encryptionPrivateKeyJwk
195
+ };
196
+ }
197
+ return generateEncryptionKeyPair();
198
+ }
199
+ function generateIdentity() {
200
+ const signingIdentity = genId();
201
+ return {
202
+ ...signingIdentity,
203
+ ...ensureIdentityEncryptionKeys({})
204
+ };
205
+ }
206
+ function identityExists(identityPath) {
207
+ return fs.existsSync(getIdentityPath(identityPath));
208
+ }
209
+ function loadIdentity(identityPath) {
210
+ const p = getIdentityPath(identityPath);
211
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
212
+ const encryption = ensureIdentityEncryptionKeys({
213
+ encryptionPublicKeyJwk: data.encryption_public_key,
214
+ encryptionPrivateKeyJwk: data.encryption_private_key
215
+ });
216
+ if (!data.encryption_public_key || !data.encryption_private_key) {
217
+ data.encryption_public_key = encryption.encryptionPublicKeyJwk;
218
+ data.encryption_private_key = encryption.encryptionPrivateKeyJwk;
219
+ fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 384 });
220
+ }
221
+ return {
222
+ publicKey: fromBase64(data.public_key),
223
+ privateKey: fromBase64(data.private_key),
224
+ ...encryption,
225
+ did: data.did,
226
+ deviceId: data.device_id,
227
+ serverUrl: data.server_url,
228
+ accessToken: data.access_token,
229
+ tokenExpiresAt: data.token_expires_at,
230
+ mode: data.mode
231
+ };
232
+ }
233
+ function saveIdentity(identity, opts, identityPath) {
234
+ const p = getIdentityPath(identityPath);
235
+ const dir = path2.dirname(p);
236
+ if (!fs.existsSync(dir)) {
237
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
238
+ }
239
+ const encryption = ensureIdentityEncryptionKeys(identity);
240
+ const stored = {
241
+ public_key: toBase64(identity.publicKey),
242
+ private_key: toBase64(identity.privateKey),
243
+ encryption_public_key: encryption.encryptionPublicKeyJwk,
244
+ encryption_private_key: encryption.encryptionPrivateKeyJwk,
245
+ did: identity.did,
246
+ device_id: identity.deviceId,
247
+ server_url: opts?.serverUrl,
248
+ access_token: opts?.accessToken,
249
+ token_expires_at: opts?.tokenExpiresAt,
250
+ mode: opts?.mode,
251
+ alias: opts?.alias
252
+ };
253
+ fs.writeFileSync(p, JSON.stringify(stored, null, 2), { mode: 384 });
254
+ }
255
+ function updateStoredToken(accessToken, expiresAt, identityPath) {
256
+ const p = getIdentityPath(identityPath);
257
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
258
+ data.access_token = accessToken;
259
+ data.token_expires_at = expiresAt;
260
+ fs.writeFileSync(p, JSON.stringify(data, null, 2), { mode: 384 });
261
+ }
262
+
263
+ // src/contacts.ts
264
+ function rowToContact(row) {
265
+ return {
266
+ did: row.did,
267
+ alias: row.alias ?? void 0,
268
+ display_name: row.display_name ?? void 0,
269
+ notes: row.notes ?? void 0,
270
+ conversation_id: row.conversation_id ?? void 0,
271
+ trusted: row.trusted === 1,
272
+ added_at: row.added_at,
273
+ last_message_at: row.last_message_at ?? void 0,
274
+ tags: row.tags ? JSON.parse(row.tags) : void 0
275
+ };
276
+ }
277
+ var ContactManager = class {
278
+ store;
279
+ constructor(store) {
280
+ this.store = store;
281
+ }
282
+ add(contact) {
283
+ const db = this.store.getDb();
284
+ const now = contact.added_at ?? Date.now();
285
+ db.prepare(`
286
+ INSERT INTO contacts (did, alias, display_name, notes, conversation_id, trusted, added_at, last_message_at, tags)
287
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
288
+ ON CONFLICT(did) DO UPDATE SET
289
+ alias = COALESCE(excluded.alias, contacts.alias),
290
+ display_name = COALESCE(excluded.display_name, contacts.display_name),
291
+ notes = COALESCE(excluded.notes, contacts.notes),
292
+ conversation_id = COALESCE(excluded.conversation_id, contacts.conversation_id),
293
+ trusted = excluded.trusted,
294
+ last_message_at = COALESCE(excluded.last_message_at, contacts.last_message_at),
295
+ tags = COALESCE(excluded.tags, contacts.tags)
296
+ `).run(
297
+ contact.did,
298
+ contact.alias ?? null,
299
+ contact.display_name ?? null,
300
+ contact.notes ?? null,
301
+ contact.conversation_id ?? null,
302
+ contact.trusted ? 1 : 0,
303
+ now,
304
+ contact.last_message_at ?? null,
305
+ contact.tags ? JSON.stringify(contact.tags) : null
306
+ );
307
+ return { ...contact, added_at: now };
308
+ }
309
+ remove(did) {
310
+ const result = this.store.getDb().prepare("DELETE FROM contacts WHERE did = ?").run(did);
311
+ return result.changes > 0;
312
+ }
313
+ get(did) {
314
+ const row = this.store.getDb().prepare("SELECT * FROM contacts WHERE did = ?").get(did);
315
+ return row ? rowToContact(row) : null;
316
+ }
317
+ update(did, updates) {
318
+ const existing = this.get(did);
319
+ if (!existing) return null;
320
+ const fields = [];
321
+ const values = [];
322
+ if (updates.alias !== void 0) {
323
+ fields.push("alias = ?");
324
+ values.push(updates.alias);
325
+ }
326
+ if (updates.display_name !== void 0) {
327
+ fields.push("display_name = ?");
328
+ values.push(updates.display_name);
329
+ }
330
+ if (updates.notes !== void 0) {
331
+ fields.push("notes = ?");
332
+ values.push(updates.notes);
333
+ }
334
+ if (updates.conversation_id !== void 0) {
335
+ fields.push("conversation_id = ?");
336
+ values.push(updates.conversation_id);
337
+ }
338
+ if (updates.trusted !== void 0) {
339
+ fields.push("trusted = ?");
340
+ values.push(updates.trusted ? 1 : 0);
341
+ }
342
+ if (updates.last_message_at !== void 0) {
343
+ fields.push("last_message_at = ?");
344
+ values.push(updates.last_message_at);
345
+ }
346
+ if (updates.tags !== void 0) {
347
+ fields.push("tags = ?");
348
+ values.push(JSON.stringify(updates.tags));
349
+ }
350
+ if (fields.length === 0) return existing;
351
+ values.push(did);
352
+ this.store.getDb().prepare(`UPDATE contacts SET ${fields.join(", ")} WHERE did = ?`).run(...values);
353
+ return this.get(did);
354
+ }
355
+ list(opts) {
356
+ const conditions = [];
357
+ const params = [];
358
+ if (opts?.trusted !== void 0) {
359
+ conditions.push("trusted = ?");
360
+ params.push(opts.trusted ? 1 : 0);
361
+ }
362
+ if (opts?.tag) {
363
+ conditions.push("tags LIKE ?");
364
+ params.push(`%"${opts.tag}"%`);
365
+ }
366
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
367
+ const limit = opts?.limit ? `LIMIT ${opts.limit}` : "";
368
+ const offset = opts?.offset ? `OFFSET ${opts.offset}` : "";
369
+ const rows = this.store.getDb().prepare(`SELECT * FROM contacts ${where} ORDER BY added_at DESC ${limit} ${offset}`).all(...params);
370
+ return rows.map(rowToContact);
371
+ }
372
+ search(query) {
373
+ const pattern = `%${query}%`;
374
+ const rows = this.store.getDb().prepare(`
375
+ SELECT * FROM contacts
376
+ WHERE did LIKE ? OR alias LIKE ? OR display_name LIKE ? OR notes LIKE ?
377
+ ORDER BY added_at DESC
378
+ `).all(pattern, pattern, pattern, pattern);
379
+ return rows.map(rowToContact);
380
+ }
381
+ export(format = "json") {
382
+ const contacts = this.list();
383
+ if (format === "csv") {
384
+ const header = "did,alias,display_name,notes,conversation_id,trusted,added_at,last_message_at,tags";
385
+ const rows = contacts.map(
386
+ (c) => [
387
+ c.did,
388
+ c.alias ?? "",
389
+ c.display_name ?? "",
390
+ c.notes ?? "",
391
+ c.conversation_id ?? "",
392
+ c.trusted,
393
+ c.added_at,
394
+ c.last_message_at ?? "",
395
+ c.tags ? JSON.stringify(c.tags) : ""
396
+ ].map((v) => `"${String(v).replace(/"/g, '""')}"`).join(",")
397
+ );
398
+ return [header, ...rows].join("\n");
399
+ }
400
+ return JSON.stringify(contacts, null, 2);
401
+ }
402
+ import(data, format = "json") {
403
+ let imported = 0;
404
+ let skipped = 0;
405
+ if (format === "json") {
406
+ const contacts = JSON.parse(data);
407
+ for (const c of contacts) {
408
+ try {
409
+ this.add(c);
410
+ imported++;
411
+ } catch {
412
+ skipped++;
413
+ }
414
+ }
415
+ } else {
416
+ const lines = data.split("\n").filter((l) => l.trim());
417
+ for (let i = 1; i < lines.length; i++) {
418
+ try {
419
+ const cols = parseCsvLine(lines[i]);
420
+ this.add({
421
+ did: cols[0],
422
+ alias: cols[1] || void 0,
423
+ display_name: cols[2] || void 0,
424
+ notes: cols[3] || void 0,
425
+ conversation_id: cols[4] || void 0,
426
+ trusted: cols[5] === "1" || cols[5] === "true",
427
+ added_at: parseInt(cols[6]) || Date.now(),
428
+ last_message_at: cols[7] ? parseInt(cols[7]) : void 0,
429
+ tags: cols[8] ? JSON.parse(cols[8]) : void 0
430
+ });
431
+ imported++;
432
+ } catch {
433
+ skipped++;
434
+ }
435
+ }
436
+ }
437
+ return { imported, skipped };
438
+ }
439
+ };
440
+ function parseCsvLine(line) {
441
+ const result = [];
442
+ let current = "";
443
+ let inQuotes = false;
444
+ for (let i = 0; i < line.length; i++) {
445
+ const ch = line[i];
446
+ if (inQuotes) {
447
+ if (ch === '"' && line[i + 1] === '"') {
448
+ current += '"';
449
+ i++;
450
+ } else if (ch === '"') {
451
+ inQuotes = false;
452
+ } else {
453
+ current += ch;
454
+ }
455
+ } else {
456
+ if (ch === '"') {
457
+ inQuotes = true;
458
+ } else if (ch === ",") {
459
+ result.push(current);
460
+ current = "";
461
+ } else {
462
+ current += ch;
463
+ }
464
+ }
465
+ }
466
+ result.push(current);
467
+ return result;
468
+ }
469
+
470
+ // src/history.ts
471
+ function rowToMessage(row) {
472
+ return {
473
+ conversation_id: row.conversation_id,
474
+ message_id: row.message_id,
475
+ seq: row.seq ?? void 0,
476
+ sender_did: row.sender_did,
477
+ schema: row.schema,
478
+ payload: JSON.parse(row.payload),
479
+ ts_ms: row.ts_ms,
480
+ direction: row.direction
481
+ };
482
+ }
483
+ var HistoryManager = class {
484
+ store;
485
+ constructor(store) {
486
+ this.store = store;
487
+ }
488
+ save(messages) {
489
+ const db = this.store.getDb();
490
+ const stmt = db.prepare(`
491
+ INSERT INTO messages (conversation_id, message_id, seq, sender_did, schema, payload, ts_ms, direction)
492
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
493
+ ON CONFLICT(message_id) DO UPDATE SET
494
+ seq = COALESCE(excluded.seq, messages.seq),
495
+ payload = excluded.payload,
496
+ ts_ms = excluded.ts_ms
497
+ `);
498
+ let count = 0;
499
+ const tx = db.transaction(() => {
500
+ for (const msg of messages) {
501
+ stmt.run(
502
+ msg.conversation_id,
503
+ msg.message_id,
504
+ msg.seq ?? null,
505
+ msg.sender_did,
506
+ msg.schema,
507
+ JSON.stringify(msg.payload),
508
+ msg.ts_ms,
509
+ msg.direction
510
+ );
511
+ count++;
512
+ }
513
+ });
514
+ tx();
515
+ return count;
516
+ }
517
+ list(conversationId, opts) {
518
+ const conditions = ["conversation_id = ?"];
519
+ const params = [conversationId];
520
+ if (opts?.beforeSeq !== void 0) {
521
+ conditions.push("seq < ?");
522
+ params.push(opts.beforeSeq);
523
+ }
524
+ if (opts?.afterSeq !== void 0) {
525
+ conditions.push("seq > ?");
526
+ params.push(opts.afterSeq);
527
+ }
528
+ if (opts?.beforeTsMs !== void 0) {
529
+ conditions.push("ts_ms < ?");
530
+ params.push(opts.beforeTsMs);
531
+ }
532
+ if (opts?.afterTsMs !== void 0) {
533
+ conditions.push("ts_ms > ?");
534
+ params.push(opts.afterTsMs);
535
+ }
536
+ const limit = opts?.limit ?? 100;
537
+ const rows = this.store.getDb().prepare(`SELECT * FROM messages WHERE ${conditions.join(" AND ")} ORDER BY COALESCE(seq, ts_ms) ASC LIMIT ?`).all(...params, limit);
538
+ return rows.map(rowToMessage);
539
+ }
540
+ /**
541
+ * List the most recent N messages *before* a seq (paging older history).
542
+ * Only compares rows where seq is not null.
543
+ */
544
+ listBeforeSeq(conversationId, beforeSeq, limit) {
545
+ const rows = this.store.getDb().prepare(
546
+ `SELECT * FROM messages
547
+ WHERE conversation_id = ? AND seq IS NOT NULL AND seq < ?
548
+ ORDER BY seq DESC
549
+ LIMIT ?`
550
+ ).all(conversationId, beforeSeq, limit);
551
+ return rows.map(rowToMessage).reverse();
552
+ }
553
+ /**
554
+ * List the most recent N messages *before* a timestamp (paging older history).
555
+ */
556
+ listBeforeTs(conversationId, beforeTsMs, limit) {
557
+ const rows = this.store.getDb().prepare(
558
+ `SELECT * FROM messages
559
+ WHERE conversation_id = ? AND ts_ms < ?
560
+ ORDER BY ts_ms DESC
561
+ LIMIT ?`
562
+ ).all(conversationId, beforeTsMs, limit);
563
+ return rows.map(rowToMessage).reverse();
564
+ }
565
+ /** Returns the N most recent messages (chronological order, oldest to newest of those N). */
566
+ listRecent(conversationId, limit) {
567
+ const rows = this.store.getDb().prepare(
568
+ `SELECT * FROM messages WHERE conversation_id = ? ORDER BY COALESCE(seq, ts_ms) DESC LIMIT ?`
569
+ ).all(conversationId, limit);
570
+ return rows.map(rowToMessage).reverse();
571
+ }
572
+ search(query, opts) {
573
+ const conditions = ["payload LIKE ?"];
574
+ const params = [`%${query}%`];
575
+ if (opts?.conversationId) {
576
+ conditions.push("conversation_id = ?");
577
+ params.push(opts.conversationId);
578
+ }
579
+ const limit = opts?.limit ?? 50;
580
+ const rows = this.store.getDb().prepare(`SELECT * FROM messages WHERE ${conditions.join(" AND ")} ORDER BY ts_ms DESC LIMIT ?`).all(...params, limit);
581
+ return rows.map(rowToMessage);
582
+ }
583
+ delete(conversationId) {
584
+ const result = this.store.getDb().prepare("DELETE FROM messages WHERE conversation_id = ?").run(conversationId);
585
+ this.store.getDb().prepare("DELETE FROM sync_state WHERE conversation_id = ?").run(conversationId);
586
+ return result.changes;
587
+ }
588
+ listConversations() {
589
+ const rows = this.store.getDb().prepare(`
590
+ SELECT conversation_id, COUNT(*) as message_count, MAX(ts_ms) as last_message_at
591
+ FROM messages
592
+ GROUP BY conversation_id
593
+ ORDER BY last_message_at DESC
594
+ `).all();
595
+ return rows;
596
+ }
597
+ getLastSyncedSeq(conversationId) {
598
+ const row = this.store.getDb().prepare("SELECT last_synced_seq FROM sync_state WHERE conversation_id = ?").get(conversationId);
599
+ return row?.last_synced_seq ?? 0;
600
+ }
601
+ setLastSyncedSeq(conversationId, seq) {
602
+ this.store.getDb().prepare(`
603
+ INSERT INTO sync_state (conversation_id, last_synced_seq, last_synced_at)
604
+ VALUES (?, ?, ?)
605
+ ON CONFLICT(conversation_id) DO UPDATE SET last_synced_seq = excluded.last_synced_seq, last_synced_at = excluded.last_synced_at
606
+ `).run(conversationId, seq, Date.now());
607
+ }
608
+ export(opts) {
609
+ const format = opts?.format ?? "json";
610
+ let messages;
611
+ if (opts?.conversationId) {
612
+ messages = this.list(opts.conversationId, { limit: 1e5 });
613
+ } else {
614
+ const rows = this.store.getDb().prepare("SELECT * FROM messages ORDER BY ts_ms ASC").all();
615
+ messages = rows.map(rowToMessage);
616
+ }
617
+ if (format === "csv") {
618
+ const header = "conversation_id,message_id,seq,sender_did,schema,payload,ts_ms,direction";
619
+ const lines = messages.map(
620
+ (m) => [
621
+ m.conversation_id,
622
+ m.message_id,
623
+ m.seq ?? "",
624
+ m.sender_did,
625
+ m.schema,
626
+ JSON.stringify(m.payload),
627
+ m.ts_ms,
628
+ m.direction
629
+ ].map((v) => `"${String(v).replace(/"/g, '""')}"`).join(",")
630
+ );
631
+ return [header, ...lines].join("\n");
632
+ }
633
+ return JSON.stringify(messages, null, 2);
634
+ }
635
+ async syncFromServer(client, conversationId, opts) {
636
+ const sinceSeq = opts?.full ? 0 : this.getLastSyncedSeq(conversationId);
637
+ let totalSynced = 0;
638
+ let currentSeq = sinceSeq;
639
+ while (true) {
640
+ const res = await client.fetchInbox(conversationId, {
641
+ sinceSeq: currentSeq,
642
+ limit: 50
643
+ });
644
+ if (!res.ok || !res.data || res.data.messages.length === 0) break;
645
+ const messages = res.data.messages.map((msg) => ({
646
+ conversation_id: conversationId,
647
+ message_id: msg.message_id,
648
+ seq: msg.seq,
649
+ sender_did: msg.sender_did,
650
+ schema: msg.schema,
651
+ payload: msg.payload,
652
+ ts_ms: msg.ts_ms,
653
+ direction: "received"
654
+ }));
655
+ this.save(messages);
656
+ totalSynced += messages.length;
657
+ currentSeq = res.data.next_since_seq;
658
+ this.setLastSyncedSeq(conversationId, currentSeq);
659
+ if (!res.data.has_more) break;
660
+ }
661
+ return { synced: totalSynced };
662
+ }
663
+ async listMergedRecent(client, conversationId, opts) {
664
+ const limit = opts?.limit ?? 50;
665
+ const box = opts?.box ?? "ready";
666
+ const local = this.listRecent(conversationId, limit * 2);
667
+ const remoteRes = await client.fetchInbox(conversationId, { sinceSeq: 0, limit, box: box === "all" ? "ready" : box });
668
+ const remote = remoteRes.ok && remoteRes.data ? remoteRes.data.messages.map((msg) => ({
669
+ conversation_id: conversationId,
670
+ message_id: msg.message_id,
671
+ seq: msg.seq,
672
+ sender_did: msg.sender_did,
673
+ schema: msg.schema,
674
+ payload: msg.payload,
675
+ ts_ms: msg.ts_ms,
676
+ direction: msg.sender_did === client.getDid() ? "sent" : "received"
677
+ })) : [];
678
+ const byId = /* @__PURE__ */ new Map();
679
+ for (const msg of [...local, ...remote]) byId.set(msg.message_id, msg);
680
+ return Array.from(byId.values()).sort((a, b) => (a.seq ?? a.ts_ms) - (b.seq ?? b.ts_ms)).slice(-limit);
681
+ }
682
+ };
683
+
684
+ // src/session.ts
685
+ function rowToSession(row) {
686
+ return {
687
+ session_key: row.session_key,
688
+ remote_did: row.remote_did ?? void 0,
689
+ conversation_id: row.conversation_id ?? void 0,
690
+ trust_state: row.trust_state || "stranger",
691
+ last_message_preview: row.last_message_preview ?? void 0,
692
+ last_remote_activity_at: row.last_remote_activity_at ?? void 0,
693
+ last_read_seq: row.last_read_seq ?? 0,
694
+ unread_count: row.unread_count ?? 0,
695
+ active: row.active === 1,
696
+ updated_at: row.updated_at ?? 0
697
+ };
698
+ }
699
+ function previewFromPayload(payload) {
700
+ if (payload == null) return "";
701
+ if (typeof payload === "string") return payload.slice(0, 280);
702
+ if (typeof payload !== "object") return String(payload).slice(0, 280);
703
+ const value = payload;
704
+ const preferred = value.text ?? value.title ?? value.summary ?? value.message ?? value.description ?? value.error_message;
705
+ if (typeof preferred === "string") return preferred.slice(0, 280);
706
+ return JSON.stringify(payload).slice(0, 280);
707
+ }
708
+ function buildSessionKey(opts) {
709
+ const type = opts.conversationType ?? "";
710
+ if (type === "channel" || type === "group" || opts.conversationId.startsWith("c_grp_")) {
711
+ return `hook:im:conv:${opts.conversationId}`;
712
+ }
713
+ const remote = (opts.remoteDid ?? "").trim();
714
+ if (!remote) {
715
+ return `hook:im:conv:${opts.conversationId}`;
716
+ }
717
+ const local = (opts.localDid ?? "").trim();
718
+ if (!local) {
719
+ return `hook:im:peer:${remote}:conv:${opts.conversationId}`;
720
+ }
721
+ return `hook:im:did:${local}:peer:${remote}`;
722
+ }
723
+ var SessionManager = class {
724
+ constructor(store) {
725
+ this.store = store;
726
+ }
727
+ upsert(state) {
728
+ const now = state.updated_at ?? Date.now();
729
+ const existing = this.get(state.session_key);
730
+ const next = {
731
+ session_key: state.session_key,
732
+ remote_did: state.remote_did ?? existing?.remote_did,
733
+ conversation_id: state.conversation_id ?? existing?.conversation_id,
734
+ trust_state: state.trust_state ?? existing?.trust_state ?? "stranger",
735
+ last_message_preview: state.last_message_preview ?? existing?.last_message_preview,
736
+ last_remote_activity_at: state.last_remote_activity_at ?? existing?.last_remote_activity_at,
737
+ last_read_seq: state.last_read_seq ?? existing?.last_read_seq ?? 0,
738
+ unread_count: state.unread_count ?? existing?.unread_count ?? 0,
739
+ active: state.active ?? existing?.active ?? false,
740
+ updated_at: now
741
+ };
742
+ if (next.active) {
743
+ this.store.getDb().prepare("UPDATE session_state SET active = 0 WHERE session_key != ?").run(next.session_key);
744
+ }
745
+ this.store.getDb().prepare(`
746
+ INSERT INTO session_state (
747
+ session_key, remote_did, conversation_id, trust_state, last_message_preview,
748
+ last_remote_activity_at, last_read_seq, unread_count, active, updated_at
749
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
750
+ ON CONFLICT(session_key) DO UPDATE SET
751
+ remote_did = COALESCE(excluded.remote_did, session_state.remote_did),
752
+ conversation_id = COALESCE(excluded.conversation_id, session_state.conversation_id),
753
+ trust_state = excluded.trust_state,
754
+ last_message_preview = COALESCE(excluded.last_message_preview, session_state.last_message_preview),
755
+ last_remote_activity_at = COALESCE(excluded.last_remote_activity_at, session_state.last_remote_activity_at),
756
+ last_read_seq = excluded.last_read_seq,
757
+ unread_count = excluded.unread_count,
758
+ active = excluded.active,
759
+ updated_at = excluded.updated_at
760
+ `).run(
761
+ next.session_key,
762
+ next.remote_did ?? null,
763
+ next.conversation_id ?? null,
764
+ next.trust_state,
765
+ next.last_message_preview ?? null,
766
+ next.last_remote_activity_at ?? null,
767
+ next.last_read_seq,
768
+ next.unread_count,
769
+ next.active ? 1 : 0,
770
+ next.updated_at
771
+ );
772
+ return next;
773
+ }
774
+ upsertFromMessage(input) {
775
+ const sessionKey = input.session_key ?? buildSessionKey({
776
+ localDid: "",
777
+ remoteDid: input.remote_did ?? input.sender_did ?? "",
778
+ conversationId: input.conversation_id
779
+ });
780
+ const existing = this.get(sessionKey);
781
+ const preview = previewFromPayload(input.payload);
782
+ const isRemote = input.sender_is_self !== true;
783
+ const seq = input.seq ?? 0;
784
+ const lastReadSeq = existing?.last_read_seq ?? 0;
785
+ const unreadCount = isRemote && seq > lastReadSeq ? Math.max(existing?.unread_count ?? 0, seq - lastReadSeq) : existing?.unread_count ?? 0;
786
+ const activity = isRemote ? input.ts_ms ?? Date.now() : existing?.last_remote_activity_at;
787
+ return this.upsert({
788
+ session_key: sessionKey,
789
+ remote_did: input.remote_did ?? existing?.remote_did ?? (isRemote ? input.sender_did : void 0),
790
+ conversation_id: input.conversation_id,
791
+ trust_state: input.trust_state ?? existing?.trust_state ?? "stranger",
792
+ last_message_preview: preview || existing?.last_message_preview,
793
+ last_remote_activity_at: activity,
794
+ last_read_seq: existing?.last_read_seq ?? 0,
795
+ unread_count: unreadCount,
796
+ active: existing?.active ?? false,
797
+ updated_at: input.ts_ms ?? Date.now()
798
+ });
799
+ }
800
+ get(sessionKey) {
801
+ const row = this.store.getDb().prepare("SELECT * FROM session_state WHERE session_key = ?").get(sessionKey);
802
+ return row ? rowToSession(row) : null;
803
+ }
804
+ getByConversationId(conversationId) {
805
+ const row = this.store.getDb().prepare("SELECT * FROM session_state WHERE conversation_id = ? ORDER BY updated_at DESC LIMIT 1").get(conversationId);
806
+ return row ? rowToSession(row) : null;
807
+ }
808
+ getActiveSession() {
809
+ const row = this.store.getDb().prepare("SELECT * FROM session_state WHERE active = 1 ORDER BY updated_at DESC LIMIT 1").get();
810
+ return row ? rowToSession(row) : null;
811
+ }
812
+ listRecentSessions(limit = 20) {
813
+ const rows = this.store.getDb().prepare("SELECT * FROM session_state ORDER BY COALESCE(last_remote_activity_at, updated_at) DESC, updated_at DESC LIMIT ?").all(limit);
814
+ return rows.map(rowToSession);
815
+ }
816
+ focusSession(sessionKey) {
817
+ const session = this.get(sessionKey);
818
+ if (!session) return null;
819
+ this.store.getDb().prepare("UPDATE session_state SET active = CASE WHEN session_key = ? THEN 1 ELSE 0 END").run(sessionKey);
820
+ return this.get(sessionKey);
821
+ }
822
+ markRead(sessionKey, seq) {
823
+ const session = this.get(sessionKey);
824
+ if (!session) return null;
825
+ const lastReadSeq = Math.max(session.last_read_seq, seq ?? session.last_read_seq);
826
+ return this.upsert({
827
+ session_key: sessionKey,
828
+ last_read_seq: lastReadSeq,
829
+ unread_count: 0,
830
+ active: session.active
831
+ });
832
+ }
833
+ nextUnread() {
834
+ const row = this.store.getDb().prepare("SELECT * FROM session_state WHERE unread_count > 0 ORDER BY last_remote_activity_at DESC, updated_at DESC LIMIT 1").get();
835
+ return row ? rowToSession(row) : null;
836
+ }
837
+ resolveReplyTarget(sessionKey) {
838
+ const session = sessionKey ? this.get(sessionKey) : this.getActiveSession();
839
+ if (!session) return null;
840
+ return {
841
+ session_key: session.session_key,
842
+ remote_did: session.remote_did,
843
+ conversation_id: session.conversation_id,
844
+ trust_state: session.trust_state
845
+ };
846
+ }
847
+ };
848
+
849
+ // src/session-summaries.ts
850
+ function rowToSummary(row) {
851
+ return {
852
+ session_key: row.session_key,
853
+ objective: row.objective ?? void 0,
854
+ context: row.context ?? void 0,
855
+ constraints: row.constraints ?? void 0,
856
+ decisions: row.decisions ?? void 0,
857
+ open_questions: row.open_questions ?? void 0,
858
+ next_action: row.next_action ?? void 0,
859
+ handoff_ready_text: row.handoff_ready_text ?? void 0,
860
+ updated_at: row.updated_at
861
+ };
862
+ }
863
+ function cleanText(value) {
864
+ const text = String(value ?? "").trim();
865
+ return text ? text.slice(0, 8e3) : void 0;
866
+ }
867
+ var SESSION_SUMMARY_FIELDS = [
868
+ "objective",
869
+ "context",
870
+ "constraints",
871
+ "decisions",
872
+ "open_questions",
873
+ "next_action",
874
+ "handoff_ready_text"
875
+ ];
876
+ function resolveField(value, existing) {
877
+ if (value === void 0) return existing;
878
+ if (value === null) return void 0;
879
+ const cleaned = cleanText(value);
880
+ return cleaned === void 0 ? void 0 : cleaned;
881
+ }
882
+ function buildSessionSummaryHandoffText(summary) {
883
+ if (!summary) return "";
884
+ if (summary.handoff_ready_text) return summary.handoff_ready_text;
885
+ const parts = [
886
+ summary.objective ? `Objective: ${summary.objective}` : null,
887
+ summary.context ? `Context: ${summary.context}` : null,
888
+ summary.constraints ? `Constraints: ${summary.constraints}` : null,
889
+ summary.decisions ? `Decisions: ${summary.decisions}` : null,
890
+ summary.open_questions ? `Open questions: ${summary.open_questions}` : null,
891
+ summary.next_action ? `Next action: ${summary.next_action}` : null
892
+ ].filter(Boolean);
893
+ return parts.join("\n");
894
+ }
895
+ var SessionSummaryManager = class {
896
+ constructor(store) {
897
+ this.store = store;
898
+ }
899
+ upsert(input) {
900
+ const now = input.updated_at ?? Date.now();
901
+ const existing = this.get(input.session_key);
902
+ const next = {
903
+ session_key: input.session_key,
904
+ objective: resolveField(input.objective, existing?.objective),
905
+ context: resolveField(input.context, existing?.context),
906
+ constraints: resolveField(input.constraints, existing?.constraints),
907
+ decisions: resolveField(input.decisions, existing?.decisions),
908
+ open_questions: resolveField(input.open_questions, existing?.open_questions),
909
+ next_action: resolveField(input.next_action, existing?.next_action),
910
+ handoff_ready_text: resolveField(input.handoff_ready_text, existing?.handoff_ready_text),
911
+ updated_at: now
912
+ };
913
+ this.store.getDb().prepare(`
914
+ INSERT INTO session_summaries (
915
+ session_key, objective, context, constraints, decisions, open_questions, next_action, handoff_ready_text, updated_at
916
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
917
+ ON CONFLICT(session_key) DO UPDATE SET
918
+ objective = excluded.objective,
919
+ context = excluded.context,
920
+ constraints = excluded.constraints,
921
+ decisions = excluded.decisions,
922
+ open_questions = excluded.open_questions,
923
+ next_action = excluded.next_action,
924
+ handoff_ready_text = excluded.handoff_ready_text,
925
+ updated_at = excluded.updated_at
926
+ `).run(
927
+ next.session_key,
928
+ next.objective ?? null,
929
+ next.context ?? null,
930
+ next.constraints ?? null,
931
+ next.decisions ?? null,
932
+ next.open_questions ?? null,
933
+ next.next_action ?? null,
934
+ next.handoff_ready_text ?? null,
935
+ next.updated_at
936
+ );
937
+ return next;
938
+ }
939
+ get(sessionKey) {
940
+ const row = this.store.getDb().prepare("SELECT * FROM session_summaries WHERE session_key = ?").get(sessionKey);
941
+ return row ? rowToSummary(row) : null;
942
+ }
943
+ listRecent(limit = 20) {
944
+ const rows = this.store.getDb().prepare("SELECT * FROM session_summaries ORDER BY updated_at DESC LIMIT ?").all(limit);
945
+ return rows.map(rowToSummary);
946
+ }
947
+ clearFields(sessionKey, fields, updatedAt = Date.now()) {
948
+ const patch = {
949
+ session_key: sessionKey,
950
+ updated_at: updatedAt
951
+ };
952
+ for (const field of fields) patch[field] = null;
953
+ return this.upsert(patch);
954
+ }
955
+ };
956
+
957
+ // src/task-threads.ts
958
+ function rowToThread(row) {
959
+ return {
960
+ task_id: row.task_id,
961
+ session_key: row.session_key,
962
+ conversation_id: row.conversation_id,
963
+ title: row.title ?? void 0,
964
+ status: row.status,
965
+ started_at: row.started_at ?? void 0,
966
+ updated_at: row.updated_at,
967
+ result_summary: row.result_summary ?? void 0,
968
+ error_code: row.error_code ?? void 0,
969
+ error_message: row.error_message ?? void 0
970
+ };
971
+ }
972
+ var TaskThreadManager = class {
973
+ constructor(store) {
974
+ this.store = store;
975
+ }
976
+ upsert(thread) {
977
+ const updatedAt = thread.updated_at ?? Date.now();
978
+ const existing = this.get(thread.task_id);
979
+ this.store.getDb().prepare(`
980
+ INSERT INTO task_threads (
981
+ task_id, session_key, conversation_id, title, status, started_at, updated_at, result_summary, error_code, error_message
982
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
983
+ ON CONFLICT(task_id) DO UPDATE SET
984
+ session_key = excluded.session_key,
985
+ conversation_id = excluded.conversation_id,
986
+ title = COALESCE(excluded.title, task_threads.title),
987
+ status = excluded.status,
988
+ started_at = COALESCE(excluded.started_at, task_threads.started_at),
989
+ updated_at = excluded.updated_at,
990
+ result_summary = COALESCE(excluded.result_summary, task_threads.result_summary),
991
+ error_code = COALESCE(excluded.error_code, task_threads.error_code),
992
+ error_message = COALESCE(excluded.error_message, task_threads.error_message)
993
+ `).run(
994
+ thread.task_id,
995
+ thread.session_key,
996
+ thread.conversation_id,
997
+ thread.title ?? null,
998
+ thread.status,
999
+ thread.started_at ?? existing?.started_at ?? updatedAt,
1000
+ updatedAt,
1001
+ thread.result_summary ?? null,
1002
+ thread.error_code ?? null,
1003
+ thread.error_message ?? null
1004
+ );
1005
+ return this.get(thread.task_id);
1006
+ }
1007
+ get(taskId) {
1008
+ const row = this.store.getDb().prepare("SELECT * FROM task_threads WHERE task_id = ?").get(taskId);
1009
+ return row ? rowToThread(row) : null;
1010
+ }
1011
+ listBySession(sessionKey, limit = 20) {
1012
+ const rows = this.store.getDb().prepare("SELECT * FROM task_threads WHERE session_key = ? ORDER BY updated_at DESC LIMIT ?").all(sessionKey, limit);
1013
+ return rows.map(rowToThread);
1014
+ }
1015
+ listByConversation(conversationId, limit = 20) {
1016
+ const rows = this.store.getDb().prepare("SELECT * FROM task_threads WHERE conversation_id = ? ORDER BY updated_at DESC LIMIT ?").all(conversationId, limit);
1017
+ return rows.map(rowToThread);
1018
+ }
1019
+ listRecent(limit = 20) {
1020
+ const rows = this.store.getDb().prepare("SELECT * FROM task_threads ORDER BY updated_at DESC LIMIT ?").all(limit);
1021
+ return rows.map(rowToThread);
1022
+ }
1023
+ };
1024
+
1025
+ // src/task-handoffs.ts
1026
+ function cleanText2(value) {
1027
+ const text = String(value ?? "").trim();
1028
+ return text ? text.slice(0, 8e3) : void 0;
1029
+ }
1030
+ function rowToHandoff(row) {
1031
+ return {
1032
+ task_id: row.task_id,
1033
+ session_key: row.session_key,
1034
+ conversation_id: row.conversation_id,
1035
+ objective: row.objective ?? void 0,
1036
+ carry_forward_summary: row.carry_forward_summary ?? void 0,
1037
+ success_criteria: row.success_criteria ?? void 0,
1038
+ callback_session_key: row.callback_session_key ?? void 0,
1039
+ priority: row.priority ?? void 0,
1040
+ delegated_by: row.delegated_by ?? void 0,
1041
+ delegated_to: row.delegated_to ?? void 0,
1042
+ updated_at: row.updated_at
1043
+ };
1044
+ }
1045
+ var TaskHandoffManager = class {
1046
+ constructor(store) {
1047
+ this.store = store;
1048
+ }
1049
+ upsert(input) {
1050
+ const now = input.updated_at ?? Date.now();
1051
+ const existing = this.get(input.task_id);
1052
+ const next = {
1053
+ task_id: input.task_id,
1054
+ session_key: input.session_key,
1055
+ conversation_id: input.conversation_id,
1056
+ objective: cleanText2(input.objective) ?? existing?.objective,
1057
+ carry_forward_summary: cleanText2(input.carry_forward_summary) ?? existing?.carry_forward_summary,
1058
+ success_criteria: cleanText2(input.success_criteria) ?? existing?.success_criteria,
1059
+ callback_session_key: cleanText2(input.callback_session_key) ?? existing?.callback_session_key,
1060
+ priority: cleanText2(input.priority) ?? existing?.priority,
1061
+ delegated_by: cleanText2(input.delegated_by) ?? existing?.delegated_by,
1062
+ delegated_to: cleanText2(input.delegated_to) ?? existing?.delegated_to,
1063
+ updated_at: now
1064
+ };
1065
+ this.store.getDb().prepare(`
1066
+ INSERT INTO task_handoffs (
1067
+ task_id, session_key, conversation_id, objective, carry_forward_summary, success_criteria,
1068
+ callback_session_key, priority, delegated_by, delegated_to, updated_at
1069
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1070
+ ON CONFLICT(task_id) DO UPDATE SET
1071
+ session_key = excluded.session_key,
1072
+ conversation_id = excluded.conversation_id,
1073
+ objective = excluded.objective,
1074
+ carry_forward_summary = excluded.carry_forward_summary,
1075
+ success_criteria = excluded.success_criteria,
1076
+ callback_session_key = excluded.callback_session_key,
1077
+ priority = excluded.priority,
1078
+ delegated_by = excluded.delegated_by,
1079
+ delegated_to = excluded.delegated_to,
1080
+ updated_at = excluded.updated_at
1081
+ `).run(
1082
+ next.task_id,
1083
+ next.session_key,
1084
+ next.conversation_id,
1085
+ next.objective ?? null,
1086
+ next.carry_forward_summary ?? null,
1087
+ next.success_criteria ?? null,
1088
+ next.callback_session_key ?? null,
1089
+ next.priority ?? null,
1090
+ next.delegated_by ?? null,
1091
+ next.delegated_to ?? null,
1092
+ next.updated_at
1093
+ );
1094
+ return next;
1095
+ }
1096
+ get(taskId) {
1097
+ const row = this.store.getDb().prepare("SELECT * FROM task_handoffs WHERE task_id = ?").get(taskId);
1098
+ return row ? rowToHandoff(row) : null;
1099
+ }
1100
+ listBySession(sessionKey, limit = 20) {
1101
+ const rows = this.store.getDb().prepare("SELECT * FROM task_handoffs WHERE session_key = ? ORDER BY updated_at DESC LIMIT ?").all(sessionKey, limit);
1102
+ return rows.map(rowToHandoff);
1103
+ }
1104
+ };
1105
+
1106
+ // src/collaboration-projection-outbox.ts
1107
+ function normalizeSummary(value) {
1108
+ const text = String(value ?? "").trim();
1109
+ return text ? text.slice(0, 2e3) : "(no summary)";
1110
+ }
1111
+ function rowToEntry(row) {
1112
+ return {
1113
+ id: row.id,
1114
+ created_at: row.created_at,
1115
+ session_key: row.session_key ?? void 0,
1116
+ conversation_id: row.conversation_id ?? void 0,
1117
+ target_human_session: row.target_human_session ?? void 0,
1118
+ projection_kind: row.projection_kind,
1119
+ summary: row.summary,
1120
+ body: row.body_json ? JSON.parse(row.body_json) : {},
1121
+ status: row.status,
1122
+ attempt_count: row.attempt_count,
1123
+ last_error: row.last_error ?? void 0,
1124
+ delivered_at: row.delivered_at ?? void 0,
1125
+ source_event_id: row.source_event_id ?? void 0
1126
+ };
1127
+ }
1128
+ var CollaborationProjectionOutboxManager = class {
1129
+ constructor(store) {
1130
+ this.store = store;
1131
+ }
1132
+ enqueue(input) {
1133
+ const createdAt = input.created_at ?? Date.now();
1134
+ const result = this.store.getDb().prepare(`
1135
+ INSERT INTO collaboration_projection_outbox (
1136
+ created_at,
1137
+ session_key,
1138
+ conversation_id,
1139
+ target_human_session,
1140
+ projection_kind,
1141
+ summary,
1142
+ body_json,
1143
+ status,
1144
+ attempt_count,
1145
+ last_error,
1146
+ delivered_at,
1147
+ source_event_id
1148
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', 0, NULL, NULL, ?)
1149
+ `).run(
1150
+ createdAt,
1151
+ input.session_key ?? null,
1152
+ input.conversation_id ?? null,
1153
+ input.target_human_session ?? null,
1154
+ input.projection_kind,
1155
+ normalizeSummary(input.summary),
1156
+ JSON.stringify(input.body ?? {}),
1157
+ input.source_event_id ?? null
1158
+ );
1159
+ return {
1160
+ id: Number(result.lastInsertRowid),
1161
+ created_at: createdAt,
1162
+ session_key: input.session_key,
1163
+ conversation_id: input.conversation_id,
1164
+ target_human_session: input.target_human_session,
1165
+ projection_kind: input.projection_kind,
1166
+ summary: normalizeSummary(input.summary),
1167
+ body: input.body ?? {},
1168
+ status: "pending",
1169
+ attempt_count: 0,
1170
+ source_event_id: input.source_event_id
1171
+ };
1172
+ }
1173
+ get(id) {
1174
+ const row = this.store.getDb().prepare("SELECT * FROM collaboration_projection_outbox WHERE id = ?").get(id);
1175
+ return row ? rowToEntry(row) : null;
1176
+ }
1177
+ listRecent(limit = 50) {
1178
+ const rows = this.store.getDb().prepare("SELECT * FROM collaboration_projection_outbox ORDER BY created_at DESC, id DESC LIMIT ?").all(limit);
1179
+ return rows.map(rowToEntry);
1180
+ }
1181
+ listPending(limit = 50) {
1182
+ const rows = this.store.getDb().prepare(`
1183
+ SELECT * FROM collaboration_projection_outbox
1184
+ WHERE status = 'pending'
1185
+ ORDER BY created_at ASC, id ASC
1186
+ LIMIT ?
1187
+ `).all(limit);
1188
+ return rows.map(rowToEntry);
1189
+ }
1190
+ listDispatchable(limit = 50) {
1191
+ const rows = this.store.getDb().prepare(`
1192
+ SELECT * FROM collaboration_projection_outbox
1193
+ WHERE status IN ('pending', 'failed')
1194
+ ORDER BY
1195
+ CASE status WHEN 'pending' THEN 0 ELSE 1 END,
1196
+ created_at ASC,
1197
+ id ASC
1198
+ LIMIT ?
1199
+ `).all(limit);
1200
+ return rows.map(rowToEntry);
1201
+ }
1202
+ listBySession(sessionKey, limit = 50) {
1203
+ const rows = this.store.getDb().prepare(`
1204
+ SELECT * FROM collaboration_projection_outbox
1205
+ WHERE session_key = ?
1206
+ ORDER BY created_at DESC, id DESC
1207
+ LIMIT ?
1208
+ `).all(sessionKey, limit);
1209
+ return rows.map(rowToEntry);
1210
+ }
1211
+ listByStatus(status, limit = 50) {
1212
+ const rows = this.store.getDb().prepare(`
1213
+ SELECT * FROM collaboration_projection_outbox
1214
+ WHERE status = ?
1215
+ ORDER BY created_at DESC, id DESC
1216
+ LIMIT ?
1217
+ `).all(status, limit);
1218
+ return rows.map(rowToEntry);
1219
+ }
1220
+ findBySourceEventId(sourceEventId, projectionKind) {
1221
+ const rows = projectionKind ? this.store.getDb().prepare(`
1222
+ SELECT * FROM collaboration_projection_outbox
1223
+ WHERE source_event_id = ? AND projection_kind = ?
1224
+ ORDER BY created_at DESC, id DESC
1225
+ `).all(sourceEventId, projectionKind) : this.store.getDb().prepare(`
1226
+ SELECT * FROM collaboration_projection_outbox
1227
+ WHERE source_event_id = ?
1228
+ ORDER BY created_at DESC, id DESC
1229
+ `).all(sourceEventId);
1230
+ return rows.map(rowToEntry);
1231
+ }
1232
+ markSent(id, deliveredAt = Date.now()) {
1233
+ this.store.getDb().prepare(`
1234
+ UPDATE collaboration_projection_outbox
1235
+ SET status = 'sent',
1236
+ attempt_count = attempt_count + 1,
1237
+ last_error = NULL,
1238
+ delivered_at = ?
1239
+ WHERE id = ?
1240
+ `).run(deliveredAt, id);
1241
+ return this.get(id);
1242
+ }
1243
+ markFailed(id, error) {
1244
+ this.store.getDb().prepare(`
1245
+ UPDATE collaboration_projection_outbox
1246
+ SET status = 'failed',
1247
+ attempt_count = attempt_count + 1,
1248
+ last_error = ?
1249
+ WHERE id = ?
1250
+ `).run(String(error ?? "unknown error").slice(0, 2e3), id);
1251
+ return this.get(id);
1252
+ }
1253
+ };
1254
+
1255
+ // src/collaboration-events.ts
1256
+ function normalizeSummary2(value) {
1257
+ const text = String(value ?? "").trim();
1258
+ return text ? text.slice(0, 2e3) : "(no summary)";
1259
+ }
1260
+ function rowToEvent(row) {
1261
+ return {
1262
+ id: row.id,
1263
+ ts_ms: row.ts_ms,
1264
+ event_type: row.event_type,
1265
+ severity: row.severity,
1266
+ session_key: row.session_key ?? void 0,
1267
+ conversation_id: row.conversation_id ?? void 0,
1268
+ target_human_session: row.target_human_session ?? void 0,
1269
+ summary: row.summary,
1270
+ approval_required: row.approval_required === 1,
1271
+ approval_status: row.approval_status,
1272
+ detail: row.detail_json ? JSON.parse(row.detail_json) : void 0
1273
+ };
1274
+ }
1275
+ function buildSummary(events) {
1276
+ const byType = {};
1277
+ const bySeverity = {};
1278
+ let latestTsMs = 0;
1279
+ let pendingApprovals = 0;
1280
+ for (const event of events) {
1281
+ byType[event.event_type] = (byType[event.event_type] ?? 0) + 1;
1282
+ bySeverity[event.severity] = (bySeverity[event.severity] ?? 0) + 1;
1283
+ latestTsMs = Math.max(latestTsMs, event.ts_ms);
1284
+ if (event.approval_required && event.approval_status === "pending") pendingApprovals += 1;
1285
+ }
1286
+ return {
1287
+ total_events: events.length,
1288
+ latest_ts_ms: latestTsMs || void 0,
1289
+ by_type: byType,
1290
+ by_severity: bySeverity,
1291
+ pending_approvals: pendingApprovals
1292
+ };
1293
+ }
1294
+ var CollaborationEventManager = class {
1295
+ constructor(store) {
1296
+ this.store = store;
1297
+ }
1298
+ record(input) {
1299
+ const tsMs = input.ts_ms ?? Date.now();
1300
+ const approvalRequired = input.approval_required === true;
1301
+ const approvalStatus = input.approval_status ?? (approvalRequired ? "pending" : "not_required");
1302
+ const result = this.store.getDb().prepare(`
1303
+ INSERT INTO collaboration_events (
1304
+ ts_ms, event_type, severity, session_key, conversation_id, target_human_session,
1305
+ summary, approval_required, approval_status, detail_json
1306
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1307
+ `).run(
1308
+ tsMs,
1309
+ input.event_type,
1310
+ input.severity ?? "info",
1311
+ input.session_key ?? null,
1312
+ input.conversation_id ?? null,
1313
+ input.target_human_session ?? null,
1314
+ normalizeSummary2(input.summary),
1315
+ approvalRequired ? 1 : 0,
1316
+ approvalStatus,
1317
+ input.detail ? JSON.stringify(input.detail) : null
1318
+ );
1319
+ return {
1320
+ id: Number(result.lastInsertRowid),
1321
+ ts_ms: tsMs,
1322
+ event_type: input.event_type,
1323
+ severity: input.severity ?? "info",
1324
+ session_key: input.session_key,
1325
+ conversation_id: input.conversation_id,
1326
+ target_human_session: input.target_human_session,
1327
+ summary: normalizeSummary2(input.summary),
1328
+ approval_required: approvalRequired,
1329
+ approval_status: approvalStatus,
1330
+ detail: input.detail
1331
+ };
1332
+ }
1333
+ listRecent(limit = 50) {
1334
+ const rows = this.store.getDb().prepare("SELECT * FROM collaboration_events ORDER BY ts_ms DESC, id DESC LIMIT ?").all(limit);
1335
+ return rows.map(rowToEvent);
1336
+ }
1337
+ listBySession(sessionKey, limit = 50) {
1338
+ const rows = this.store.getDb().prepare("SELECT * FROM collaboration_events WHERE session_key = ? ORDER BY ts_ms DESC, id DESC LIMIT ?").all(sessionKey, limit);
1339
+ return rows.map(rowToEvent);
1340
+ }
1341
+ listByConversation(conversationId, limit = 50) {
1342
+ const rows = this.store.getDb().prepare("SELECT * FROM collaboration_events WHERE conversation_id = ? ORDER BY ts_ms DESC, id DESC LIMIT ?").all(conversationId, limit);
1343
+ return rows.map(rowToEvent);
1344
+ }
1345
+ listByTargetHumanSession(targetHumanSession, limit = 50) {
1346
+ const rows = this.store.getDb().prepare("SELECT * FROM collaboration_events WHERE target_human_session = ? ORDER BY ts_ms DESC, id DESC LIMIT ?").all(targetHumanSession, limit);
1347
+ return rows.map(rowToEvent);
1348
+ }
1349
+ get(id) {
1350
+ const row = this.store.getDb().prepare("SELECT * FROM collaboration_events WHERE id = ?").get(id);
1351
+ return row ? rowToEvent(row) : null;
1352
+ }
1353
+ listPending(limit = 50) {
1354
+ const rows = this.store.getDb().prepare(`
1355
+ SELECT * FROM collaboration_events
1356
+ WHERE approval_required = 1 AND approval_status = 'pending'
1357
+ ORDER BY ts_ms DESC, id DESC
1358
+ LIMIT ?
1359
+ `).all(limit);
1360
+ return rows.map(rowToEvent);
1361
+ }
1362
+ listPendingBySession(sessionKey, limit = 50) {
1363
+ const rows = this.store.getDb().prepare(`
1364
+ SELECT * FROM collaboration_events
1365
+ WHERE session_key = ? AND approval_required = 1 AND approval_status = 'pending'
1366
+ ORDER BY ts_ms DESC, id DESC
1367
+ LIMIT ?
1368
+ `).all(sessionKey, limit);
1369
+ return rows.map(rowToEvent);
1370
+ }
1371
+ listPendingOlderThan(cutoffTs, limit = 50) {
1372
+ const rows = this.store.getDb().prepare(`
1373
+ SELECT * FROM collaboration_events
1374
+ WHERE approval_required = 1
1375
+ AND approval_status = 'pending'
1376
+ AND ts_ms <= ?
1377
+ ORDER BY ts_ms ASC, id ASC
1378
+ LIMIT ?
1379
+ `).all(cutoffTs, limit);
1380
+ return rows.map(rowToEvent);
1381
+ }
1382
+ resolveApproval(id, approvalStatus, input = {}) {
1383
+ const existing = this.get(id);
1384
+ if (!existing) return null;
1385
+ if (!existing.approval_required) {
1386
+ throw new Error(`Collaboration event ${id} does not require approval.`);
1387
+ }
1388
+ if (existing.approval_status === approvalStatus) {
1389
+ const reusedResolution = this.record({
1390
+ ts_ms: input.ts_ms ?? Date.now(),
1391
+ event_type: "agent_progress",
1392
+ severity: approvalStatus === "approved" ? "notice" : "warning",
1393
+ session_key: existing.session_key,
1394
+ conversation_id: existing.conversation_id,
1395
+ target_human_session: existing.target_human_session,
1396
+ summary: approvalStatus === "approved" ? `Decision already approved: ${existing.summary}` : `Decision already rejected: ${existing.summary}`,
1397
+ detail: {
1398
+ decision_event_id: existing.id,
1399
+ approval_status: approvalStatus,
1400
+ resolved_by: input.resolved_by ?? null,
1401
+ note: input.note ?? null,
1402
+ detail_ref: existing.detail && typeof existing.detail === "object" && "detail_ref" in existing.detail ? existing.detail.detail_ref : {
1403
+ session_key: existing.session_key ?? null,
1404
+ conversation_id: existing.conversation_id ?? null
1405
+ }
1406
+ }
1407
+ });
1408
+ const projectionOutbox2 = new CollaborationProjectionOutboxManager(this.store).enqueue({
1409
+ created_at: input.ts_ms ?? Date.now(),
1410
+ session_key: existing.session_key,
1411
+ conversation_id: existing.conversation_id,
1412
+ target_human_session: existing.target_human_session,
1413
+ projection_kind: "approval_resolution",
1414
+ summary: approvalStatus === "approved" ? `Approved: ${existing.summary}` : `Rejected: ${existing.summary}`,
1415
+ body: {
1416
+ message: approvalStatus === "approved" ? [
1417
+ "[CollaborationDecision]",
1418
+ `summary=Approved: ${existing.summary}`,
1419
+ "next=The system will continue with the approved collaboration path.",
1420
+ `detail_ref_session=${existing.session_key ?? "(none)"}`,
1421
+ `detail_ref_conversation=${existing.conversation_id ?? "(none)"}`,
1422
+ "detail_view=Open the collaboration detail in Host Panel, TUI, or MCP to inspect raw messages, handoffs, audit, and runtime changes."
1423
+ ].join("\n") : [
1424
+ "[CollaborationDecision]",
1425
+ `summary=Rejected: ${existing.summary}`,
1426
+ "next=The system paused the risky path and is waiting for a new instruction.",
1427
+ `detail_ref_session=${existing.session_key ?? "(none)"}`,
1428
+ `detail_ref_conversation=${existing.conversation_id ?? "(none)"}`,
1429
+ "detail_view=Open the collaboration detail in Host Panel, TUI, or MCP to inspect raw messages, handoffs, audit, and runtime changes."
1430
+ ].join("\n"),
1431
+ approval_status: approvalStatus,
1432
+ detail_ref: existing.detail && typeof existing.detail === "object" && "detail_ref" in existing.detail ? existing.detail.detail_ref : {
1433
+ session_key: existing.session_key ?? null,
1434
+ conversation_id: existing.conversation_id ?? null
1435
+ }
1436
+ },
1437
+ source_event_id: reusedResolution.id
1438
+ });
1439
+ return { updated: existing, resolution_event: reusedResolution, projection_outbox: projectionOutbox2 };
1440
+ }
1441
+ const resolvedAt = input.ts_ms ?? Date.now();
1442
+ const nextDetail = {
1443
+ ...existing.detail ?? {},
1444
+ approval_resolution: {
1445
+ approval_status: approvalStatus,
1446
+ resolved_at: resolvedAt,
1447
+ resolved_by: input.resolved_by ?? null,
1448
+ note: input.note ?? null
1449
+ }
1450
+ };
1451
+ this.store.getDb().prepare("UPDATE collaboration_events SET approval_status = ?, detail_json = ? WHERE id = ?").run(approvalStatus, JSON.stringify(nextDetail), id);
1452
+ const updated = this.get(id);
1453
+ if (!updated) return null;
1454
+ const resolutionEvent = this.record({
1455
+ ts_ms: resolvedAt,
1456
+ event_type: "agent_progress",
1457
+ severity: approvalStatus === "approved" ? "notice" : "warning",
1458
+ session_key: updated.session_key,
1459
+ conversation_id: updated.conversation_id,
1460
+ target_human_session: updated.target_human_session,
1461
+ summary: approvalStatus === "approved" ? `Decision approved: ${updated.summary}` : `Decision rejected: ${updated.summary}`,
1462
+ detail: {
1463
+ decision_event_id: updated.id,
1464
+ approval_status: approvalStatus,
1465
+ resolved_by: input.resolved_by ?? null,
1466
+ note: input.note ?? null,
1467
+ detail_ref: nextDetail && typeof nextDetail === "object" && "detail_ref" in nextDetail ? nextDetail.detail_ref : {
1468
+ session_key: updated.session_key ?? null,
1469
+ conversation_id: updated.conversation_id ?? null
1470
+ }
1471
+ }
1472
+ });
1473
+ const projectionOutbox = new CollaborationProjectionOutboxManager(this.store).enqueue({
1474
+ created_at: resolvedAt,
1475
+ session_key: updated.session_key,
1476
+ conversation_id: updated.conversation_id,
1477
+ target_human_session: updated.target_human_session,
1478
+ projection_kind: "approval_resolution",
1479
+ summary: approvalStatus === "approved" ? `Approved: ${updated.summary}` : `Rejected: ${updated.summary}`,
1480
+ body: {
1481
+ message: approvalStatus === "approved" ? [
1482
+ "[CollaborationDecision]",
1483
+ `summary=Approved: ${updated.summary}`,
1484
+ "next=The system will continue with the approved collaboration path.",
1485
+ `detail_ref_session=${updated.session_key ?? "(none)"}`,
1486
+ `detail_ref_conversation=${updated.conversation_id ?? "(none)"}`,
1487
+ input.note ? `note=${input.note}` : null,
1488
+ "detail_view=Open the collaboration detail in Host Panel, TUI, or MCP to inspect raw messages, handoffs, audit, and runtime changes."
1489
+ ].filter(Boolean).join("\n") : [
1490
+ "[CollaborationDecision]",
1491
+ `summary=Rejected: ${updated.summary}`,
1492
+ "next=The system paused the risky path and is waiting for a new instruction.",
1493
+ `detail_ref_session=${updated.session_key ?? "(none)"}`,
1494
+ `detail_ref_conversation=${updated.conversation_id ?? "(none)"}`,
1495
+ input.note ? `note=${input.note}` : null,
1496
+ "detail_view=Open the collaboration detail in Host Panel, TUI, or MCP to inspect raw messages, handoffs, audit, and runtime changes."
1497
+ ].filter(Boolean).join("\n"),
1498
+ approval_status: approvalStatus,
1499
+ detail_ref: nextDetail && typeof nextDetail === "object" && "detail_ref" in nextDetail ? nextDetail.detail_ref : {
1500
+ session_key: updated.session_key ?? null,
1501
+ conversation_id: updated.conversation_id ?? null
1502
+ }
1503
+ },
1504
+ source_event_id: resolutionEvent.id
1505
+ });
1506
+ return { updated, resolution_event: resolutionEvent, projection_outbox: projectionOutbox };
1507
+ }
1508
+ summarize(limit = 200) {
1509
+ return buildSummary(this.listRecent(limit));
1510
+ }
1511
+ };
1512
+
1513
+ // src/capability-card.ts
1514
+ function normalizeStringList(value) {
1515
+ if (!Array.isArray(value)) return void 0;
1516
+ const list = value.map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, 32);
1517
+ return list.length ? list : void 0;
1518
+ }
1519
+ function normalizeCapabilityCardItem(value) {
1520
+ if (!value || typeof value !== "object") return null;
1521
+ const source = value;
1522
+ const id = String(source.id ?? "").trim();
1523
+ if (!id) return null;
1524
+ const label = String(source.label ?? "").trim() || void 0;
1525
+ const description = String(source.description ?? "").trim() || void 0;
1526
+ const acceptsTasks = typeof source.accepts_tasks === "boolean" ? source.accepts_tasks : void 0;
1527
+ const acceptsMessages = typeof source.accepts_messages === "boolean" ? source.accepts_messages : void 0;
1528
+ return {
1529
+ id,
1530
+ label,
1531
+ description,
1532
+ accepts_tasks: acceptsTasks,
1533
+ accepts_messages: acceptsMessages,
1534
+ input_modes: normalizeStringList(source.input_modes),
1535
+ output_modes: normalizeStringList(source.output_modes),
1536
+ examples: normalizeStringList(source.examples)
1537
+ };
1538
+ }
1539
+ function normalizeCapabilityCard(value) {
1540
+ if (!value || typeof value !== "object") return void 0;
1541
+ const source = value;
1542
+ const preferred = String(source.preferred_contact_mode ?? "").trim();
1543
+ const capabilities = Array.isArray(source.capabilities) ? source.capabilities.map(normalizeCapabilityCardItem).filter(Boolean) : [];
1544
+ return {
1545
+ version: String(source.version ?? "1").trim() || "1",
1546
+ summary: String(source.summary ?? "").trim() || void 0,
1547
+ accepts_new_work: typeof source.accepts_new_work === "boolean" ? source.accepts_new_work : void 0,
1548
+ preferred_contact_mode: preferred === "dm" || preferred === "task" || preferred === "either" ? preferred : void 0,
1549
+ capabilities
1550
+ };
1551
+ }
1552
+ function capabilityCardToLegacyCapabilities(card) {
1553
+ if (!card?.capabilities?.length) return [];
1554
+ return card.capabilities.map((item) => item.label || item.id).map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, 24);
1555
+ }
1556
+ function formatCapabilityCardSummary(card) {
1557
+ if (!card) return "(none)";
1558
+ const parts = [];
1559
+ if (card.summary) parts.push(card.summary);
1560
+ if (card.preferred_contact_mode) parts.push(`preferred_contact_mode=${card.preferred_contact_mode}`);
1561
+ if (typeof card.accepts_new_work === "boolean") parts.push(`accepts_new_work=${card.accepts_new_work}`);
1562
+ if (card.capabilities?.length) {
1563
+ parts.push(`capabilities=${card.capabilities.map((item) => item.label || item.id).join(", ")}`);
1564
+ }
1565
+ return parts.join(" | ") || "(none)";
1566
+ }
1567
+
1568
+ // src/e2ee.ts
1569
+ import { webcrypto } from "crypto";
1570
+ import { SCHEMA_CONTACT_REQUEST, SCHEMA_RESULT, SCHEMA_TASK, SCHEMA_TEXT } from "@pingagent/schemas";
1571
+ var subtle = webcrypto.subtle;
1572
+ var encoder = new TextEncoder();
1573
+ var decoder = new TextDecoder();
1574
+ function bytesToBase64Url(bytes) {
1575
+ return Buffer.from(bytes).toString("base64url");
1576
+ }
1577
+ function base64UrlToBytes(value) {
1578
+ return new Uint8Array(Buffer.from(value, "base64url"));
1579
+ }
1580
+ function isObject(value) {
1581
+ return value != null && typeof value === "object" && !Array.isArray(value);
1582
+ }
1583
+ async function importEncryptionPublicKey(jwk) {
1584
+ return subtle.importKey(
1585
+ "jwk",
1586
+ { ...jwk, ext: true },
1587
+ { name: "ECDH", namedCurve: "P-256" },
1588
+ false,
1589
+ []
1590
+ );
1591
+ }
1592
+ async function importEncryptionPrivateKey(jwk) {
1593
+ return subtle.importKey(
1594
+ "jwk",
1595
+ { ...jwk, ext: true, key_ops: ["deriveKey"] },
1596
+ { name: "ECDH", namedCurve: "P-256" },
1597
+ false,
1598
+ ["deriveKey"]
1599
+ );
1600
+ }
1601
+ function splitPayloadForEncryption(schema, payload) {
1602
+ if (schema === SCHEMA_TASK) {
1603
+ const {
1604
+ task_id,
1605
+ idempotency_key,
1606
+ expected_output_schema,
1607
+ timeout_ms,
1608
+ ...protectedPayload
1609
+ } = payload;
1610
+ return {
1611
+ cleartext: {
1612
+ ...task_id ? { task_id } : {},
1613
+ ...idempotency_key ? { idempotency_key } : {},
1614
+ ...expected_output_schema ? { expected_output_schema } : {},
1615
+ ...timeout_ms ? { timeout_ms } : {}
1616
+ },
1617
+ protectedPayload
1618
+ };
1619
+ }
1620
+ if (schema === SCHEMA_RESULT) {
1621
+ const { task_id, status, elapsed_ms, ...protectedPayload } = payload;
1622
+ return {
1623
+ cleartext: {
1624
+ ...task_id ? { task_id } : {},
1625
+ ...status ? { status } : {},
1626
+ ...elapsed_ms != null ? { elapsed_ms } : {}
1627
+ },
1628
+ protectedPayload
1629
+ };
1630
+ }
1631
+ if (schema === SCHEMA_TEXT || schema === SCHEMA_CONTACT_REQUEST) {
1632
+ return { cleartext: {}, protectedPayload: payload };
1633
+ }
1634
+ return { cleartext: payload, protectedPayload: {} };
1635
+ }
1636
+ function mergePayloadFromWrapper(wrapper, decryptedProtectedPayload) {
1637
+ return {
1638
+ ...wrapper.__e2ee.cleartext,
1639
+ ...decryptedProtectedPayload
1640
+ };
1641
+ }
1642
+ function isEncryptedPayload(payload) {
1643
+ return isObject(payload) && isObject(payload.__e2ee) && Array.isArray(payload.__e2ee.recipients) && isObject(payload.__e2ee.cleartext);
1644
+ }
1645
+ function shouldEncryptConversationPayload(conversationId, schema) {
1646
+ if (!(conversationId.startsWith("c_dm_") || conversationId.startsWith("c_pdm_"))) return false;
1647
+ return schema === SCHEMA_TEXT || schema === SCHEMA_CONTACT_REQUEST || schema === SCHEMA_TASK || schema === SCHEMA_RESULT;
1648
+ }
1649
+ async function encryptPayloadForRecipients(opts) {
1650
+ const { cleartext, protectedPayload } = splitPayloadForEncryption(opts.schema, opts.payload);
1651
+ const recipientBlobs = [];
1652
+ const plaintextBytes = encoder.encode(JSON.stringify(protectedPayload));
1653
+ const uniqueRecipients = /* @__PURE__ */ new Map();
1654
+ for (const recipient of opts.recipients) {
1655
+ uniqueRecipients.set(recipient.did, recipient);
1656
+ }
1657
+ for (const recipient of uniqueRecipients.values()) {
1658
+ if (!recipient.encryption_public_key) {
1659
+ throw new Error(`Recipient ${recipient.did} has no registered encryption public key`);
1660
+ }
1661
+ const ephemeral = await subtle.generateKey(
1662
+ { name: "ECDH", namedCurve: "P-256" },
1663
+ true,
1664
+ ["deriveKey"]
1665
+ );
1666
+ const recipientPublicKey = await importEncryptionPublicKey(recipient.encryption_public_key);
1667
+ const contentKey = await subtle.deriveKey(
1668
+ { name: "ECDH", public: recipientPublicKey },
1669
+ ephemeral.privateKey,
1670
+ { name: "AES-GCM", length: 256 },
1671
+ false,
1672
+ ["encrypt"]
1673
+ );
1674
+ const iv = webcrypto.getRandomValues(new Uint8Array(12));
1675
+ const ciphertext = await subtle.encrypt(
1676
+ { name: "AES-GCM", iv },
1677
+ contentKey,
1678
+ plaintextBytes
1679
+ );
1680
+ const ephemeralPublicKey = await subtle.exportKey("jwk", ephemeral.publicKey);
1681
+ recipientBlobs.push({
1682
+ did: recipient.did,
1683
+ alg: "ECDH-P256+A256GCM",
1684
+ ephemeral_public_key: {
1685
+ kty: "EC",
1686
+ crv: "P-256",
1687
+ x: ephemeralPublicKey.x,
1688
+ y: ephemeralPublicKey.y
1689
+ },
1690
+ iv: bytesToBase64Url(iv),
1691
+ ciphertext: bytesToBase64Url(new Uint8Array(ciphertext))
1692
+ });
1693
+ }
1694
+ return {
1695
+ __e2ee: {
1696
+ v: 1,
1697
+ alg: "ECDH-P256+A256GCM",
1698
+ recipients: recipientBlobs,
1699
+ cleartext
1700
+ }
1701
+ };
1702
+ }
1703
+ async function decryptPayloadForIdentity(schema, payload, identity) {
1704
+ if (!isEncryptedPayload(payload)) return payload;
1705
+ if (!identity.encryptionPrivateKeyJwk) return payload;
1706
+ const recipientBlob = payload.__e2ee.recipients.find((recipient) => recipient.did === identity.did);
1707
+ if (!recipientBlob) return payload;
1708
+ const privateKey = await importEncryptionPrivateKey(identity.encryptionPrivateKeyJwk);
1709
+ const ephemeralPublicKey = await importEncryptionPublicKey(recipientBlob.ephemeral_public_key);
1710
+ const contentKey = await subtle.deriveKey(
1711
+ { name: "ECDH", public: ephemeralPublicKey },
1712
+ privateKey,
1713
+ { name: "AES-GCM", length: 256 },
1714
+ false,
1715
+ ["decrypt"]
1716
+ );
1717
+ const plaintext = await subtle.decrypt(
1718
+ { name: "AES-GCM", iv: base64UrlToBytes(recipientBlob.iv) },
1719
+ contentKey,
1720
+ base64UrlToBytes(recipientBlob.ciphertext)
1721
+ );
1722
+ const decryptedProtectedPayload = JSON.parse(decoder.decode(plaintext));
1723
+ return mergePayloadFromWrapper(payload, decryptedProtectedPayload);
1724
+ }
1725
+
1726
+ // src/client.ts
1727
+ function normalizeProfileList(value) {
1728
+ if (value === void 0) return void 0;
1729
+ const items = Array.isArray(value) ? value : value.split(",");
1730
+ const normalized = items.map((item) => String(item ?? "").trim()).filter(Boolean).slice(0, 32);
1731
+ return normalized;
1732
+ }
1733
+ var PingAgentClient = class {
1734
+ constructor(opts) {
1735
+ this.opts = opts;
1736
+ this.identity = Object.assign(opts.identity, ensureIdentityEncryptionKeys(opts.identity));
1737
+ this.transport = new HttpTransport({
1738
+ serverUrl: opts.serverUrl,
1739
+ accessToken: opts.accessToken,
1740
+ onTokenRefreshed: opts.onTokenRefreshed
1741
+ });
1742
+ if (opts.store) {
1743
+ this.contactManager = new ContactManager(opts.store);
1744
+ this.historyManager = new HistoryManager(opts.store);
1745
+ this.sessionManager = new SessionManager(opts.store);
1746
+ this.sessionSummaryManager = new SessionSummaryManager(opts.store);
1747
+ this.taskThreadManager = new TaskThreadManager(opts.store);
1748
+ this.taskHandoffManager = new TaskHandoffManager(opts.store);
1749
+ this.collaborationEventManager = new CollaborationEventManager(opts.store);
1750
+ }
1751
+ }
1752
+ transport;
1753
+ identity;
1754
+ contactManager;
1755
+ historyManager;
1756
+ sessionManager;
1757
+ sessionSummaryManager;
1758
+ taskThreadManager;
1759
+ taskHandoffManager;
1760
+ collaborationEventManager;
1761
+ agentCardCache = /* @__PURE__ */ new Map();
1762
+ getContactManager() {
1763
+ return this.contactManager;
1764
+ }
1765
+ getHistoryManager() {
1766
+ return this.historyManager;
1767
+ }
1768
+ getSessionManager() {
1769
+ return this.sessionManager;
1770
+ }
1771
+ getSessionSummaryManager() {
1772
+ return this.sessionSummaryManager;
1773
+ }
1774
+ getTaskThreadManager() {
1775
+ return this.taskThreadManager;
1776
+ }
1777
+ getTaskHandoffManager() {
1778
+ return this.taskHandoffManager;
1779
+ }
1780
+ getCollaborationEventManager() {
1781
+ return this.collaborationEventManager;
1782
+ }
1783
+ /** Update the in-memory access token (e.g. after proactive refresh from disk). */
1784
+ setAccessToken(token) {
1785
+ this.transport.setToken(token);
1786
+ }
1787
+ async register(developerToken) {
1788
+ let binary = "";
1789
+ for (const b of this.identity.publicKey) binary += String.fromCharCode(b);
1790
+ const publicKeyBase64 = btoa(binary);
1791
+ return this.transport.request("POST", "/v1/agent/register", {
1792
+ device_id: this.identity.deviceId,
1793
+ public_key: publicKeyBase64,
1794
+ encryption_public_key: this.identity.encryptionPublicKeyJwk,
1795
+ developer_token: developerToken
1796
+ }, true);
1797
+ }
1798
+ async openConversation(targetDid) {
1799
+ const res = await this.transport.request("POST", "/v1/conversations/open", {
1800
+ targets: [targetDid]
1801
+ });
1802
+ if (res.ok && res.data?.recipient_encryption_public_key) {
1803
+ this.agentCardCache.set(targetDid, {
1804
+ did: targetDid,
1805
+ encryption_public_key: res.data.recipient_encryption_public_key
1806
+ });
1807
+ }
1808
+ return res;
1809
+ }
1810
+ async resolveConversationPeer(conversationId) {
1811
+ const fromSession = this.sessionManager?.getByConversationId(conversationId)?.remote_did;
1812
+ if (fromSession) return fromSession;
1813
+ const convList = await this.listConversations({ type: "dm" });
1814
+ if (!convList.ok || !convList.data) return null;
1815
+ return convList.data.conversations.find((conv) => conv.conversation_id === conversationId)?.target_did ?? null;
1816
+ }
1817
+ async getRecipientEncryptionCard(did) {
1818
+ if (did === this.identity.did) {
1819
+ return {
1820
+ did,
1821
+ encryption_public_key: this.identity.encryptionPublicKeyJwk
1822
+ };
1823
+ }
1824
+ const cached = this.agentCardCache.get(did);
1825
+ if (cached?.encryption_public_key) return cached;
1826
+ const cardRes = await this.getAgentCard(did);
1827
+ if (!cardRes.ok || !cardRes.data?.encryption_public_key) {
1828
+ throw new Error(`Recipient ${did} has no registered encryption key`);
1829
+ }
1830
+ const card = {
1831
+ did,
1832
+ encryption_public_key: cardRes.data.encryption_public_key
1833
+ };
1834
+ this.agentCardCache.set(did, card);
1835
+ return card;
1836
+ }
1837
+ sameEncryptionPublicKey(left, right) {
1838
+ if (!left || !right) return false;
1839
+ return left.kty === right.kty && left.crv === right.crv && left.x === right.x && left.y === right.y;
1840
+ }
1841
+ async sendMessage(conversationId, schema, payload) {
1842
+ const ttlMs = schema === SCHEMA_TEXT2 ? 6048e5 : void 0;
1843
+ let payloadForTransport = payload;
1844
+ if (shouldEncryptConversationPayload(conversationId, schema)) {
1845
+ try {
1846
+ const remoteDid = await this.resolveConversationPeer(conversationId);
1847
+ if (!remoteDid) {
1848
+ return {
1849
+ ok: false,
1850
+ error: { code: ErrorCode2.INTERNAL, message: `Cannot determine peer DID for encrypted conversation ${conversationId}`, hint: "List conversations again or reopen the conversation before sending." }
1851
+ };
1852
+ }
1853
+ const recipient = await this.getRecipientEncryptionCard(remoteDid);
1854
+ payloadForTransport = await encryptPayloadForRecipients({
1855
+ schema,
1856
+ payload,
1857
+ recipients: [
1858
+ recipient,
1859
+ {
1860
+ did: this.identity.did,
1861
+ encryption_public_key: this.identity.encryptionPublicKeyJwk
1862
+ }
1863
+ ]
1864
+ });
1865
+ } catch (error) {
1866
+ return {
1867
+ ok: false,
1868
+ error: {
1869
+ code: ErrorCode2.INTERNAL,
1870
+ message: error?.message ?? "Failed to encrypt message payload",
1871
+ hint: "Both sides must register encryption keys before direct messages can be sent end-to-end encrypted."
1872
+ }
1873
+ };
1874
+ }
1875
+ }
1876
+ const unsigned = buildUnsignedEnvelope({
1877
+ type: "message",
1878
+ conversationId,
1879
+ senderDid: this.identity.did,
1880
+ senderDeviceId: this.identity.deviceId,
1881
+ schema,
1882
+ payload: payloadForTransport,
1883
+ ttlMs
1884
+ });
1885
+ const signed = signEnvelope(unsigned, this.identity.privateKey);
1886
+ const res = await this.transport.request("POST", "/v1/messages/send", signed);
1887
+ if (res.ok && this.historyManager) {
1888
+ this.historyManager.save([{
1889
+ conversation_id: conversationId,
1890
+ message_id: signed.message_id,
1891
+ seq: res.data?.seq,
1892
+ sender_did: this.identity.did,
1893
+ schema,
1894
+ payload,
1895
+ ts_ms: signed.ts_ms,
1896
+ direction: "sent"
1897
+ }]);
1898
+ this.sessionManager?.upsertFromMessage({
1899
+ session_key: this.sessionManager.getByConversationId(conversationId)?.session_key,
1900
+ remote_did: this.sessionManager.getByConversationId(conversationId)?.remote_did,
1901
+ conversation_id: conversationId,
1902
+ trust_state: this.sessionManager.getByConversationId(conversationId)?.trust_state ?? "trusted",
1903
+ sender_did: this.identity.did,
1904
+ sender_is_self: true,
1905
+ schema,
1906
+ payload,
1907
+ seq: res.data?.seq,
1908
+ ts_ms: signed.ts_ms
1909
+ });
1910
+ }
1911
+ return res;
1912
+ }
1913
+ async sendTask(conversationId, task) {
1914
+ return this.sendMessage(conversationId, SCHEMA_TASK2, {
1915
+ ...task,
1916
+ idempotency_key: task.task_id,
1917
+ expected_output_schema: SCHEMA_RESULT2
1918
+ });
1919
+ }
1920
+ async sendContactRequest(conversationId, message) {
1921
+ const { v7: uuidv7 } = await import("uuid");
1922
+ return this.sendMessage(conversationId, SCHEMA_CONTACT_REQUEST2, {
1923
+ request_id: `r_${uuidv7()}`,
1924
+ from_did: this.identity.did,
1925
+ to_did: "",
1926
+ capabilities: ["task", "result", "files"],
1927
+ message,
1928
+ expires_ms: 864e5
1929
+ });
1930
+ }
1931
+ async fetchInbox(conversationId, opts) {
1932
+ const params = new URLSearchParams({
1933
+ conversation_id: conversationId,
1934
+ since_seq: String(opts?.sinceSeq ?? 0),
1935
+ limit: String(opts?.limit ?? 50),
1936
+ box: opts?.box ?? "ready"
1937
+ });
1938
+ const res = await this.transport.request("GET", `/v1/inbox/fetch?${params}`);
1939
+ if (res.ok && res.data && this.historyManager) {
1940
+ const decryptedMessages = await Promise.all(res.data.messages.map(async (msg) => ({
1941
+ ...msg,
1942
+ payload: await decryptPayloadForIdentity(msg.schema, msg.payload ?? {}, this.identity).catch(() => msg.payload ?? {})
1943
+ })));
1944
+ res.data.messages = decryptedMessages;
1945
+ const msgs = decryptedMessages.map((msg) => ({
1946
+ conversation_id: conversationId,
1947
+ message_id: msg.message_id,
1948
+ seq: msg.seq,
1949
+ sender_did: msg.sender_did,
1950
+ schema: msg.schema,
1951
+ payload: msg.payload,
1952
+ ts_ms: msg.ts_ms,
1953
+ direction: msg.sender_did === this.identity.did ? "sent" : "received"
1954
+ }));
1955
+ if (msgs.length > 0) {
1956
+ this.historyManager.save(msgs);
1957
+ const maxSeq = Math.max(...msgs.map((m) => m.seq ?? 0));
1958
+ if (maxSeq > 0) this.historyManager.setLastSyncedSeq(conversationId, maxSeq);
1959
+ }
1960
+ for (const msg of decryptedMessages) {
1961
+ const existingSession = this.sessionManager?.getByConversationId(conversationId);
1962
+ this.sessionManager?.upsertFromMessage({
1963
+ session_key: existingSession?.session_key,
1964
+ remote_did: existingSession?.remote_did ?? (msg.sender_did !== this.identity.did ? msg.sender_did : void 0),
1965
+ conversation_id: conversationId,
1966
+ trust_state: existingSession?.trust_state ?? (opts?.box === "strangers" ? "pending" : "trusted"),
1967
+ sender_did: msg.sender_did,
1968
+ sender_is_self: msg.sender_did === this.identity.did,
1969
+ schema: msg.schema,
1970
+ payload: msg.payload,
1971
+ seq: msg.seq,
1972
+ ts_ms: msg.ts_ms
1973
+ });
1974
+ if (msg.schema === SCHEMA_TASK2 && msg.payload?.task_id) {
1975
+ const session = this.sessionManager?.getByConversationId(conversationId);
1976
+ const sessionKey = session?.session_key ?? buildSessionKey({ localDid: this.identity.did, conversationId, remoteDid: session?.remote_did });
1977
+ this.taskThreadManager?.upsert({
1978
+ task_id: msg.payload.task_id,
1979
+ session_key: sessionKey,
1980
+ conversation_id: conversationId,
1981
+ title: msg.payload?.title,
1982
+ status: "queued",
1983
+ started_at: msg.ts_ms ?? Date.now(),
1984
+ updated_at: msg.ts_ms ?? Date.now()
1985
+ });
1986
+ if (msg.payload?.handoff && typeof msg.payload.handoff === "object") {
1987
+ this.taskHandoffManager?.upsert({
1988
+ task_id: msg.payload.task_id,
1989
+ session_key: sessionKey,
1990
+ conversation_id: conversationId,
1991
+ ...msg.payload.handoff,
1992
+ updated_at: msg.ts_ms ?? Date.now()
1993
+ });
1994
+ }
1995
+ }
1996
+ if (msg.schema === SCHEMA_RESULT2 && msg.payload?.task_id) {
1997
+ const session = this.sessionManager?.getByConversationId(conversationId);
1998
+ this.taskThreadManager?.upsert({
1999
+ task_id: msg.payload.task_id,
2000
+ session_key: session?.session_key ?? buildSessionKey({ localDid: this.identity.did, conversationId, remoteDid: session?.remote_did }),
2001
+ conversation_id: conversationId,
2002
+ status: msg.payload.status === "ok" ? "processed" : "failed",
2003
+ updated_at: msg.ts_ms ?? Date.now(),
2004
+ result_summary: msg.payload?.output?.summary,
2005
+ error_code: msg.payload?.error?.code,
2006
+ error_message: msg.payload?.error?.message
2007
+ });
2008
+ }
2009
+ }
2010
+ }
2011
+ return res;
2012
+ }
2013
+ async ack(conversationId, forMessageId, status, opts) {
2014
+ return this.transport.request("POST", "/v1/receipts/ack", {
2015
+ conversation_id: conversationId,
2016
+ for_message_id: forMessageId,
2017
+ for_task_id: opts?.forTaskId,
2018
+ status,
2019
+ reason: opts?.reason,
2020
+ detail: opts?.detail
2021
+ });
2022
+ }
2023
+ async approveContact(conversationId) {
2024
+ const res = await this.transport.request("POST", "/v1/control/approve_contact", {
2025
+ conversation_id: conversationId
2026
+ });
2027
+ if (res.ok && this.contactManager) {
2028
+ const pendingMessages = this.historyManager?.list(conversationId, { limit: 1 });
2029
+ const senderDid = pendingMessages?.[0]?.sender_did;
2030
+ if (senderDid && senderDid !== this.identity.did) {
2031
+ this.contactManager.add({
2032
+ did: senderDid,
2033
+ conversation_id: res.data?.dm_conversation_id ?? conversationId,
2034
+ trusted: true
2035
+ });
2036
+ }
2037
+ }
2038
+ if (res.ok) {
2039
+ const existing = this.sessionManager?.getByConversationId(conversationId);
2040
+ if (existing) {
2041
+ this.sessionManager?.upsert({
2042
+ session_key: existing.session_key,
2043
+ conversation_id: res.data?.dm_conversation_id ?? conversationId,
2044
+ trust_state: "trusted",
2045
+ remote_did: existing.remote_did,
2046
+ active: existing.active
2047
+ });
2048
+ }
2049
+ }
2050
+ return res;
2051
+ }
2052
+ async revokeConversation(conversationId) {
2053
+ return this.transport.request("POST", "/v1/control/revoke_conversation", {
2054
+ conversation_id: conversationId
2055
+ });
2056
+ }
2057
+ async cancelTask(conversationId, taskId) {
2058
+ const res = await this.transport.request("POST", "/v1/control/cancel_task", {
2059
+ conversation_id: conversationId,
2060
+ task_id: taskId
2061
+ });
2062
+ if (res.ok && res.data) {
2063
+ const session = this.sessionManager?.getByConversationId(conversationId);
2064
+ this.taskThreadManager?.upsert({
2065
+ task_id: taskId,
2066
+ session_key: session?.session_key ?? buildSessionKey({ localDid: this.identity.did, conversationId, remoteDid: session?.remote_did }),
2067
+ conversation_id: conversationId,
2068
+ status: normalizeTaskThreadStatus(res.data.task_state),
2069
+ updated_at: Date.now()
2070
+ });
2071
+ }
2072
+ return res;
2073
+ }
2074
+ async blockSender(conversationId, senderDid) {
2075
+ return this.transport.request("POST", "/v1/control/block_sender", {
2076
+ conversation_id: conversationId,
2077
+ sender_did: senderDid
2078
+ });
2079
+ }
2080
+ async acquireLease(conversationId) {
2081
+ return this.transport.request("POST", "/v1/lease/acquire", {
2082
+ conversation_id: conversationId
2083
+ });
2084
+ }
2085
+ async renewLease(conversationId) {
2086
+ return this.transport.request("POST", "/v1/lease/renew", {
2087
+ conversation_id: conversationId
2088
+ });
2089
+ }
2090
+ async releaseLease(conversationId) {
2091
+ return this.transport.request("POST", "/v1/lease/release", {
2092
+ conversation_id: conversationId
2093
+ });
2094
+ }
2095
+ async uploadArtifact(content) {
2096
+ const presignRes = await this.transport.request("POST", "/v1/artifacts/presign_upload", {
2097
+ size: content.length,
2098
+ content_type: "application/octet-stream"
2099
+ });
2100
+ if (!presignRes.ok || !presignRes.data) throw new Error("Failed to presign upload");
2101
+ const { upload_url, artifact_ref } = presignRes.data;
2102
+ await this.transport.fetchWithAuth(upload_url, {
2103
+ method: "PUT",
2104
+ body: content,
2105
+ contentType: "application/octet-stream"
2106
+ });
2107
+ const { createHash } = await import("crypto");
2108
+ const hash = createHash("sha256").update(content).digest("hex");
2109
+ return { artifact_ref, sha256: hash, size: content.length };
2110
+ }
2111
+ async downloadArtifact(artifactRef, expectedSha256, expectedSize) {
2112
+ const presignRes = await this.transport.request("POST", "/v1/artifacts/presign_download", {
2113
+ artifact_ref: artifactRef
2114
+ });
2115
+ if (!presignRes.ok || !presignRes.data) throw new Error("Failed to presign download");
2116
+ const buffer = Buffer.from(await this.transport.fetchWithAuth(presignRes.data.download_url));
2117
+ if (buffer.length !== expectedSize) {
2118
+ throw new Error(`Size mismatch: expected ${expectedSize}, got ${buffer.length}`);
2119
+ }
2120
+ const { createHash } = await import("crypto");
2121
+ const hash = createHash("sha256").update(buffer).digest("hex");
2122
+ if (hash !== expectedSha256) {
2123
+ throw new Error(`SHA256 mismatch: expected ${expectedSha256}, got ${hash}`);
2124
+ }
2125
+ return buffer;
2126
+ }
2127
+ async getSubscription() {
2128
+ return this.transport.request("GET", "/v1/subscription");
2129
+ }
2130
+ async resolveAlias(alias) {
2131
+ const res = await this.transport.request("GET", `/v1/directory/resolve?alias=${encodeURIComponent(alias)}`);
2132
+ if (res.ok && res.data?.did && res.data.encryption_public_key) {
2133
+ this.agentCardCache.set(res.data.did, {
2134
+ did: res.data.did,
2135
+ encryption_public_key: res.data.encryption_public_key
2136
+ });
2137
+ }
2138
+ return res;
2139
+ }
2140
+ async getAgentCard(did) {
2141
+ const res = await this.transport.request("GET", `/v1/directory/card?did=${encodeURIComponent(did)}`, void 0, true);
2142
+ if (res.ok && res.data?.encryption_public_key) {
2143
+ this.agentCardCache.set(did, {
2144
+ did,
2145
+ encryption_public_key: res.data.encryption_public_key
2146
+ });
2147
+ }
2148
+ return res;
2149
+ }
2150
+ async registerAlias(alias) {
2151
+ return this.transport.request("POST", "/v1/directory/alias", { alias });
2152
+ }
2153
+ async listConversations(opts) {
2154
+ const params = new URLSearchParams();
2155
+ if (opts?.type) params.set("type", opts.type);
2156
+ const qs = params.toString();
2157
+ const res = await this.transport.request("GET", `/v1/conversations/list${qs ? "?" + qs : ""}`);
2158
+ if (res.ok && res.data && this.sessionManager) {
2159
+ for (const conv of res.data.conversations) {
2160
+ if (conv.recipient_encryption_public_key && conv.target_did) {
2161
+ this.agentCardCache.set(conv.target_did, {
2162
+ did: conv.target_did,
2163
+ encryption_public_key: conv.recipient_encryption_public_key
2164
+ });
2165
+ }
2166
+ const trustState = conv.trusted ? "trusted" : conv.type === "pending_dm" ? "pending" : "stranger";
2167
+ this.sessionManager.upsert({
2168
+ session_key: buildSessionKey({
2169
+ localDid: this.identity.did,
2170
+ remoteDid: conv.target_did,
2171
+ conversationId: conv.conversation_id,
2172
+ conversationType: conv.type
2173
+ }),
2174
+ remote_did: conv.target_did,
2175
+ conversation_id: conv.conversation_id,
2176
+ trust_state: trustState,
2177
+ last_remote_activity_at: conv.last_activity_at ?? void 0,
2178
+ updated_at: conv.last_activity_at ?? conv.created_at
2179
+ });
2180
+ }
2181
+ }
2182
+ return res;
2183
+ }
2184
+ async getTaskStatus(conversationId, taskId) {
2185
+ const params = new URLSearchParams({ conversation_id: conversationId, task_id: taskId });
2186
+ const res = await this.transport.request("GET", `/v1/tasks/status?${params.toString()}`);
2187
+ if (res.ok && res.data) {
2188
+ const session = this.sessionManager?.getByConversationId(conversationId);
2189
+ this.taskThreadManager?.upsert({
2190
+ task_id: taskId,
2191
+ session_key: session?.session_key ?? buildSessionKey({ localDid: this.identity.did, conversationId, remoteDid: session?.remote_did }),
2192
+ conversation_id: conversationId,
2193
+ status: res.data.status,
2194
+ updated_at: res.data.last_update_ts_ms,
2195
+ result_summary: res.data.result_summary,
2196
+ error_code: res.data.error?.code,
2197
+ error_message: res.data.error?.message ?? res.data.reason
2198
+ });
2199
+ }
2200
+ return res;
2201
+ }
2202
+ async listTaskThreads(conversationId) {
2203
+ const params = new URLSearchParams({ conversation_id: conversationId });
2204
+ const res = await this.transport.request("GET", `/v1/tasks/list?${params.toString()}`);
2205
+ if (res.ok && res.data) {
2206
+ const session = this.sessionManager?.getByConversationId(conversationId);
2207
+ for (const task of res.data.tasks) {
2208
+ this.taskThreadManager?.upsert({
2209
+ task_id: task.task_id,
2210
+ session_key: session?.session_key ?? buildSessionKey({ localDid: this.identity.did, conversationId, remoteDid: session?.remote_did }),
2211
+ conversation_id: conversationId,
2212
+ title: task.title,
2213
+ status: task.status,
2214
+ updated_at: task.last_update_ts_ms,
2215
+ result_summary: task.result_summary,
2216
+ error_code: task.error?.code,
2217
+ error_message: task.error?.message ?? task.reason
2218
+ });
2219
+ }
2220
+ }
2221
+ return res;
2222
+ }
2223
+ async getProfile() {
2224
+ return this.transport.request("GET", "/v1/directory/profile");
2225
+ }
2226
+ async getDirectorySelf() {
2227
+ return this.transport.request("GET", "/v1/directory/self");
2228
+ }
2229
+ async updateProfile(profile) {
2230
+ const normalizedCard = profile.capability_card ? normalizeCapabilityCard(profile.capability_card) : void 0;
2231
+ const nextCapabilities = profile.capabilities !== void 0 ? normalizeProfileList(profile.capabilities) : normalizedCard ? capabilityCardToLegacyCapabilities(normalizedCard) : void 0;
2232
+ const nextTags = profile.tags !== void 0 ? normalizeProfileList(profile.tags) : void 0;
2233
+ return this.transport.request("POST", "/v1/directory/profile", {
2234
+ ...profile,
2235
+ capability_card: normalizedCard,
2236
+ capabilities: nextCapabilities,
2237
+ tags: nextTags,
2238
+ encryption_public_key: this.identity.encryptionPublicKeyJwk
2239
+ });
2240
+ }
2241
+ async createPublicLink(opts) {
2242
+ return this.transport.request("POST", "/v1/public/link", {
2243
+ slug: opts?.slug,
2244
+ enabled: opts?.enabled
2245
+ });
2246
+ }
2247
+ async getPublicSelf() {
2248
+ return this.transport.request("GET", "/v1/public/self");
2249
+ }
2250
+ async getPublicAgent(slug) {
2251
+ return this.transport.request("GET", `/v1/public/agent/${encodeURIComponent(slug)}`, void 0, true);
2252
+ }
2253
+ async createContactCard(opts) {
2254
+ return this.transport.request("POST", "/v1/public/contact-card", {
2255
+ target_did: opts?.target_did,
2256
+ referrer_did: opts?.referrer_did,
2257
+ intro_note: opts?.intro_note,
2258
+ message_template: opts?.message_template
2259
+ });
2260
+ }
2261
+ async getContactCard(id) {
2262
+ return this.transport.request("GET", `/v1/public/contact-card/${encodeURIComponent(id)}`, void 0, true);
2263
+ }
2264
+ async createTaskShare(opts) {
2265
+ return this.transport.request("POST", "/v1/public/task-share", opts);
2266
+ }
2267
+ async getTaskShare(id) {
2268
+ return this.transport.request("GET", `/v1/public/task-share/${encodeURIComponent(id)}`, void 0, true);
2269
+ }
2270
+ async revokeTaskShare(id) {
2271
+ return this.transport.request("POST", `/v1/public/task-share/${encodeURIComponent(id)}/revoke`);
2272
+ }
2273
+ async ensureEncryptionKeyPublished() {
2274
+ const selfRes = await this.getDirectorySelf();
2275
+ if (!selfRes.ok || !selfRes.data) {
2276
+ return {
2277
+ ok: false,
2278
+ error: selfRes.error ?? {
2279
+ code: ErrorCode2.INTERNAL,
2280
+ message: "Failed to inspect directory card for encryption key publication",
2281
+ hint: ERROR_HINTS[ErrorCode2.INTERNAL]
2282
+ }
2283
+ };
2284
+ }
2285
+ if (this.sameEncryptionPublicKey(selfRes.data.encryption_public_key, this.identity.encryptionPublicKeyJwk)) {
2286
+ this.agentCardCache.set(this.identity.did, {
2287
+ did: this.identity.did,
2288
+ encryption_public_key: this.identity.encryptionPublicKeyJwk
2289
+ });
2290
+ return { ok: true, data: { published: true, status: "already_registered" } };
2291
+ }
2292
+ const updateRes = await this.updateProfile({});
2293
+ if (!updateRes.ok) {
2294
+ return {
2295
+ ok: false,
2296
+ error: updateRes.error ?? {
2297
+ code: ErrorCode2.INTERNAL,
2298
+ message: "Failed to publish encryption public key to directory profile",
2299
+ hint: ERROR_HINTS[ErrorCode2.INTERNAL]
2300
+ }
2301
+ };
2302
+ }
2303
+ this.agentCardCache.set(this.identity.did, {
2304
+ did: this.identity.did,
2305
+ encryption_public_key: this.identity.encryptionPublicKeyJwk
2306
+ });
2307
+ return { ok: true, data: { published: true, status: "updated" } };
2308
+ }
2309
+ async enableDiscovery() {
2310
+ return this.transport.request("POST", "/v1/directory/enable_discovery");
2311
+ }
2312
+ async disableDiscovery() {
2313
+ return this.transport.request("POST", "/v1/directory/disable_discovery");
2314
+ }
2315
+ async browseDirectory(opts) {
2316
+ const params = new URLSearchParams();
2317
+ if (opts?.tag) params.set("tag", opts.tag);
2318
+ if (opts?.query) params.set("q", opts.query);
2319
+ if (opts?.limit != null) params.set("limit", String(opts.limit));
2320
+ if (opts?.offset != null) params.set("offset", String(opts.offset));
2321
+ if (opts?.sort) params.set("sort", opts.sort);
2322
+ const qs = params.toString();
2323
+ return this.transport.request("GET", `/v1/directory/browse${qs ? "?" + qs : ""}`);
2324
+ }
2325
+ async publishPost(opts) {
2326
+ return this.transport.request("POST", "/v1/feed/publish", { text: opts.text, artifact_ref: opts.artifact_ref });
2327
+ }
2328
+ async listFeedPublic(opts) {
2329
+ const params = new URLSearchParams();
2330
+ if (opts?.limit != null) params.set("limit", String(opts.limit));
2331
+ if (opts?.since != null) params.set("since", String(opts.since));
2332
+ const qs = params.toString();
2333
+ return this.transport.request("GET", `/v1/feed/public${qs ? "?" + qs : ""}`, void 0, true);
2334
+ }
2335
+ async listFeedByDid(did, opts) {
2336
+ const params = new URLSearchParams({ did });
2337
+ if (opts?.limit != null) params.set("limit", String(opts.limit));
2338
+ return this.transport.request("GET", `/v1/feed/by_did?${params.toString()}`, void 0, true);
2339
+ }
2340
+ async createChannel(opts) {
2341
+ return this.transport.request("POST", "/v1/channels/create", {
2342
+ name: opts.name,
2343
+ alias: opts.alias,
2344
+ description: opts.description,
2345
+ join_policy: "open",
2346
+ discoverable: opts.discoverable
2347
+ });
2348
+ }
2349
+ async updateChannel(opts) {
2350
+ const alias = opts.alias.replace(/^@ch\//, "");
2351
+ return this.transport.request("PATCH", "/v1/channels/update", {
2352
+ alias,
2353
+ name: opts.name,
2354
+ description: opts.description,
2355
+ discoverable: opts.discoverable
2356
+ });
2357
+ }
2358
+ async deleteChannel(alias) {
2359
+ const a = alias.replace(/^@ch\//, "");
2360
+ return this.transport.request("DELETE", "/v1/channels/delete", { alias: a });
2361
+ }
2362
+ async discoverChannels(opts) {
2363
+ const params = new URLSearchParams();
2364
+ if (opts?.limit != null) params.set("limit", String(opts.limit));
2365
+ if (opts?.query) params.set("q", opts.query);
2366
+ const qs = params.toString();
2367
+ return this.transport.request("GET", `/v1/channels/discover${qs ? "?" + qs : ""}`, void 0, true);
2368
+ }
2369
+ async joinChannel(alias) {
2370
+ return this.transport.request("POST", "/v1/channels/join", { alias });
2371
+ }
2372
+ getDid() {
2373
+ return this.identity.did;
2374
+ }
2375
+ async decryptEnvelopePayload(envelope) {
2376
+ return decryptPayloadForIdentity(envelope.schema, envelope.payload ?? {}, this.identity);
2377
+ }
2378
+ // === Billing: device linking ===
2379
+ async createBillingLinkCode() {
2380
+ return this.transport.request("POST", "/v1/billing/link-code");
2381
+ }
2382
+ async redeemBillingLink(code) {
2383
+ return this.transport.request("POST", "/v1/billing/link", { code });
2384
+ }
2385
+ async unlinkBillingDevice(did) {
2386
+ return this.transport.request("POST", "/v1/billing/unlink", { did });
2387
+ }
2388
+ async getLinkedDevices() {
2389
+ return this.transport.request("GET", "/v1/billing/linked-devices");
2390
+ }
2391
+ // P0: High-level send-task-and-wait
2392
+ async sendTaskAndWait(targetDid, task, opts) {
2393
+ const { v7: uuidv7 } = await import("uuid");
2394
+ const timeout = opts?.timeoutMs ?? 12e4;
2395
+ const pollInterval = opts?.pollIntervalMs ?? 2e3;
2396
+ const taskId = `t_${uuidv7()}`;
2397
+ const startTime = Date.now();
2398
+ const openRes = await this.openConversation(targetDid);
2399
+ if (!openRes.ok || !openRes.data) {
2400
+ return { status: "error", task_id: taskId, error: { code: "E_OPEN_FAILED", message: "Failed to open conversation" }, elapsed_ms: Date.now() - startTime };
2401
+ }
2402
+ let conversationId = openRes.data.conversation_id;
2403
+ if (!openRes.data.trusted) {
2404
+ await this.sendContactRequest(conversationId, task.title);
2405
+ const approvalDeadline = startTime + timeout;
2406
+ while (Date.now() < approvalDeadline) {
2407
+ await sleep2(pollInterval);
2408
+ const reopen = await this.openConversation(targetDid);
2409
+ if (reopen.ok && reopen.data?.trusted) {
2410
+ conversationId = reopen.data.conversation_id;
2411
+ break;
2412
+ }
2413
+ }
2414
+ if (Date.now() >= approvalDeadline) {
2415
+ return { status: "error", task_id: taskId, error: { code: "E_NOT_APPROVED", message: "Contact request not approved within timeout" }, elapsed_ms: Date.now() - startTime };
2416
+ }
2417
+ }
2418
+ const sendRes = await this.sendTask(conversationId, { task_id: taskId, ...task });
2419
+ if (!sendRes.ok) {
2420
+ return { status: "error", task_id: taskId, error: { code: sendRes.error?.code ?? "E_SEND_FAILED", message: sendRes.error?.message ?? "Send failed" }, elapsed_ms: Date.now() - startTime };
2421
+ }
2422
+ const session = this.sessionManager?.getByConversationId(conversationId);
2423
+ this.taskThreadManager?.upsert({
2424
+ task_id: taskId,
2425
+ session_key: session?.session_key ?? buildSessionKey({ localDid: this.identity.did, conversationId, remoteDid: targetDid }),
2426
+ conversation_id: conversationId,
2427
+ title: task.title,
2428
+ status: "queued",
2429
+ started_at: Date.now(),
2430
+ updated_at: Date.now()
2431
+ });
2432
+ const sinceSeq = sendRes.data?.seq ?? 0;
2433
+ const deadline = startTime + timeout;
2434
+ while (Date.now() < deadline) {
2435
+ await sleep2(pollInterval);
2436
+ const statusRes = await this.getTaskStatus(conversationId, taskId);
2437
+ if (statusRes.ok && statusRes.data) {
2438
+ if (statusRes.data.status === "failed" || statusRes.data.status === "cancelled") {
2439
+ return {
2440
+ status: "error",
2441
+ task_id: taskId,
2442
+ error: {
2443
+ code: statusRes.data.error?.code ?? (statusRes.data.status === "cancelled" ? "E_CANCELLED" : "E_TASK_FAILED"),
2444
+ message: statusRes.data.error?.message ?? statusRes.data.reason ?? `Task ${statusRes.data.status}`
2445
+ },
2446
+ elapsed_ms: Date.now() - startTime
2447
+ };
2448
+ }
2449
+ }
2450
+ const fetchRes = await this.fetchInbox(conversationId, { sinceSeq });
2451
+ if (!fetchRes.ok || !fetchRes.data) continue;
2452
+ for (const msg of fetchRes.data.messages) {
2453
+ if (msg.schema === SCHEMA_RESULT2 && msg.payload?.task_id === taskId) {
2454
+ return {
2455
+ status: msg.payload.status === "ok" ? "ok" : "error",
2456
+ task_id: taskId,
2457
+ result: msg.payload.output,
2458
+ error: msg.payload.error,
2459
+ elapsed_ms: Date.now() - startTime
2460
+ };
2461
+ }
2462
+ }
2463
+ }
2464
+ return { status: "error", task_id: taskId, error: { code: "E_TIMEOUT", message: `No result within ${timeout}ms` }, elapsed_ms: timeout };
2465
+ }
2466
+ async sendHandoff(targetDid, handoff, opts) {
2467
+ const { v7: uuidv7 } = await import("uuid");
2468
+ const timeout = opts?.timeoutMs ?? 12e4;
2469
+ const pollInterval = opts?.pollIntervalMs ?? 2e3;
2470
+ const taskId = `t_${uuidv7()}`;
2471
+ const openRes = await this.openConversation(targetDid);
2472
+ if (!openRes.ok || !openRes.data) {
2473
+ return {
2474
+ ok: false,
2475
+ error: {
2476
+ code: ErrorCode2.INTERNAL,
2477
+ message: openRes.error?.message ?? "Failed to open conversation",
2478
+ hint: ERROR_HINTS[ErrorCode2.INTERNAL]
2479
+ }
2480
+ };
2481
+ }
2482
+ let conversationId = openRes.data.conversation_id;
2483
+ if (!openRes.data.trusted) {
2484
+ await this.sendContactRequest(conversationId, handoff.title);
2485
+ const approvalDeadline = Date.now() + timeout;
2486
+ while (Date.now() < approvalDeadline) {
2487
+ await sleep2(pollInterval);
2488
+ const reopen = await this.openConversation(targetDid);
2489
+ if (reopen.ok && reopen.data?.trusted) {
2490
+ conversationId = reopen.data.conversation_id;
2491
+ break;
2492
+ }
2493
+ }
2494
+ if (Date.now() >= approvalDeadline) {
2495
+ return {
2496
+ ok: false,
2497
+ error: {
2498
+ code: ErrorCode2.STRANGER_QUARANTINED,
2499
+ message: "Contact request not approved within timeout",
2500
+ hint: ERROR_HINTS[ErrorCode2.STRANGER_QUARANTINED]
2501
+ }
2502
+ };
2503
+ }
2504
+ }
2505
+ const summary = opts?.sessionKey ? this.sessionSummaryManager?.get(opts.sessionKey) : opts?.conversationId ? this.sessionSummaryManager?.get(this.sessionManager?.getByConversationId(opts.conversationId)?.session_key || "") : void 0;
2506
+ const handoffPayload = {
2507
+ objective: handoff.objective,
2508
+ carry_forward_summary: (handoff.carry_forward_summary ?? buildSessionSummaryHandoffText(summary)) || void 0,
2509
+ success_criteria: handoff.success_criteria,
2510
+ callback_session_key: handoff.callback_session_key ?? opts?.sessionKey,
2511
+ priority: handoff.priority,
2512
+ delegated_by: this.identity.did,
2513
+ delegated_to: targetDid
2514
+ };
2515
+ const sendRes = await this.sendTask(conversationId, {
2516
+ task_id: taskId,
2517
+ title: handoff.title,
2518
+ description: handoff.description,
2519
+ input: handoff.input,
2520
+ handoff: handoffPayload
2521
+ });
2522
+ if (!sendRes.ok) {
2523
+ return {
2524
+ ok: false,
2525
+ error: {
2526
+ code: sendRes.error?.code ?? ErrorCode2.INTERNAL,
2527
+ message: sendRes.error?.message ?? "Send failed",
2528
+ hint: ERROR_HINTS[sendRes.error?.code ?? ErrorCode2.INTERNAL]
2529
+ }
2530
+ };
2531
+ }
2532
+ const session = this.sessionManager?.getByConversationId(conversationId);
2533
+ const sessionKey = session?.session_key ?? buildSessionKey({ localDid: this.identity.did, conversationId, remoteDid: targetDid });
2534
+ this.taskThreadManager?.upsert({
2535
+ task_id: taskId,
2536
+ session_key: sessionKey,
2537
+ conversation_id: conversationId,
2538
+ title: handoff.title,
2539
+ status: "queued",
2540
+ started_at: Date.now(),
2541
+ updated_at: Date.now()
2542
+ });
2543
+ this.taskHandoffManager?.upsert({
2544
+ task_id: taskId,
2545
+ session_key: sessionKey,
2546
+ conversation_id: conversationId,
2547
+ ...handoffPayload,
2548
+ updated_at: Date.now()
2549
+ });
2550
+ return {
2551
+ ok: true,
2552
+ data: {
2553
+ task_id: taskId,
2554
+ conversation_id: conversationId,
2555
+ handoff: handoffPayload
2556
+ }
2557
+ };
2558
+ }
2559
+ };
2560
+ function normalizeTaskThreadStatus(state) {
2561
+ if (state === "completed") return "processed";
2562
+ if (state === "cancel_requested") return "running";
2563
+ if (state === "cancelled") return "cancelled";
2564
+ if (state === "failed") return "failed";
2565
+ if (state === "running") return "running";
2566
+ return "queued";
2567
+ }
2568
+ function sleep2(ms) {
2569
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
2570
+ }
2571
+
2572
+ // src/auth.ts
2573
+ var REFRESH_GRACE_MS = 5 * 60 * 1e3;
2574
+ async function ensureTokenValid(identityPath, serverUrl) {
2575
+ if (!identityExists(identityPath)) return false;
2576
+ const id = loadIdentity(identityPath);
2577
+ const token = id.accessToken;
2578
+ const expiresAt = id.tokenExpiresAt;
2579
+ if (!token || expiresAt == null) return false;
2580
+ const now = Date.now();
2581
+ if (expiresAt > now + REFRESH_GRACE_MS) return false;
2582
+ const baseUrl = (serverUrl ?? id.serverUrl ?? "https://pingagent.chat").replace(/\/$/, "");
2583
+ try {
2584
+ const res = await fetch(`${baseUrl}/v1/auth/refresh`, {
2585
+ method: "POST",
2586
+ headers: { "Content-Type": "application/json" },
2587
+ body: JSON.stringify({ access_token: token })
2588
+ });
2589
+ const text = await res.text();
2590
+ let data;
2591
+ try {
2592
+ data = text ? JSON.parse(text) : {};
2593
+ } catch {
2594
+ return false;
2595
+ }
2596
+ if (!res.ok || !data.ok || !data.data?.access_token || data.data.expires_ms == null) return false;
2597
+ const newExpiresAt = now + data.data.expires_ms;
2598
+ updateStoredToken(data.data.access_token, newExpiresAt, identityPath);
2599
+ return true;
2600
+ } catch {
2601
+ return false;
2602
+ }
2603
+ }
2604
+
2605
+ // src/store.ts
2606
+ import Database from "better-sqlite3";
2607
+ import * as fs2 from "fs";
2608
+ import * as path3 from "path";
2609
+ var SCHEMA_SQL = `
2610
+ CREATE TABLE IF NOT EXISTS contacts (
2611
+ did TEXT PRIMARY KEY,
2612
+ alias TEXT,
2613
+ display_name TEXT,
2614
+ notes TEXT,
2615
+ conversation_id TEXT,
2616
+ trusted INTEGER NOT NULL DEFAULT 0,
2617
+ added_at INTEGER NOT NULL,
2618
+ last_message_at INTEGER,
2619
+ tags TEXT
2620
+ );
2621
+ CREATE INDEX IF NOT EXISTS idx_contacts_alias ON contacts(alias);
2622
+
2623
+ CREATE TABLE IF NOT EXISTS messages (
2624
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2625
+ conversation_id TEXT NOT NULL,
2626
+ message_id TEXT NOT NULL UNIQUE,
2627
+ seq INTEGER,
2628
+ sender_did TEXT NOT NULL,
2629
+ schema TEXT NOT NULL,
2630
+ payload TEXT NOT NULL,
2631
+ ts_ms INTEGER NOT NULL,
2632
+ direction TEXT NOT NULL CHECK(direction IN ('sent', 'received'))
2633
+ );
2634
+ CREATE INDEX IF NOT EXISTS idx_messages_conv_seq ON messages(conversation_id, seq);
2635
+ CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages(ts_ms);
2636
+
2637
+ CREATE TABLE IF NOT EXISTS sync_state (
2638
+ conversation_id TEXT PRIMARY KEY,
2639
+ last_synced_seq INTEGER NOT NULL DEFAULT 0,
2640
+ last_synced_at INTEGER
2641
+ );
2642
+
2643
+ CREATE TABLE IF NOT EXISTS session_state (
2644
+ session_key TEXT PRIMARY KEY,
2645
+ remote_did TEXT,
2646
+ conversation_id TEXT,
2647
+ trust_state TEXT NOT NULL DEFAULT 'stranger',
2648
+ last_message_preview TEXT,
2649
+ last_remote_activity_at INTEGER,
2650
+ last_read_seq INTEGER NOT NULL DEFAULT 0,
2651
+ unread_count INTEGER NOT NULL DEFAULT 0,
2652
+ active INTEGER NOT NULL DEFAULT 0,
2653
+ updated_at INTEGER NOT NULL
2654
+ );
2655
+ CREATE INDEX IF NOT EXISTS idx_session_state_updated ON session_state(updated_at DESC);
2656
+ CREATE INDEX IF NOT EXISTS idx_session_state_conversation ON session_state(conversation_id);
2657
+ CREATE INDEX IF NOT EXISTS idx_session_state_remote_did ON session_state(remote_did);
2658
+
2659
+ CREATE TABLE IF NOT EXISTS task_threads (
2660
+ task_id TEXT PRIMARY KEY,
2661
+ session_key TEXT NOT NULL,
2662
+ conversation_id TEXT NOT NULL,
2663
+ title TEXT,
2664
+ status TEXT NOT NULL,
2665
+ started_at INTEGER,
2666
+ updated_at INTEGER NOT NULL,
2667
+ result_summary TEXT,
2668
+ error_code TEXT,
2669
+ error_message TEXT
2670
+ );
2671
+ CREATE INDEX IF NOT EXISTS idx_task_threads_session ON task_threads(session_key, updated_at DESC);
2672
+ CREATE INDEX IF NOT EXISTS idx_task_threads_conversation ON task_threads(conversation_id, updated_at DESC);
2673
+
2674
+ CREATE TABLE IF NOT EXISTS session_summaries (
2675
+ session_key TEXT PRIMARY KEY,
2676
+ objective TEXT,
2677
+ context TEXT,
2678
+ constraints TEXT,
2679
+ decisions TEXT,
2680
+ open_questions TEXT,
2681
+ next_action TEXT,
2682
+ handoff_ready_text TEXT,
2683
+ updated_at INTEGER NOT NULL
2684
+ );
2685
+ CREATE INDEX IF NOT EXISTS idx_session_summaries_updated ON session_summaries(updated_at DESC);
2686
+
2687
+ CREATE TABLE IF NOT EXISTS task_handoffs (
2688
+ task_id TEXT PRIMARY KEY,
2689
+ session_key TEXT NOT NULL,
2690
+ conversation_id TEXT NOT NULL,
2691
+ objective TEXT,
2692
+ carry_forward_summary TEXT,
2693
+ success_criteria TEXT,
2694
+ callback_session_key TEXT,
2695
+ priority TEXT,
2696
+ delegated_by TEXT,
2697
+ delegated_to TEXT,
2698
+ updated_at INTEGER NOT NULL
2699
+ );
2700
+ CREATE INDEX IF NOT EXISTS idx_task_handoffs_session ON task_handoffs(session_key, updated_at DESC);
2701
+ CREATE INDEX IF NOT EXISTS idx_task_handoffs_conversation ON task_handoffs(conversation_id, updated_at DESC);
2702
+
2703
+ CREATE TABLE IF NOT EXISTS trust_policy_audit (
2704
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2705
+ ts_ms INTEGER NOT NULL,
2706
+ event_type TEXT NOT NULL,
2707
+ policy_scope TEXT,
2708
+ remote_did TEXT,
2709
+ sender_alias TEXT,
2710
+ sender_verification_status TEXT,
2711
+ session_key TEXT,
2712
+ conversation_id TEXT,
2713
+ action TEXT,
2714
+ outcome TEXT,
2715
+ explanation TEXT,
2716
+ matched_rule TEXT,
2717
+ detail_json TEXT
2718
+ );
2719
+ CREATE INDEX IF NOT EXISTS idx_trust_policy_audit_ts ON trust_policy_audit(ts_ms DESC, id DESC);
2720
+ CREATE INDEX IF NOT EXISTS idx_trust_policy_audit_session ON trust_policy_audit(session_key, ts_ms DESC);
2721
+ CREATE INDEX IF NOT EXISTS idx_trust_policy_audit_remote ON trust_policy_audit(remote_did, ts_ms DESC);
2722
+
2723
+ CREATE TABLE IF NOT EXISTS trust_policy_recommendations (
2724
+ id TEXT PRIMARY KEY,
2725
+ policy TEXT NOT NULL,
2726
+ remote_did TEXT NOT NULL,
2727
+ match TEXT NOT NULL,
2728
+ action TEXT NOT NULL,
2729
+ current_action TEXT NOT NULL,
2730
+ confidence TEXT NOT NULL,
2731
+ reason TEXT NOT NULL,
2732
+ signals_json TEXT NOT NULL,
2733
+ status TEXT NOT NULL DEFAULT 'open',
2734
+ first_seen_at INTEGER NOT NULL,
2735
+ last_seen_at INTEGER NOT NULL,
2736
+ updated_at INTEGER NOT NULL,
2737
+ last_state_change_at INTEGER,
2738
+ applied_at INTEGER,
2739
+ dismissed_at INTEGER
2740
+ );
2741
+ CREATE INDEX IF NOT EXISTS idx_trust_policy_recommendations_status ON trust_policy_recommendations(status, updated_at DESC);
2742
+ CREATE INDEX IF NOT EXISTS idx_trust_policy_recommendations_remote ON trust_policy_recommendations(remote_did, updated_at DESC);
2743
+
2744
+ CREATE TABLE IF NOT EXISTS collaboration_events (
2745
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2746
+ ts_ms INTEGER NOT NULL,
2747
+ event_type TEXT NOT NULL,
2748
+ severity TEXT NOT NULL,
2749
+ session_key TEXT,
2750
+ conversation_id TEXT,
2751
+ target_human_session TEXT,
2752
+ summary TEXT NOT NULL,
2753
+ approval_required INTEGER NOT NULL DEFAULT 0,
2754
+ approval_status TEXT NOT NULL DEFAULT 'not_required',
2755
+ detail_json TEXT
2756
+ );
2757
+ CREATE INDEX IF NOT EXISTS idx_collaboration_events_ts ON collaboration_events(ts_ms DESC, id DESC);
2758
+ CREATE INDEX IF NOT EXISTS idx_collaboration_events_session ON collaboration_events(session_key, ts_ms DESC);
2759
+ CREATE INDEX IF NOT EXISTS idx_collaboration_events_conversation ON collaboration_events(conversation_id, ts_ms DESC);
2760
+ CREATE INDEX IF NOT EXISTS idx_collaboration_events_target_human ON collaboration_events(target_human_session, ts_ms DESC);
2761
+
2762
+ CREATE TABLE IF NOT EXISTS collaboration_projection_outbox (
2763
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2764
+ created_at INTEGER NOT NULL,
2765
+ session_key TEXT,
2766
+ conversation_id TEXT,
2767
+ target_human_session TEXT,
2768
+ projection_kind TEXT NOT NULL,
2769
+ summary TEXT NOT NULL,
2770
+ body_json TEXT NOT NULL,
2771
+ status TEXT NOT NULL DEFAULT 'pending',
2772
+ attempt_count INTEGER NOT NULL DEFAULT 0,
2773
+ last_error TEXT,
2774
+ delivered_at INTEGER,
2775
+ source_event_id INTEGER
2776
+ );
2777
+ CREATE INDEX IF NOT EXISTS idx_collaboration_projection_outbox_status ON collaboration_projection_outbox(status, created_at ASC, id ASC);
2778
+ CREATE INDEX IF NOT EXISTS idx_collaboration_projection_outbox_session ON collaboration_projection_outbox(session_key, created_at DESC, id DESC);
2779
+ CREATE INDEX IF NOT EXISTS idx_collaboration_projection_outbox_target ON collaboration_projection_outbox(target_human_session, created_at DESC, id DESC);
2780
+
2781
+ CREATE TABLE IF NOT EXISTS operator_seen_state (
2782
+ operator_id TEXT NOT NULL,
2783
+ scope_type TEXT NOT NULL,
2784
+ scope_key TEXT NOT NULL DEFAULT '',
2785
+ last_seen_ts INTEGER NOT NULL,
2786
+ updated_at INTEGER NOT NULL,
2787
+ PRIMARY KEY (operator_id, scope_type, scope_key)
2788
+ );
2789
+ CREATE INDEX IF NOT EXISTS idx_operator_seen_state_updated ON operator_seen_state(updated_at DESC);
2790
+ CREATE INDEX IF NOT EXISTS idx_operator_seen_state_scope ON operator_seen_state(scope_type, scope_key, updated_at DESC);
2791
+ `;
2792
+ var LocalStore = class {
2793
+ db;
2794
+ constructor(dbPath) {
2795
+ const p = getStorePath(dbPath);
2796
+ const dir = path3.dirname(p);
2797
+ if (!fs2.existsSync(dir)) {
2798
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
2799
+ }
2800
+ this.db = new Database(p);
2801
+ this.db.pragma("journal_mode = WAL");
2802
+ this.db.exec(SCHEMA_SQL);
2803
+ this.applyMigrations();
2804
+ }
2805
+ getDb() {
2806
+ return this.db;
2807
+ }
2808
+ close() {
2809
+ this.db.close();
2810
+ }
2811
+ applyMigrations() {
2812
+ const alterStatements = [
2813
+ `ALTER TABLE session_state ADD COLUMN remote_did TEXT`,
2814
+ `ALTER TABLE session_state ADD COLUMN conversation_id TEXT`,
2815
+ `ALTER TABLE session_state ADD COLUMN trust_state TEXT NOT NULL DEFAULT 'stranger'`,
2816
+ `ALTER TABLE session_state ADD COLUMN last_message_preview TEXT`,
2817
+ `ALTER TABLE session_state ADD COLUMN last_remote_activity_at INTEGER`,
2818
+ `ALTER TABLE session_state ADD COLUMN last_read_seq INTEGER NOT NULL DEFAULT 0`,
2819
+ `ALTER TABLE session_state ADD COLUMN unread_count INTEGER NOT NULL DEFAULT 0`,
2820
+ `ALTER TABLE session_state ADD COLUMN active INTEGER NOT NULL DEFAULT 0`,
2821
+ `ALTER TABLE session_state ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0`,
2822
+ `ALTER TABLE task_threads ADD COLUMN title TEXT`,
2823
+ `ALTER TABLE task_threads ADD COLUMN result_summary TEXT`,
2824
+ `ALTER TABLE task_threads ADD COLUMN error_code TEXT`,
2825
+ `ALTER TABLE task_threads ADD COLUMN error_message TEXT`
2826
+ ];
2827
+ for (const sql of alterStatements) {
2828
+ try {
2829
+ this.db.exec(sql);
2830
+ } catch {
2831
+ }
2832
+ }
2833
+ }
2834
+ };
2835
+
2836
+ // src/trust-policy.ts
2837
+ var CONTACT_ACTIONS = /* @__PURE__ */ new Set(["approve", "manual", "reject"]);
2838
+ var TASK_ACTIONS = /* @__PURE__ */ new Set(["bridge", "execute", "deny"]);
2839
+ var COLLABORATION_PROJECTION_PRESETS = /* @__PURE__ */ new Set(["quiet", "balanced", "strict"]);
2840
+ function defaultTrustPolicyDoc() {
2841
+ return {
2842
+ version: 1,
2843
+ contact_policy: {
2844
+ enabled: true,
2845
+ default_action: "manual",
2846
+ rules: []
2847
+ },
2848
+ task_policy: {
2849
+ enabled: true,
2850
+ default_action: "bridge",
2851
+ rules: []
2852
+ },
2853
+ collaboration_projection: {
2854
+ preset: "balanced"
2855
+ }
2856
+ };
2857
+ }
2858
+ function normalizeTrustPolicyDoc(raw) {
2859
+ const base = defaultTrustPolicyDoc();
2860
+ const contactRules = Array.isArray(raw?.contact_policy?.rules) ? raw.contact_policy.rules.filter((rule) => typeof rule?.match === "string" && CONTACT_ACTIONS.has(rule?.action)) : [];
2861
+ const taskRules = Array.isArray(raw?.task_policy?.rules) ? raw.task_policy.rules.filter((rule) => typeof rule?.match === "string" && TASK_ACTIONS.has(rule?.action)) : [];
2862
+ return {
2863
+ version: 1,
2864
+ contact_policy: {
2865
+ enabled: raw?.contact_policy?.enabled !== false,
2866
+ default_action: CONTACT_ACTIONS.has(raw?.contact_policy?.default_action) ? raw.contact_policy.default_action : base.contact_policy.default_action,
2867
+ rules: contactRules
2868
+ },
2869
+ task_policy: {
2870
+ enabled: raw?.task_policy?.enabled !== false,
2871
+ default_action: TASK_ACTIONS.has(raw?.task_policy?.default_action) ? raw.task_policy.default_action : base.task_policy.default_action,
2872
+ rules: taskRules
2873
+ },
2874
+ collaboration_projection: {
2875
+ preset: COLLABORATION_PROJECTION_PRESETS.has(raw?.collaboration_projection?.preset) ? raw.collaboration_projection.preset : base.collaboration_projection.preset
2876
+ }
2877
+ };
2878
+ }
2879
+ function matchesTrustPolicyRule(match, context) {
2880
+ if (match === "*") return true;
2881
+ if (match.startsWith("did:agent:")) {
2882
+ return context.sender_did === match;
2883
+ }
2884
+ if (match.startsWith("verification_status:")) {
2885
+ return context.sender_verification_status === match.split(":")[1];
2886
+ }
2887
+ if (match.startsWith("alias:")) {
2888
+ const pattern = match.slice(6);
2889
+ const senderAlias = context.sender_alias ?? "";
2890
+ if (pattern.endsWith("*")) return senderAlias.startsWith(pattern.slice(0, -1));
2891
+ return senderAlias === pattern;
2892
+ }
2893
+ return false;
2894
+ }
2895
+ function decideContactPolicy(policyDoc, context, opts) {
2896
+ if (policyDoc.contact_policy.enabled) {
2897
+ for (const rule of policyDoc.contact_policy.rules) {
2898
+ if (matchesTrustPolicyRule(rule.match, context)) {
2899
+ return {
2900
+ action: rule.action,
2901
+ source: "rule",
2902
+ matched_rule: rule,
2903
+ explanation: `Matched contact_policy rule ${rule.match} -> ${rule.action}`
2904
+ };
2905
+ }
2906
+ }
2907
+ return {
2908
+ action: policyDoc.contact_policy.default_action,
2909
+ source: "default",
2910
+ explanation: `No contact_policy rule matched; using default_action=${policyDoc.contact_policy.default_action}`
2911
+ };
2912
+ }
2913
+ if (opts?.legacyAutoApproveEnabled) {
2914
+ for (const rule of opts.legacyAutoApproveRules ?? []) {
2915
+ if (rule.action === "approve" && matchesTrustPolicyRule(rule.match, context)) {
2916
+ return {
2917
+ action: "approve",
2918
+ source: "legacy_auto_approve",
2919
+ matched_rule: { match: rule.match, action: "approve" },
2920
+ explanation: `Matched legacy auto_approve rule ${rule.match} -> approve`
2921
+ };
2922
+ }
2923
+ }
2924
+ }
2925
+ return {
2926
+ action: "manual",
2927
+ source: "runtime_default",
2928
+ explanation: "contact_policy disabled and no legacy auto_approve match; defaulting to manual"
2929
+ };
2930
+ }
2931
+ function decideTaskPolicy(policyDoc, context, opts) {
2932
+ if (policyDoc.task_policy.enabled) {
2933
+ for (const rule of policyDoc.task_policy.rules) {
2934
+ if (matchesTrustPolicyRule(rule.match, context)) {
2935
+ return {
2936
+ action: rule.action,
2937
+ source: "rule",
2938
+ matched_rule: rule,
2939
+ explanation: `Matched task_policy rule ${rule.match} -> ${rule.action}`
2940
+ };
2941
+ }
2942
+ }
2943
+ return {
2944
+ action: policyDoc.task_policy.default_action,
2945
+ source: "default",
2946
+ explanation: `No task_policy rule matched; using default_action=${policyDoc.task_policy.default_action}`
2947
+ };
2948
+ }
2949
+ const runtimeDefault = opts?.runtimeMode === "executor" ? "execute" : "bridge";
2950
+ return {
2951
+ action: runtimeDefault,
2952
+ source: "runtime_default",
2953
+ explanation: `task_policy disabled; defaulting to runtime_mode=${opts?.runtimeMode ?? "bridge"} -> ${runtimeDefault}`
2954
+ };
2955
+ }
2956
+
2957
+ // src/trust-policy-audit.ts
2958
+ function rowToEvent2(row) {
2959
+ return {
2960
+ id: row.id,
2961
+ ts_ms: row.ts_ms,
2962
+ event_type: row.event_type,
2963
+ policy_scope: row.policy_scope ?? void 0,
2964
+ remote_did: row.remote_did ?? void 0,
2965
+ sender_alias: row.sender_alias ?? void 0,
2966
+ sender_verification_status: row.sender_verification_status ?? void 0,
2967
+ session_key: row.session_key ?? void 0,
2968
+ conversation_id: row.conversation_id ?? void 0,
2969
+ action: row.action ?? void 0,
2970
+ outcome: row.outcome ?? void 0,
2971
+ explanation: row.explanation ?? void 0,
2972
+ matched_rule: row.matched_rule ?? void 0,
2973
+ detail: row.detail_json ? JSON.parse(row.detail_json) : void 0
2974
+ };
2975
+ }
2976
+ function buildSummaryFromEvents(events) {
2977
+ const byType = {};
2978
+ const byPolicyScope = {};
2979
+ let latestTsMs = 0;
2980
+ for (const event of events) {
2981
+ byType[event.event_type] = (byType[event.event_type] ?? 0) + 1;
2982
+ if (event.policy_scope) byPolicyScope[event.policy_scope] = (byPolicyScope[event.policy_scope] ?? 0) + 1;
2983
+ latestTsMs = Math.max(latestTsMs, event.ts_ms);
2984
+ }
2985
+ return {
2986
+ total_events: events.length,
2987
+ latest_ts_ms: latestTsMs || void 0,
2988
+ by_type: byType,
2989
+ by_policy_scope: byPolicyScope
2990
+ };
2991
+ }
2992
+ var TrustPolicyAuditManager = class {
2993
+ constructor(store) {
2994
+ this.store = store;
2995
+ }
2996
+ record(event) {
2997
+ const tsMs = event.ts_ms ?? Date.now();
2998
+ const result = this.store.getDb().prepare(`
2999
+ INSERT INTO trust_policy_audit (
3000
+ ts_ms, event_type, policy_scope, remote_did, sender_alias, sender_verification_status,
3001
+ session_key, conversation_id, action, outcome, explanation, matched_rule, detail_json
3002
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3003
+ `).run(
3004
+ tsMs,
3005
+ event.event_type,
3006
+ event.policy_scope ?? null,
3007
+ event.remote_did ?? null,
3008
+ event.sender_alias ?? null,
3009
+ event.sender_verification_status ?? null,
3010
+ event.session_key ?? null,
3011
+ event.conversation_id ?? null,
3012
+ event.action ?? null,
3013
+ event.outcome ?? null,
3014
+ event.explanation ?? null,
3015
+ event.matched_rule ?? null,
3016
+ event.detail ? JSON.stringify(event.detail) : null
3017
+ );
3018
+ return {
3019
+ id: Number(result.lastInsertRowid),
3020
+ ts_ms: tsMs,
3021
+ event_type: event.event_type,
3022
+ policy_scope: event.policy_scope,
3023
+ remote_did: event.remote_did,
3024
+ sender_alias: event.sender_alias,
3025
+ sender_verification_status: event.sender_verification_status,
3026
+ session_key: event.session_key,
3027
+ conversation_id: event.conversation_id,
3028
+ action: event.action,
3029
+ outcome: event.outcome,
3030
+ explanation: event.explanation,
3031
+ matched_rule: event.matched_rule,
3032
+ detail: event.detail
3033
+ };
3034
+ }
3035
+ listRecent(limit = 50) {
3036
+ const rows = this.store.getDb().prepare("SELECT * FROM trust_policy_audit ORDER BY ts_ms DESC, id DESC LIMIT ?").all(limit);
3037
+ return rows.map(rowToEvent2);
3038
+ }
3039
+ listBySession(sessionKey, limit = 50) {
3040
+ const rows = this.store.getDb().prepare("SELECT * FROM trust_policy_audit WHERE session_key = ? ORDER BY ts_ms DESC, id DESC LIMIT ?").all(sessionKey, limit);
3041
+ return rows.map(rowToEvent2);
3042
+ }
3043
+ listByRemoteDid(remoteDid, limit = 50) {
3044
+ const rows = this.store.getDb().prepare("SELECT * FROM trust_policy_audit WHERE remote_did = ? ORDER BY ts_ms DESC, id DESC LIMIT ?").all(remoteDid, limit);
3045
+ return rows.map(rowToEvent2);
3046
+ }
3047
+ summarize(limit = 200) {
3048
+ return buildSummaryFromEvents(this.listRecent(limit));
3049
+ }
3050
+ };
3051
+ function aggregateLearning(sessions, tasks, events) {
3052
+ const summaries = /* @__PURE__ */ new Map();
3053
+ function ensure(remoteDid) {
3054
+ if (!remoteDid) return null;
3055
+ const existing = summaries.get(remoteDid);
3056
+ if (existing) return existing;
3057
+ const created = {
3058
+ remote_did: remoteDid,
3059
+ trusted_sessions: 0,
3060
+ pending_sessions: 0,
3061
+ blocked_sessions: 0,
3062
+ revoked_sessions: 0,
3063
+ processed_tasks: 0,
3064
+ failed_tasks: 0,
3065
+ cancelled_tasks: 0,
3066
+ contact_approvals: 0,
3067
+ contact_manual_reviews: 0,
3068
+ contact_rejections: 0,
3069
+ task_bridged: 0,
3070
+ task_executed: 0,
3071
+ task_denied: 0
3072
+ };
3073
+ summaries.set(remoteDid, created);
3074
+ return created;
3075
+ }
3076
+ const remoteBySession = /* @__PURE__ */ new Map();
3077
+ for (const session of sessions) {
3078
+ const summary = ensure(session.remote_did);
3079
+ if (!summary) continue;
3080
+ remoteBySession.set(session.session_key, session.remote_did);
3081
+ summary.last_seen_at = Math.max(summary.last_seen_at ?? 0, session.last_remote_activity_at ?? 0, session.updated_at ?? 0) || summary.last_seen_at;
3082
+ if (session.trust_state === "trusted") summary.trusted_sessions += 1;
3083
+ if (session.trust_state === "pending") summary.pending_sessions += 1;
3084
+ if (session.trust_state === "blocked") summary.blocked_sessions += 1;
3085
+ if (session.trust_state === "revoked") summary.revoked_sessions += 1;
3086
+ }
3087
+ for (const task of tasks) {
3088
+ const remoteDid = remoteBySession.get(task.session_key);
3089
+ const summary = ensure(remoteDid);
3090
+ if (!summary) continue;
3091
+ summary.last_seen_at = Math.max(summary.last_seen_at ?? 0, task.updated_at ?? 0) || summary.last_seen_at;
3092
+ if (task.status === "processed") summary.processed_tasks += 1;
3093
+ if (task.status === "failed") summary.failed_tasks += 1;
3094
+ if (task.status === "cancelled") summary.cancelled_tasks += 1;
3095
+ }
3096
+ for (const event of events) {
3097
+ const summary = ensure(event.remote_did ?? remoteBySession.get(event.session_key ?? ""));
3098
+ if (!summary) continue;
3099
+ summary.last_seen_at = Math.max(summary.last_seen_at ?? 0, event.ts_ms ?? 0) || summary.last_seen_at;
3100
+ if (event.event_type === "contact_auto_approved") summary.contact_approvals += 1;
3101
+ if (event.event_type === "contact_manual_review") summary.contact_manual_reviews += 1;
3102
+ if (event.event_type === "contact_rejected" || event.event_type === "session_blocked") summary.contact_rejections += 1;
3103
+ if (event.event_type === "session_revoked") summary.revoked_sessions += 1;
3104
+ if (event.event_type === "task_bridged") summary.task_bridged += 1;
3105
+ if (event.event_type === "task_executed") summary.task_executed += 1;
3106
+ if (event.event_type === "task_denied") summary.task_denied += 1;
3107
+ if (event.event_type === "task_processed") summary.processed_tasks += 1;
3108
+ if (event.event_type === "task_failed") summary.failed_tasks += 1;
3109
+ if (event.event_type === "task_cancelled") summary.cancelled_tasks += 1;
3110
+ }
3111
+ return [...summaries.values()].sort((a, b) => (b.last_seen_at ?? 0) - (a.last_seen_at ?? 0));
3112
+ }
3113
+ function recommendationId(policy, remoteDid, action) {
3114
+ return `${policy}:${remoteDid}:${action}`;
3115
+ }
3116
+ function describePositiveSignals(summary) {
3117
+ const parts = [];
3118
+ if (summary.trusted_sessions > 0) parts.push(`trusted_sessions=${summary.trusted_sessions}`);
3119
+ if (summary.contact_approvals > 0) parts.push(`contact_approvals=${summary.contact_approvals}`);
3120
+ if (summary.processed_tasks > 0) parts.push(`processed_tasks=${summary.processed_tasks}`);
3121
+ return parts.join(", ");
3122
+ }
3123
+ function describeRiskSignals(summary) {
3124
+ const parts = [];
3125
+ if (summary.blocked_sessions > 0) parts.push(`blocked_sessions=${summary.blocked_sessions}`);
3126
+ if (summary.revoked_sessions > 0) parts.push(`revoked_sessions=${summary.revoked_sessions}`);
3127
+ if (summary.contact_rejections > 0) parts.push(`contact_rejections=${summary.contact_rejections}`);
3128
+ if (summary.task_denied > 0) parts.push(`task_denied=${summary.task_denied}`);
3129
+ if (summary.failed_tasks > 0) parts.push(`failed_tasks=${summary.failed_tasks}`);
3130
+ return parts.join(", ");
3131
+ }
3132
+ function buildTrustPolicyRecommendations(opts) {
3133
+ const policyDoc = normalizeTrustPolicyDoc(opts.policyDoc);
3134
+ const summaries = aggregateLearning(opts.sessions, opts.tasks, opts.auditEvents);
3135
+ const recommendations = [];
3136
+ for (const summary of summaries) {
3137
+ const contactDecision = decideContactPolicy(policyDoc, { sender_did: summary.remote_did });
3138
+ const taskDecision = decideTaskPolicy(policyDoc, { sender_did: summary.remote_did }, { runtimeMode: opts.runtimeMode });
3139
+ const riskSignals = summary.blocked_sessions + summary.revoked_sessions + summary.contact_rejections + summary.task_denied;
3140
+ const positiveSignals = summary.trusted_sessions + summary.contact_approvals + summary.processed_tasks;
3141
+ if (riskSignals > 0) {
3142
+ if (contactDecision.action !== "reject") {
3143
+ recommendations.push({
3144
+ id: recommendationId("contact", summary.remote_did, "reject"),
3145
+ policy: "contact",
3146
+ remote_did: summary.remote_did,
3147
+ match: summary.remote_did,
3148
+ action: "reject",
3149
+ current_action: contactDecision.action,
3150
+ confidence: riskSignals >= 2 ? "high" : "medium",
3151
+ reason: `Observed repeated negative signals: ${describeRiskSignals(summary)}`,
3152
+ signals: {
3153
+ trusted_sessions: summary.trusted_sessions,
3154
+ blocked_sessions: summary.blocked_sessions,
3155
+ revoked_sessions: summary.revoked_sessions,
3156
+ processed_tasks: summary.processed_tasks,
3157
+ failed_tasks: summary.failed_tasks,
3158
+ cancelled_tasks: summary.cancelled_tasks,
3159
+ contact_approvals: summary.contact_approvals,
3160
+ contact_rejections: summary.contact_rejections,
3161
+ task_denied: summary.task_denied,
3162
+ last_seen_at: summary.last_seen_at
3163
+ }
3164
+ });
3165
+ }
3166
+ if (taskDecision.action !== "deny") {
3167
+ recommendations.push({
3168
+ id: recommendationId("task", summary.remote_did, "deny"),
3169
+ policy: "task",
3170
+ remote_did: summary.remote_did,
3171
+ match: summary.remote_did,
3172
+ action: "deny",
3173
+ current_action: taskDecision.action,
3174
+ confidence: summary.task_denied > 0 || summary.blocked_sessions > 0 || summary.contact_rejections > 0 ? "high" : "medium",
3175
+ reason: `Observed repeated negative task/session signals: ${describeRiskSignals(summary)}`,
3176
+ signals: {
3177
+ trusted_sessions: summary.trusted_sessions,
3178
+ blocked_sessions: summary.blocked_sessions,
3179
+ revoked_sessions: summary.revoked_sessions,
3180
+ processed_tasks: summary.processed_tasks,
3181
+ failed_tasks: summary.failed_tasks,
3182
+ cancelled_tasks: summary.cancelled_tasks,
3183
+ contact_approvals: summary.contact_approvals,
3184
+ contact_rejections: summary.contact_rejections,
3185
+ task_denied: summary.task_denied,
3186
+ last_seen_at: summary.last_seen_at
3187
+ }
3188
+ });
3189
+ }
3190
+ continue;
3191
+ }
3192
+ if (positiveSignals > 0 && contactDecision.action !== "approve" && (summary.trusted_sessions > 0 || summary.contact_approvals > 0 || summary.processed_tasks >= 2)) {
3193
+ recommendations.push({
3194
+ id: recommendationId("contact", summary.remote_did, "approve"),
3195
+ policy: "contact",
3196
+ remote_did: summary.remote_did,
3197
+ match: summary.remote_did,
3198
+ action: "approve",
3199
+ current_action: contactDecision.action,
3200
+ confidence: summary.processed_tasks >= 2 || summary.contact_approvals > 0 ? "high" : "medium",
3201
+ reason: `Observed stable positive collaboration: ${describePositiveSignals(summary)}`,
3202
+ signals: {
3203
+ trusted_sessions: summary.trusted_sessions,
3204
+ blocked_sessions: summary.blocked_sessions,
3205
+ revoked_sessions: summary.revoked_sessions,
3206
+ processed_tasks: summary.processed_tasks,
3207
+ failed_tasks: summary.failed_tasks,
3208
+ cancelled_tasks: summary.cancelled_tasks,
3209
+ contact_approvals: summary.contact_approvals,
3210
+ contact_rejections: summary.contact_rejections,
3211
+ task_denied: summary.task_denied,
3212
+ last_seen_at: summary.last_seen_at
3213
+ }
3214
+ });
3215
+ }
3216
+ const desiredTaskAction = opts.runtimeMode === "executor" ? "execute" : "bridge";
3217
+ if (summary.processed_tasks >= 2 && summary.failed_tasks === 0 && summary.cancelled_tasks === 0 && summary.task_denied === 0 && taskDecision.action !== desiredTaskAction) {
3218
+ recommendations.push({
3219
+ id: recommendationId("task", summary.remote_did, desiredTaskAction),
3220
+ policy: "task",
3221
+ remote_did: summary.remote_did,
3222
+ match: summary.remote_did,
3223
+ action: desiredTaskAction,
3224
+ current_action: taskDecision.action,
3225
+ confidence: "high",
3226
+ reason: `Observed successful task flow without negative outcomes: processed_tasks=${summary.processed_tasks}`,
3227
+ signals: {
3228
+ trusted_sessions: summary.trusted_sessions,
3229
+ blocked_sessions: summary.blocked_sessions,
3230
+ revoked_sessions: summary.revoked_sessions,
3231
+ processed_tasks: summary.processed_tasks,
3232
+ failed_tasks: summary.failed_tasks,
3233
+ cancelled_tasks: summary.cancelled_tasks,
3234
+ contact_approvals: summary.contact_approvals,
3235
+ contact_rejections: summary.contact_rejections,
3236
+ task_denied: summary.task_denied,
3237
+ last_seen_at: summary.last_seen_at
3238
+ }
3239
+ });
3240
+ }
3241
+ }
3242
+ recommendations.sort((a, b) => {
3243
+ const confidenceScore = (value) => value === "high" ? 2 : 1;
3244
+ const delta = confidenceScore(b.confidence) - confidenceScore(a.confidence);
3245
+ if (delta !== 0) return delta;
3246
+ return (b.signals.last_seen_at ?? 0) - (a.signals.last_seen_at ?? 0);
3247
+ });
3248
+ return recommendations.slice(0, opts.limit ?? 20);
3249
+ }
3250
+ function upsertTrustPolicyRecommendation(policyDoc, recommendation) {
3251
+ const doc = normalizeTrustPolicyDoc(policyDoc);
3252
+ if (recommendation.policy === "contact") {
3253
+ return normalizeTrustPolicyDoc({
3254
+ ...doc,
3255
+ contact_policy: {
3256
+ ...doc.contact_policy,
3257
+ enabled: true,
3258
+ rules: [
3259
+ { match: recommendation.match, action: recommendation.action },
3260
+ ...doc.contact_policy.rules.filter((rule) => rule.match !== recommendation.match)
3261
+ ]
3262
+ }
3263
+ });
3264
+ }
3265
+ return normalizeTrustPolicyDoc({
3266
+ ...doc,
3267
+ task_policy: {
3268
+ ...doc.task_policy,
3269
+ enabled: true,
3270
+ rules: [
3271
+ { match: recommendation.match, action: recommendation.action },
3272
+ ...doc.task_policy.rules.filter((rule) => rule.match !== recommendation.match)
3273
+ ]
3274
+ }
3275
+ });
3276
+ }
3277
+ function summarizeTrustPolicyAudit(events) {
3278
+ return buildSummaryFromEvents(events);
3279
+ }
3280
+
3281
+ // src/trust-policy-recommendations.ts
3282
+ function getTrustRecommendationActionLabel(recommendation) {
3283
+ if (recommendation.status === "dismissed" || recommendation.status === "superseded") {
3284
+ return "Reopen";
3285
+ }
3286
+ if (recommendation.status === "applied") {
3287
+ return "Applied";
3288
+ }
3289
+ if (recommendation.policy === "contact") {
3290
+ if (recommendation.action === "approve") return "Approve + remember sender";
3291
+ if (recommendation.action === "manual") return "Keep contact manual";
3292
+ if (recommendation.action === "reject") return "Block this sender";
3293
+ }
3294
+ if (recommendation.policy === "task") {
3295
+ if (recommendation.action === "bridge") return "Keep tasks manual";
3296
+ if (recommendation.action === "execute") return "Allow tasks from this sender";
3297
+ if (recommendation.action === "deny") return "Block tasks from this sender";
3298
+ }
3299
+ return `${recommendation.policy}:${recommendation.action}`;
3300
+ }
3301
+ function rowToRecommendation(row) {
3302
+ return {
3303
+ id: row.id,
3304
+ policy: row.policy,
3305
+ remote_did: row.remote_did,
3306
+ match: row.match,
3307
+ action: row.action,
3308
+ current_action: row.current_action,
3309
+ confidence: row.confidence,
3310
+ reason: row.reason,
3311
+ signals: JSON.parse(row.signals_json),
3312
+ status: row.status,
3313
+ first_seen_at: row.first_seen_at,
3314
+ last_seen_at: row.last_seen_at,
3315
+ updated_at: row.updated_at,
3316
+ last_state_change_at: row.last_state_change_at ?? void 0,
3317
+ applied_at: row.applied_at ?? void 0,
3318
+ dismissed_at: row.dismissed_at ?? void 0
3319
+ };
3320
+ }
3321
+ var TrustRecommendationManager = class {
3322
+ constructor(store) {
3323
+ this.store = store;
3324
+ }
3325
+ upsertRecommendation(recommendation, now, opts) {
3326
+ const existing = this.get(recommendation.id);
3327
+ const status = opts?.status ?? (opts?.preserveState !== false ? existing?.status : void 0) ?? (recommendation.current_action === recommendation.action ? "applied" : "open");
3328
+ const lastStateChangeAt = existing?.status !== status ? now : existing?.last_state_change_at ?? now;
3329
+ const appliedAt = status === "applied" ? existing?.applied_at ?? now : void 0;
3330
+ const dismissedAt = status === "dismissed" ? existing?.dismissed_at ?? now : void 0;
3331
+ this.store.getDb().prepare(`
3332
+ INSERT INTO trust_policy_recommendations (
3333
+ id, policy, remote_did, match, action, current_action, confidence, reason, signals_json,
3334
+ status, first_seen_at, last_seen_at, updated_at, last_state_change_at, applied_at, dismissed_at
3335
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3336
+ ON CONFLICT(id) DO UPDATE SET
3337
+ policy = excluded.policy,
3338
+ remote_did = excluded.remote_did,
3339
+ match = excluded.match,
3340
+ action = excluded.action,
3341
+ current_action = excluded.current_action,
3342
+ confidence = excluded.confidence,
3343
+ reason = excluded.reason,
3344
+ signals_json = excluded.signals_json,
3345
+ status = excluded.status,
3346
+ last_seen_at = excluded.last_seen_at,
3347
+ updated_at = excluded.updated_at,
3348
+ last_state_change_at = excluded.last_state_change_at,
3349
+ applied_at = COALESCE(excluded.applied_at, trust_policy_recommendations.applied_at),
3350
+ dismissed_at = COALESCE(excluded.dismissed_at, trust_policy_recommendations.dismissed_at)
3351
+ `).run(
3352
+ recommendation.id,
3353
+ recommendation.policy,
3354
+ recommendation.remote_did,
3355
+ recommendation.match,
3356
+ recommendation.action,
3357
+ recommendation.current_action,
3358
+ recommendation.confidence,
3359
+ recommendation.reason,
3360
+ JSON.stringify(recommendation.signals),
3361
+ status,
3362
+ existing?.first_seen_at ?? now,
3363
+ now,
3364
+ now,
3365
+ lastStateChangeAt,
3366
+ appliedAt ?? null,
3367
+ dismissedAt ?? null
3368
+ );
3369
+ return this.get(recommendation.id);
3370
+ }
3371
+ sync(input) {
3372
+ const now = Date.now();
3373
+ const computed = buildTrustPolicyRecommendations({
3374
+ policyDoc: input.policyDoc,
3375
+ sessions: input.sessions,
3376
+ tasks: input.tasks,
3377
+ auditEvents: input.auditEvents,
3378
+ runtimeMode: input.runtimeMode,
3379
+ limit: input.limit ?? 50
3380
+ });
3381
+ const activeIds = /* @__PURE__ */ new Set();
3382
+ const synced = [];
3383
+ for (const recommendation of computed) {
3384
+ activeIds.add(recommendation.id);
3385
+ synced.push(this.upsertRecommendation(recommendation, now));
3386
+ }
3387
+ const openRows = this.store.getDb().prepare("SELECT id FROM trust_policy_recommendations WHERE status = ?").all("open");
3388
+ for (const row of openRows) {
3389
+ if (!activeIds.has(row.id)) {
3390
+ this.store.getDb().prepare("UPDATE trust_policy_recommendations SET status = ?, updated_at = ?, last_state_change_at = ? WHERE id = ?").run("superseded", now, now, row.id);
3391
+ }
3392
+ }
3393
+ return this.list({ limit: input.limit ?? 50 });
3394
+ }
3395
+ get(id) {
3396
+ const row = this.store.getDb().prepare("SELECT * FROM trust_policy_recommendations WHERE id = ?").get(id);
3397
+ return row ? rowToRecommendation(row) : null;
3398
+ }
3399
+ list(opts) {
3400
+ const conditions = [];
3401
+ const params = [];
3402
+ if (opts?.status) {
3403
+ const statuses = Array.isArray(opts.status) ? opts.status : [opts.status];
3404
+ conditions.push(`status IN (${statuses.map(() => "?").join(",")})`);
3405
+ params.push(...statuses);
3406
+ }
3407
+ if (opts?.remoteDid) {
3408
+ conditions.push("remote_did = ?");
3409
+ params.push(opts.remoteDid);
3410
+ }
3411
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3412
+ const rows = this.store.getDb().prepare(`SELECT * FROM trust_policy_recommendations ${where} ORDER BY updated_at DESC, last_seen_at DESC LIMIT ?`).all(...params, opts?.limit ?? 50);
3413
+ return rows.map(rowToRecommendation);
3414
+ }
3415
+ apply(id) {
3416
+ const existing = this.get(id);
3417
+ if (!existing) return null;
3418
+ const now = Date.now();
3419
+ this.store.getDb().prepare("UPDATE trust_policy_recommendations SET status = ?, updated_at = ?, last_state_change_at = ?, applied_at = ? WHERE id = ?").run("applied", now, now, now, id);
3420
+ return this.get(id);
3421
+ }
3422
+ dismiss(id) {
3423
+ const existing = this.get(id);
3424
+ if (!existing) return null;
3425
+ const now = Date.now();
3426
+ this.store.getDb().prepare("UPDATE trust_policy_recommendations SET status = ?, updated_at = ?, last_state_change_at = ?, dismissed_at = ? WHERE id = ?").run("dismissed", now, now, now, id);
3427
+ return this.get(id);
3428
+ }
3429
+ reopen(id) {
3430
+ const existing = this.get(id);
3431
+ if (!existing) return null;
3432
+ const now = Date.now();
3433
+ this.store.getDb().prepare("UPDATE trust_policy_recommendations SET status = ?, updated_at = ?, last_state_change_at = ? WHERE id = ?").run("open", now, now, id);
3434
+ return this.get(id);
3435
+ }
3436
+ summarize() {
3437
+ const rows = this.store.getDb().prepare("SELECT status, COUNT(*) AS count FROM trust_policy_recommendations GROUP BY status").all();
3438
+ const byStatus = {};
3439
+ let total = 0;
3440
+ for (const row of rows) {
3441
+ byStatus[row.status] = row.count;
3442
+ total += row.count;
3443
+ }
3444
+ return { total, by_status: byStatus };
3445
+ }
3446
+ };
3447
+
3448
+ // src/operator-seen-state.ts
3449
+ function normalizeScopeKey(scopeType, scopeKey) {
3450
+ if (scopeType === "global") return "";
3451
+ return String(scopeKey ?? "").trim();
3452
+ }
3453
+ function rowToState(row) {
3454
+ if (!row) return null;
3455
+ return {
3456
+ operator_id: row.operator_id,
3457
+ scope_type: row.scope_type,
3458
+ scope_key: row.scope_key || null,
3459
+ last_seen_ts: row.last_seen_ts,
3460
+ updated_at: row.updated_at
3461
+ };
3462
+ }
3463
+ var OperatorSeenStateManager = class {
3464
+ constructor(store) {
3465
+ this.store = store;
3466
+ }
3467
+ get(operatorId, scopeType, scopeKey) {
3468
+ const row = this.store.getDb().prepare(`
3469
+ SELECT operator_id, scope_type, scope_key, last_seen_ts, updated_at
3470
+ FROM operator_seen_state
3471
+ WHERE operator_id = ? AND scope_type = ? AND scope_key = ?
3472
+ `).get(operatorId, scopeType, normalizeScopeKey(scopeType, scopeKey));
3473
+ return rowToState(row);
3474
+ }
3475
+ markSeen(input) {
3476
+ const ts = Math.max(0, Number(input.last_seen_ts) || Date.now());
3477
+ const updatedAt = Date.now();
3478
+ const normalizedScopeKey = normalizeScopeKey(input.scope_type, input.scope_key);
3479
+ this.store.getDb().prepare(`
3480
+ INSERT INTO operator_seen_state (
3481
+ operator_id,
3482
+ scope_type,
3483
+ scope_key,
3484
+ last_seen_ts,
3485
+ updated_at
3486
+ ) VALUES (?, ?, ?, ?, ?)
3487
+ ON CONFLICT(operator_id, scope_type, scope_key)
3488
+ DO UPDATE SET
3489
+ last_seen_ts = excluded.last_seen_ts,
3490
+ updated_at = excluded.updated_at
3491
+ `).run(
3492
+ input.operator_id,
3493
+ input.scope_type,
3494
+ normalizedScopeKey,
3495
+ ts,
3496
+ updatedAt
3497
+ );
3498
+ return {
3499
+ operator_id: input.operator_id,
3500
+ scope_type: input.scope_type,
3501
+ scope_key: normalizedScopeKey || null,
3502
+ last_seen_ts: ts,
3503
+ updated_at: updatedAt
3504
+ };
3505
+ }
3506
+ listByOperator(operatorId) {
3507
+ const rows = this.store.getDb().prepare(`
3508
+ SELECT operator_id, scope_type, scope_key, last_seen_ts, updated_at
3509
+ FROM operator_seen_state
3510
+ WHERE operator_id = ?
3511
+ ORDER BY updated_at DESC, scope_type ASC, scope_key ASC
3512
+ `).all(operatorId);
3513
+ return rows.map((row) => rowToState(row)).filter(Boolean);
3514
+ }
3515
+ };
3516
+
3517
+ // src/projection-disposition.ts
3518
+ var ALWAYS_PROJECT = /* @__PURE__ */ new Set([
3519
+ "decision_required",
3520
+ "runtime_degraded",
3521
+ "runtime_repaired",
3522
+ "transport_switch_recommended",
3523
+ "transport_switched"
3524
+ ]);
3525
+ var BALANCED_PROJECT = /* @__PURE__ */ new Set([
3526
+ "agent_conclusion",
3527
+ "handoff_started",
3528
+ "handoff_completed",
3529
+ "task_completed"
3530
+ ]);
3531
+ var STRICT_ONLY_PROJECT = /* @__PURE__ */ new Set([
3532
+ "risk_detected",
3533
+ "external_message_received"
3534
+ ]);
3535
+ function getProjectedEventTypes(preset) {
3536
+ const result = new Set(ALWAYS_PROJECT);
3537
+ if (preset === "balanced" || preset === "strict") {
3538
+ for (const eventType of BALANCED_PROJECT) result.add(eventType);
3539
+ }
3540
+ if (preset === "strict") {
3541
+ for (const eventType of STRICT_ONLY_PROJECT) result.add(eventType);
3542
+ }
3543
+ return Array.from(result.values());
3544
+ }
3545
+ function describeProjectionPreset(preset) {
3546
+ const immediate = new Set(getProjectedEventTypes(preset));
3547
+ const all = [
3548
+ "external_message_received",
3549
+ "agent_progress",
3550
+ "agent_conclusion",
3551
+ "handoff_started",
3552
+ "handoff_completed",
3553
+ "risk_detected",
3554
+ "decision_required",
3555
+ "task_completed",
3556
+ "runtime_degraded",
3557
+ "runtime_repaired",
3558
+ "transport_switch_recommended",
3559
+ "transport_switched"
3560
+ ];
3561
+ return {
3562
+ immediate_event_types: all.filter((eventType) => immediate.has(eventType)),
3563
+ detail_only_event_types: all.filter((eventType) => !immediate.has(eventType))
3564
+ };
3565
+ }
3566
+ function getProjectionDisposition(input) {
3567
+ const projectionMode = input.projection_mode ?? "update";
3568
+ if (projectionMode === "decision") {
3569
+ return {
3570
+ disposition: "project",
3571
+ reason: "decision projection always reaches the human thread"
3572
+ };
3573
+ }
3574
+ if (projectionMode === "runtime") {
3575
+ return {
3576
+ disposition: "project",
3577
+ reason: "runtime state projection always reaches the human thread"
3578
+ };
3579
+ }
3580
+ const eventType = input.event_type ?? null;
3581
+ if (eventType && ALWAYS_PROJECT.has(eventType)) {
3582
+ return {
3583
+ disposition: "project",
3584
+ reason: `${eventType} is always projected`
3585
+ };
3586
+ }
3587
+ if (input.has_pending_decision) {
3588
+ return {
3589
+ disposition: "decision_gate",
3590
+ reason: "pending decision gates ordinary updates"
3591
+ };
3592
+ }
3593
+ const immediate = new Set(getProjectedEventTypes(input.preset));
3594
+ if (eventType && immediate.has(eventType)) {
3595
+ return {
3596
+ disposition: "project",
3597
+ reason: `${eventType} is projected under preset=${input.preset}`
3598
+ };
3599
+ }
3600
+ return {
3601
+ disposition: "detail_only",
3602
+ reason: eventType ? `${eventType} remains detail-only under preset=${input.preset}` : `update remains detail-only under preset=${input.preset}`
3603
+ };
3604
+ }
3605
+
3606
+ // src/collaboration-insights.ts
3607
+ function latestTimestamp(values) {
3608
+ const latest = values.reduce((max, value) => Math.max(max, value), 0);
3609
+ return latest > 0 ? latest : void 0;
3610
+ }
3611
+ function previewFromPayload2(payload) {
3612
+ if (payload == null) return "";
3613
+ if (typeof payload === "string") return payload;
3614
+ if (typeof payload !== "object") return String(payload);
3615
+ const record = payload;
3616
+ return String(record.text ?? record.title ?? record.summary ?? "").trim();
3617
+ }
3618
+ function buildDecisionView(event, overdueThresholdMs) {
3619
+ const overdueByMs = Date.now() - event.ts_ms - overdueThresholdMs;
3620
+ return {
3621
+ ...event,
3622
+ overdue: overdueByMs >= 0,
3623
+ overdue_by_ms: overdueByMs >= 0 ? overdueByMs : void 0
3624
+ };
3625
+ }
3626
+ function listPendingDecisionViews(store, limit = 100, overdueThresholdMs = 15 * 60 * 1e3) {
3627
+ const events = new CollaborationEventManager(store).listPending(limit);
3628
+ return events.map((event) => buildDecisionView(event, overdueThresholdMs)).sort((a, b) => {
3629
+ if (a.overdue !== b.overdue) return a.overdue ? -1 : 1;
3630
+ const severityRank = { critical: 3, warning: 2, notice: 1, info: 0 };
3631
+ return (severityRank[b.severity] ?? 0) - (severityRank[a.severity] ?? 0) || b.ts_ms - a.ts_ms || b.id - a.id;
3632
+ });
3633
+ }
3634
+ function summarizeSinceLastSeen(store, input) {
3635
+ const seenState = new OperatorSeenStateManager(store).get(
3636
+ input.operator_id,
3637
+ input.scope_type,
3638
+ input.scope_key
3639
+ );
3640
+ const lastSeenTs = seenState?.last_seen_ts ?? 0;
3641
+ const db = store.getDb();
3642
+ const sessionFilter = input.scope_type === "session" ? "AND session_key = ?" : "";
3643
+ const params = input.scope_type === "session" ? [lastSeenTs, String(input.scope_key ?? "").trim()] : [lastSeenTs];
3644
+ const rows = db.prepare(`
3645
+ SELECT ts_ms, event_type, approval_required
3646
+ FROM collaboration_events
3647
+ WHERE ts_ms > ?
3648
+ ${sessionFilter}
3649
+ ORDER BY ts_ms DESC, id DESC
3650
+ `).all(...params);
3651
+ const outboxRows = db.prepare(`
3652
+ SELECT created_at
3653
+ FROM collaboration_projection_outbox
3654
+ WHERE created_at > ?
3655
+ AND status = 'failed'
3656
+ ${input.scope_type === "session" ? "AND session_key = ?" : ""}
3657
+ `).all(...params);
3658
+ const latestTs = latestTimestamp([
3659
+ ...rows.map((row) => row.ts_ms),
3660
+ ...outboxRows.map((row) => row.created_at)
3661
+ ]);
3662
+ return {
3663
+ operator_id: input.operator_id,
3664
+ scope_type: input.scope_type,
3665
+ scope_key: input.scope_type === "session" ? String(input.scope_key ?? "").trim() || null : null,
3666
+ last_seen_ts: lastSeenTs || void 0,
3667
+ new_external_messages: rows.filter((row) => row.event_type === "external_message_received").length,
3668
+ new_conclusions: rows.filter((row) => row.event_type === "agent_conclusion").length,
3669
+ new_handoffs: rows.filter((row) => row.event_type === "handoff_started" || row.event_type === "handoff_completed").length,
3670
+ new_decisions: rows.filter((row) => row.event_type === "decision_required" || row.approval_required === 1).length,
3671
+ new_failures: rows.filter((row) => row.event_type === "runtime_degraded" || row.event_type === "risk_detected").length,
3672
+ new_repairs: rows.filter((row) => row.event_type === "runtime_repaired").length,
3673
+ new_projection_failures: outboxRows.length,
3674
+ latest_ts: latestTs
3675
+ };
3676
+ }
3677
+ function buildDeliveryTimeline(store, sessionKey, limit = 40) {
3678
+ const session = new SessionManager(store).get(sessionKey);
3679
+ const historyManager = new HistoryManager(store);
3680
+ const eventManager = new CollaborationEventManager(store);
3681
+ const taskThreadManager = new TaskThreadManager(store);
3682
+ const taskHandoffManager = new TaskHandoffManager(store);
3683
+ const outboxManager = new CollaborationProjectionOutboxManager(store);
3684
+ const entries = [];
3685
+ if (session?.conversation_id) {
3686
+ for (const message of historyManager.listRecent(session.conversation_id, limit)) {
3687
+ const preview = previewFromPayload2(message.payload);
3688
+ entries.push({
3689
+ ts_ms: message.ts_ms,
3690
+ kind: message.direction === "received" ? "inbound_message" : "outbound_message",
3691
+ summary: preview || `${message.direction} ${message.schema}`,
3692
+ session_key: sessionKey,
3693
+ conversation_id: session.conversation_id,
3694
+ status: message.schema,
3695
+ detail: {
3696
+ message_id: message.message_id,
3697
+ sender_did: message.sender_did,
3698
+ schema: message.schema
3699
+ }
3700
+ });
3701
+ }
3702
+ }
3703
+ for (const event of eventManager.listBySession(sessionKey, limit)) {
3704
+ entries.push({
3705
+ ts_ms: event.ts_ms,
3706
+ kind: `event:${event.event_type}`,
3707
+ summary: event.summary,
3708
+ session_key: sessionKey,
3709
+ conversation_id: event.conversation_id ?? null,
3710
+ status: event.approval_required ? event.approval_status : event.severity,
3711
+ detail: event.detail ?? null
3712
+ });
3713
+ }
3714
+ for (const task of taskThreadManager.listBySession(sessionKey, limit)) {
3715
+ const handoff = taskHandoffManager.get(task.task_id);
3716
+ entries.push({
3717
+ ts_ms: task.updated_at,
3718
+ kind: "task_status",
3719
+ summary: `${task.title || task.task_id} [${task.status}]`,
3720
+ session_key: sessionKey,
3721
+ conversation_id: task.conversation_id,
3722
+ status: task.status,
3723
+ detail: {
3724
+ task_id: task.task_id,
3725
+ handoff: handoff ?? null,
3726
+ result_summary: task.result_summary ?? null,
3727
+ error_code: task.error_code ?? null
3728
+ }
3729
+ });
3730
+ }
3731
+ for (const row of outboxManager.listBySession(sessionKey, limit)) {
3732
+ entries.push({
3733
+ ts_ms: row.delivered_at ?? row.created_at,
3734
+ kind: `projection:${row.status}`,
3735
+ summary: row.summary,
3736
+ session_key: sessionKey,
3737
+ conversation_id: row.conversation_id ?? null,
3738
+ status: row.status,
3739
+ detail: {
3740
+ projection_kind: row.projection_kind,
3741
+ target_human_session: row.target_human_session ?? null,
3742
+ last_error: row.last_error ?? null,
3743
+ attempt_count: row.attempt_count
3744
+ }
3745
+ });
3746
+ }
3747
+ return entries.sort((a, b) => b.ts_ms - a.ts_ms || a.kind.localeCompare(b.kind)).slice(0, limit);
3748
+ }
3749
+ function buildProjectionPreview(store, sessionKey, preset, limit = 5) {
3750
+ const eventManager = new CollaborationEventManager(store);
3751
+ const recentEvents = eventManager.listBySession(sessionKey, Math.max(limit * 3, limit));
3752
+ const hasPendingDecision = eventManager.listPendingBySession(sessionKey, 1).length > 0;
3753
+ return {
3754
+ preset,
3755
+ rules: describeProjectionPreset(preset),
3756
+ recent: recentEvents.slice(0, limit).map((event) => {
3757
+ const decision = getProjectionDisposition({
3758
+ preset,
3759
+ event_type: event.event_type,
3760
+ has_pending_decision: hasPendingDecision && !(event.approval_required && event.approval_status === "pending"),
3761
+ projection_mode: "update"
3762
+ });
3763
+ return {
3764
+ event_id: event.id,
3765
+ ts_ms: event.ts_ms,
3766
+ event_type: event.event_type,
3767
+ summary: event.summary,
3768
+ disposition: decision.disposition,
3769
+ reason: decision.reason
3770
+ };
3771
+ })
3772
+ };
3773
+ }
3774
+ function buildDecisionReminderOutboxMessage(event) {
3775
+ return [
3776
+ "[CollaborationDecision]",
3777
+ `summary=Decision overdue: ${event.summary}`,
3778
+ "next=A pending collaboration decision still needs review before the human thread can be considered current.",
3779
+ `detail_ref_session=${event.session_key ?? "(none)"}`,
3780
+ `detail_ref_conversation=${event.conversation_id ?? "(none)"}`,
3781
+ "detail_view=Open the collaboration detail in Host Panel, TUI, or MCP to inspect raw messages, handoffs, audit, and runtime changes."
3782
+ ].join("\n");
3783
+ }
3784
+ function hasDecisionPromptOutbox(outboxManager, sourceEventId) {
3785
+ return outboxManager.listRecent(200).some((row) => row.source_event_id === sourceEventId && row.projection_kind === "decision_prompt");
3786
+ }
3787
+
3788
+ // src/transport-preferences.ts
3789
+ import * as fs3 from "fs";
3790
+ import * as os2 from "os";
3791
+ import * as path4 from "path";
3792
+ import { spawnSync } from "child_process";
3793
+ function nowIso() {
3794
+ return (/* @__PURE__ */ new Date()).toISOString();
3795
+ }
3796
+ function resolvePath(p) {
3797
+ if (!p) return null;
3798
+ if (p.startsWith("~")) return path4.join(os2.homedir(), p.slice(1));
3799
+ return path4.resolve(p);
3800
+ }
3801
+ function normalizeTransportMode(value, fallback = "bridge") {
3802
+ return String(value ?? "").trim().toLowerCase() === "channel" ? "channel" : fallback;
3803
+ }
3804
+ function getDefaultTransportPreferencePath() {
3805
+ return path4.join(os2.homedir(), ".openclaw", "pingagent-im-ingress", "transport-preference.json");
3806
+ }
3807
+ function getTransportPreferenceFilePath() {
3808
+ return resolvePath(process.env.OPENCLAW_TRANSPORT_PREFERENCE_FILE?.trim()) || resolvePath(process.env.PINGAGENT_TRANSPORT_PREFERENCE_FILE?.trim()) || getDefaultTransportPreferencePath();
3809
+ }
3810
+ function readTransportPreference(filePath = getTransportPreferenceFilePath()) {
3811
+ try {
3812
+ if (!fs3.existsSync(filePath)) return null;
3813
+ const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
3814
+ return {
3815
+ preferred_mode: normalizeTransportMode(raw?.preferred_mode, "bridge"),
3816
+ updated_at: raw?.updated_at ? String(raw.updated_at) : nowIso(),
3817
+ updated_by: raw?.updated_by ? String(raw.updated_by) : "unknown",
3818
+ last_auto_switch_at: raw?.last_auto_switch_at ? String(raw.last_auto_switch_at) : null,
3819
+ last_auto_switch_reason: raw?.last_auto_switch_reason ? String(raw.last_auto_switch_reason) : null
3820
+ };
3821
+ } catch {
3822
+ return null;
3823
+ }
3824
+ }
3825
+ function writeTransportPreference(input, filePath = getTransportPreferenceFilePath()) {
3826
+ const next = {
3827
+ preferred_mode: normalizeTransportMode(input.preferred_mode, "bridge"),
3828
+ updated_at: nowIso(),
3829
+ updated_by: String(input.updated_by ?? "unknown").trim() || "unknown",
3830
+ last_auto_switch_at: input.last_auto_switch_at ? String(input.last_auto_switch_at) : null,
3831
+ last_auto_switch_reason: input.last_auto_switch_reason ? String(input.last_auto_switch_reason) : null
3832
+ };
3833
+ fs3.mkdirSync(path4.dirname(filePath), { recursive: true, mode: 448 });
3834
+ fs3.writeFileSync(filePath, JSON.stringify(next, null, 2), "utf-8");
3835
+ return next;
3836
+ }
3837
+ function tryRestartWithCommand(command) {
3838
+ const result = spawnSync(command, {
3839
+ shell: true,
3840
+ encoding: "utf-8",
3841
+ env: process.env
3842
+ });
3843
+ if ((result.status ?? 1) === 0) {
3844
+ return { restarted: true, method: "custom_command" };
3845
+ }
3846
+ return {
3847
+ restarted: false,
3848
+ method: "custom_command",
3849
+ error: String(result.stderr ?? result.stdout ?? "").trim() || `exit_${result.status ?? 1}`
3850
+ };
3851
+ }
3852
+ function tryRestartSystemdService(serviceName) {
3853
+ const restart = spawnSync("systemctl", ["--user", "restart", `${serviceName}.service`], {
3854
+ encoding: "utf-8",
3855
+ env: process.env
3856
+ });
3857
+ if ((restart.status ?? 1) === 0) {
3858
+ return { restarted: true, method: `systemd:${serviceName}` };
3859
+ }
3860
+ return {
3861
+ restarted: false,
3862
+ method: `systemd:${serviceName}`,
3863
+ error: String(restart.stderr ?? restart.stdout ?? "").trim() || `exit_${restart.status ?? 1}`
3864
+ };
3865
+ }
3866
+ function tryRestartLaunchAgent(label) {
3867
+ const uid = typeof process.getuid === "function" ? String(process.getuid()) : null;
3868
+ if (!uid) return { restarted: false, method: `launchd:${label}`, error: "missing_uid" };
3869
+ const restart = spawnSync("launchctl", ["kickstart", "-k", `gui/${uid}/${label}`], {
3870
+ encoding: "utf-8",
3871
+ env: process.env
3872
+ });
3873
+ if ((restart.status ?? 1) === 0) {
3874
+ return { restarted: true, method: `launchd:${label}` };
3875
+ }
3876
+ return {
3877
+ restarted: false,
3878
+ method: `launchd:${label}`,
3879
+ error: String(restart.stderr ?? restart.stdout ?? "").trim() || `exit_${restart.status ?? 1}`
3880
+ };
3881
+ }
3882
+ function tryManagedRestart() {
3883
+ const explicit = String(process.env.PINGAGENT_TRANSPORT_SWITCH_RESTART_CMD ?? "").trim();
3884
+ if (explicit) return tryRestartWithCommand(explicit);
3885
+ if (process.platform === "linux") {
3886
+ const restart = tryRestartSystemdService("pingagent-im-ingress");
3887
+ if (restart.restarted) return restart;
3888
+ return { restarted: false, method: restart.method ?? null, error: restart.error ?? null };
3889
+ }
3890
+ if (process.platform === "darwin") {
3891
+ const restart = tryRestartLaunchAgent("com.pingagent.pingagent-im-ingress");
3892
+ if (restart.restarted) return restart;
3893
+ return { restarted: false, method: restart.method ?? null, error: restart.error ?? null };
3894
+ }
3895
+ return {
3896
+ restarted: false,
3897
+ method: null,
3898
+ error: "managed_restart_unavailable"
3899
+ };
3900
+ }
3901
+ function switchTransportPreference(preferredMode, opts) {
3902
+ const preferencePath = opts.filePath ?? getTransportPreferenceFilePath();
3903
+ const timestamp = nowIso();
3904
+ const saved = writeTransportPreference({
3905
+ preferred_mode: normalizeTransportMode(preferredMode, "bridge"),
3906
+ updated_by: opts.updated_by,
3907
+ last_auto_switch_at: opts.auto_switch_reason ? timestamp : null,
3908
+ last_auto_switch_reason: opts.auto_switch_reason ?? null
3909
+ }, preferencePath);
3910
+ const restart = opts.attempt_restart === false ? { restarted: false, method: null, error: null } : tryManagedRestart();
3911
+ return {
3912
+ preferred_mode: saved.preferred_mode,
3913
+ preference_path: preferencePath,
3914
+ updated_at: saved.updated_at,
3915
+ updated_by: saved.updated_by,
3916
+ last_auto_switch_at: saved.last_auto_switch_at ?? null,
3917
+ last_auto_switch_reason: saved.last_auto_switch_reason ?? null,
3918
+ restarted: restart.restarted,
3919
+ restart_required: !restart.restarted,
3920
+ restart_method: restart.method ?? null,
3921
+ restart_error: restart.error ?? null
3922
+ };
3923
+ }
3924
+
3925
+ // src/transport-health.ts
3926
+ function asTimestamp(value) {
3927
+ if (!value) return 0;
3928
+ const parsed = Date.parse(String(value));
3929
+ return Number.isFinite(parsed) ? parsed : 0;
3930
+ }
3931
+ function pickLatestTransportEventTimestamp(events, eventType) {
3932
+ return events.filter((event) => event.event_type === eventType).reduce((latest, event) => Math.max(latest, event.ts_ms), 0);
3933
+ }
3934
+ function deriveTransportHealth(input) {
3935
+ const runtimeStatus = input.runtime_status ?? null;
3936
+ const recentEvents = input.recent_events ?? [];
3937
+ const failedOutbox = input.projection_outbox_failed ?? [];
3938
+ const transportMode = runtimeStatus?.transport_mode === "channel" ? "channel" : "bridge";
3939
+ const preferredMode = runtimeStatus?.preferred_transport_mode === "channel" ? "channel" : "bridge";
3940
+ const recommended = runtimeStatus?.transport_switch_recommended === true || pickLatestTransportEventTimestamp(recentEvents, "transport_switch_recommended") > pickLatestTransportEventTimestamp(recentEvents, "transport_switched");
3941
+ const degradedAt = Math.max(
3942
+ asTimestamp(runtimeStatus?.channel_last_degraded_at),
3943
+ pickLatestTransportEventTimestamp(recentEvents, "runtime_degraded")
3944
+ );
3945
+ const repairedAt = Math.max(
3946
+ asTimestamp(runtimeStatus?.channel_last_repaired_at),
3947
+ pickLatestTransportEventTimestamp(recentEvents, "runtime_repaired")
3948
+ );
3949
+ const retryQueueLength = Math.max(0, Number(runtimeStatus?.channel_retry_queue_length ?? 0) || 0);
3950
+ const consecutiveFailures = Math.max(0, Number(runtimeStatus?.channel_consecutive_failures ?? 0) || 0);
3951
+ const lastError = runtimeStatus?.channel_last_error ?? runtimeStatus?.hooks_last_error ?? failedOutbox[0]?.last_error ?? null;
3952
+ let state = "Ready";
3953
+ if (recommended) state = "Switching Recommended";
3954
+ else if (transportMode === "channel" ? retryQueueLength > 0 || degradedAt > repairedAt || consecutiveFailures > 0 || !!lastError : runtimeStatus?.receive_mode === "polling_degraded" || failedOutbox.length > 0 || degradedAt > repairedAt) {
3955
+ state = "Degraded";
3956
+ }
3957
+ return {
3958
+ transport_mode: transportMode,
3959
+ preferred_transport_mode: preferredMode,
3960
+ state,
3961
+ transport_switch_recommended: recommended,
3962
+ transport_switch_reason: runtimeStatus?.transport_switch_reason ?? null,
3963
+ retry_queue_length: retryQueueLength,
3964
+ last_inbound_at: runtimeStatus?.channel_last_inbound_at ?? null,
3965
+ last_outbound_at: runtimeStatus?.channel_last_outbound_at ?? null,
3966
+ last_degraded_at: degradedAt > 0 ? new Date(degradedAt).toISOString() : null,
3967
+ last_repaired_at: repairedAt > 0 ? new Date(repairedAt).toISOString() : null,
3968
+ last_error: lastError,
3969
+ consecutive_failures: consecutiveFailures,
3970
+ receive_mode: runtimeStatus?.receive_mode ?? null
3971
+ };
3972
+ }
3973
+
3974
+ // src/a2a-adapter.ts
3975
+ import {
3976
+ A2AClient
3977
+ } from "@pingagent/a2a";
3978
+ var A2AAdapter = class {
3979
+ client;
3980
+ cachedCard = null;
3981
+ constructor(opts) {
3982
+ this.client = new A2AClient({
3983
+ agentUrl: opts.agentUrl,
3984
+ authToken: opts.authToken ? `Bearer ${opts.authToken}` : void 0,
3985
+ timeoutMs: opts.timeoutMs
3986
+ });
3987
+ }
3988
+ async getAgentCard() {
3989
+ if (!this.cachedCard) {
3990
+ this.cachedCard = await this.client.fetchAgentCard();
3991
+ }
3992
+ return this.cachedCard;
3993
+ }
3994
+ /**
3995
+ * Send a task to the external A2A agent and optionally wait for completion.
3996
+ */
3997
+ async sendTask(opts) {
3998
+ const parts = [];
3999
+ parts.push({ kind: "text", text: opts.title });
4000
+ if (opts.description) {
4001
+ parts.push({ kind: "text", text: opts.description });
4002
+ }
4003
+ if (opts.input) {
4004
+ parts.push({
4005
+ kind: "data",
4006
+ data: typeof opts.input === "object" ? opts.input : { value: opts.input }
4007
+ });
4008
+ }
4009
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4010
+ const message = {
4011
+ kind: "message",
4012
+ messageId,
4013
+ role: "user",
4014
+ parts
4015
+ };
4016
+ if (opts.wait) {
4017
+ const task = await this.client.sendAndWait(
4018
+ { message, configuration: { blocking: true } },
4019
+ { maxPollMs: opts.timeoutMs ?? 12e4 }
4020
+ );
4021
+ return this.convertTask(task);
4022
+ }
4023
+ const result = await this.client.sendMessage({ message });
4024
+ if (result.kind === "task") {
4025
+ return this.convertTask(result);
4026
+ }
4027
+ return {
4028
+ taskId: result.messageId,
4029
+ state: "completed",
4030
+ summary: this.extractText(result.parts)
4031
+ };
4032
+ }
4033
+ async getTaskStatus(taskId) {
4034
+ const task = await this.client.getTask(taskId);
4035
+ return this.convertTask(task);
4036
+ }
4037
+ async cancelTask(taskId) {
4038
+ const task = await this.client.cancelTask(taskId);
4039
+ return this.convertTask(task);
4040
+ }
4041
+ async sendText(text, opts) {
4042
+ const result = await this.client.sendText(text, { blocking: opts?.blocking });
4043
+ if (result.kind === "task") {
4044
+ return this.convertTask(result);
4045
+ }
4046
+ return {
4047
+ taskId: result.messageId,
4048
+ state: "completed",
4049
+ summary: this.extractText(result.parts)
4050
+ };
4051
+ }
4052
+ convertTask(task) {
4053
+ const summary = task.artifacts?.flatMap((a) => a.parts).filter((p) => p.kind === "text").map((p) => p.text).join("\n");
4054
+ const output = task.artifacts?.flatMap((a) => a.parts).filter((p) => p.kind === "data").map((p) => p.data);
4055
+ return {
4056
+ taskId: task.id,
4057
+ contextId: task.contextId,
4058
+ state: task.status.state,
4059
+ summary: summary || void 0,
4060
+ output: output && output.length > 0 ? output.length === 1 ? output[0] : output : void 0,
4061
+ timestamp: task.status.timestamp
4062
+ };
4063
+ }
4064
+ extractText(parts) {
4065
+ return parts.filter((p) => p.kind === "text").map((p) => p.text).join("\n") || void 0;
4066
+ }
4067
+ };
4068
+
4069
+ // src/ws-subscription.ts
4070
+ import WebSocket from "ws";
4071
+ var RECONNECT_BASE_MS = 1e3;
4072
+ var RECONNECT_MAX_MS = 3e4;
4073
+ var RECONNECT_JITTER = 0.2;
4074
+ var LIST_CONVERSATIONS_INTERVAL_MS = 6e4;
4075
+ var DEFAULT_HEARTBEAT = {
4076
+ enable: true,
4077
+ idleThresholdMs: 1e4,
4078
+ pingIntervalMs: 15e3,
4079
+ pongTimeoutMs: 1e4,
4080
+ maxMissedPongs: 2,
4081
+ tickMs: 5e3,
4082
+ jitter: 0.2
4083
+ };
4084
+ var WsSubscription = class {
4085
+ opts;
4086
+ connections = /* @__PURE__ */ new Map();
4087
+ reconnectTimers = /* @__PURE__ */ new Map();
4088
+ reconnectAttempts = /* @__PURE__ */ new Map();
4089
+ listInterval = null;
4090
+ stopped = false;
4091
+ /** Conversation IDs that were explicitly stopped (e.g. revoke); do not reconnect. */
4092
+ stoppedConversations = /* @__PURE__ */ new Set();
4093
+ lastParseErrorAtByConv = /* @__PURE__ */ new Map();
4094
+ lastFrameAtByConv = /* @__PURE__ */ new Map();
4095
+ heartbeatTimer = null;
4096
+ heartbeatStates = /* @__PURE__ */ new Map();
4097
+ constructor(opts) {
4098
+ this.opts = opts;
4099
+ }
4100
+ start() {
4101
+ this.stopped = false;
4102
+ this.connectAll();
4103
+ const listIntervalMs = this.opts.listIntervalMs ?? LIST_CONVERSATIONS_INTERVAL_MS;
4104
+ this.listInterval = setInterval(() => void this.syncConnections(), listIntervalMs);
4105
+ const hb = this.opts.heartbeat ?? {};
4106
+ if (hb.enable ?? DEFAULT_HEARTBEAT.enable) {
4107
+ this.heartbeatTimer = setInterval(() => this.heartbeatTick(), hb.tickMs ?? DEFAULT_HEARTBEAT.tickMs);
4108
+ }
4109
+ }
4110
+ stop() {
4111
+ this.stopped = true;
4112
+ if (this.listInterval) {
4113
+ clearInterval(this.listInterval);
4114
+ this.listInterval = null;
4115
+ }
4116
+ if (this.heartbeatTimer) {
4117
+ clearInterval(this.heartbeatTimer);
4118
+ this.heartbeatTimer = null;
4119
+ }
4120
+ for (const timer of this.reconnectTimers.values()) {
4121
+ clearTimeout(timer);
4122
+ }
4123
+ this.reconnectTimers.clear();
4124
+ for (const [convId, ws] of this.connections) {
4125
+ ws.removeAllListeners();
4126
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4127
+ ws.close();
4128
+ }
4129
+ this.connections.delete(convId);
4130
+ }
4131
+ this.reconnectAttempts.clear();
4132
+ this.stoppedConversations.clear();
4133
+ this.lastParseErrorAtByConv.clear();
4134
+ this.lastFrameAtByConv.clear();
4135
+ for (const state of this.heartbeatStates.values()) {
4136
+ if (state.pongTimeout) clearTimeout(state.pongTimeout);
4137
+ }
4138
+ this.heartbeatStates.clear();
4139
+ }
4140
+ async syncNow() {
4141
+ await this.syncConnections();
4142
+ }
4143
+ /**
4144
+ * Stop a single conversation's WebSocket and do not reconnect.
4145
+ * Used when the conversation is revoked or the client no longer wants to subscribe.
4146
+ */
4147
+ stopConversation(conversationId) {
4148
+ this.stoppedConversations.add(conversationId);
4149
+ const timer = this.reconnectTimers.get(conversationId);
4150
+ if (timer) {
4151
+ clearTimeout(timer);
4152
+ this.reconnectTimers.delete(conversationId);
4153
+ }
4154
+ const hbState = this.heartbeatStates.get(conversationId);
4155
+ if (hbState?.pongTimeout) clearTimeout(hbState.pongTimeout);
4156
+ this.heartbeatStates.delete(conversationId);
4157
+ const ws = this.connections.get(conversationId);
4158
+ if (ws) {
4159
+ ws.removeAllListeners();
4160
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
4161
+ ws.close();
4162
+ }
4163
+ this.connections.delete(conversationId);
4164
+ }
4165
+ this.lastParseErrorAtByConv.delete(conversationId);
4166
+ this.lastFrameAtByConv.delete(conversationId);
4167
+ }
4168
+ wsUrl(conversationId) {
4169
+ const base = this.opts.serverUrl.replace(/^http/, "ws").replace(/\/$/, "");
4170
+ return `${base}/v1/ws?conversation_id=${encodeURIComponent(conversationId)}`;
4171
+ }
4172
+ async connectAsync(conversationId) {
4173
+ if (this.stopped || this.stoppedConversations.has(conversationId) || this.connections.has(conversationId)) return;
4174
+ const token = await Promise.resolve(this.opts.getAccessToken());
4175
+ const url = this.wsUrl(conversationId);
4176
+ const ws = new WebSocket(url, {
4177
+ headers: { Authorization: `Bearer ${token}` }
4178
+ });
4179
+ this.connections.set(conversationId, ws);
4180
+ const hb = this.opts.heartbeat ?? {};
4181
+ const hbEnabled = hb.enable ?? DEFAULT_HEARTBEAT.enable;
4182
+ if (hbEnabled) {
4183
+ const now = Date.now();
4184
+ this.heartbeatStates.set(conversationId, {
4185
+ lastRxAt: now,
4186
+ lastPingAt: 0,
4187
+ nextPingAt: now + (hb.idleThresholdMs ?? DEFAULT_HEARTBEAT.idleThresholdMs),
4188
+ missedPongs: 0,
4189
+ pongTimeout: null
4190
+ });
4191
+ }
4192
+ ws.on("open", () => {
4193
+ this.reconnectAttempts.set(conversationId, 0);
4194
+ this.opts.onOpen?.(conversationId);
4195
+ });
4196
+ ws.on("message", (data) => {
4197
+ try {
4198
+ const state = this.heartbeatStates.get(conversationId);
4199
+ if (state) {
4200
+ state.lastRxAt = Date.now();
4201
+ state.missedPongs = 0;
4202
+ if (state.pongTimeout) {
4203
+ clearTimeout(state.pongTimeout);
4204
+ state.pongTimeout = null;
4205
+ }
4206
+ }
4207
+ const rawText = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString() : Array.isArray(data) ? Buffer.concat(data).toString() : data instanceof ArrayBuffer ? Buffer.from(new Uint8Array(data)).toString() : (
4208
+ // Fallback: try best-effort stringification
4209
+ String(data)
4210
+ );
4211
+ this.lastFrameAtByConv.set(conversationId, Date.now());
4212
+ const msg = JSON.parse(rawText);
4213
+ if (msg.type === "ws_connected") {
4214
+ this.opts.onDebug?.({
4215
+ event: "ws_connected",
4216
+ conversationId,
4217
+ detail: {
4218
+ your_did: msg.your_did,
4219
+ server_ts_ms: msg.server_ts_ms,
4220
+ conversation_id: msg.conversation_id
4221
+ }
4222
+ });
4223
+ } else if (msg.type === "ws_message" && msg.envelope) {
4224
+ const env = msg.envelope;
4225
+ const ignoreSelf = this.opts.ignoreSelfMessages ?? true;
4226
+ if (!ignoreSelf || env.sender_did !== this.opts.myDid) {
4227
+ Promise.resolve(this.opts.onMessage(env, conversationId)).catch((error) => {
4228
+ this.opts.onError?.(error instanceof Error ? error : new Error(String(error)));
4229
+ });
4230
+ } else {
4231
+ this.opts.onDebug?.({
4232
+ event: "ws_message_ignored_self",
4233
+ conversationId,
4234
+ detail: { sender_did: env.sender_did, message_id: env.message_id, seq: env.seq }
4235
+ });
4236
+ }
4237
+ } else if (msg.type === "ws_control" && msg.control) {
4238
+ this.opts.onControl?.(msg.control, conversationId);
4239
+ this.opts.onDebug?.({ event: "ws_control", conversationId, detail: msg.control });
4240
+ } else if (msg.type === "ws_receipt" && msg.receipt) {
4241
+ this.opts.onDebug?.({ event: "ws_receipt", conversationId, detail: msg.receipt });
4242
+ } else if (msg?.type) {
4243
+ this.opts.onDebug?.({ event: "ws_unknown_type", conversationId, detail: { type: msg.type } });
4244
+ }
4245
+ } catch (e) {
4246
+ const now = Date.now();
4247
+ const last = this.lastParseErrorAtByConv.get(conversationId) ?? 0;
4248
+ if (now - last > 3e4) {
4249
+ this.lastParseErrorAtByConv.set(conversationId, now);
4250
+ this.opts.onDebug?.({
4251
+ event: "ws_parse_error",
4252
+ conversationId,
4253
+ detail: {
4254
+ message: e?.message ?? String(e),
4255
+ last_frame_at_ms: this.lastFrameAtByConv.get(conversationId) ?? null
4256
+ }
4257
+ });
4258
+ }
4259
+ }
4260
+ });
4261
+ ws.on("pong", () => {
4262
+ const state = this.heartbeatStates.get(conversationId);
4263
+ if (!state) return;
4264
+ state.lastRxAt = Date.now();
4265
+ state.missedPongs = 0;
4266
+ if (state.pongTimeout) {
4267
+ clearTimeout(state.pongTimeout);
4268
+ state.pongTimeout = null;
4269
+ }
4270
+ });
4271
+ ws.on("close", () => {
4272
+ this.connections.delete(conversationId);
4273
+ const state = this.heartbeatStates.get(conversationId);
4274
+ if (state?.pongTimeout) clearTimeout(state.pongTimeout);
4275
+ this.heartbeatStates.delete(conversationId);
4276
+ if (!this.stopped && !this.stoppedConversations.has(conversationId)) {
4277
+ this.scheduleReconnect(conversationId);
4278
+ }
4279
+ });
4280
+ ws.on("error", (err) => {
4281
+ this.opts.onError?.(err);
4282
+ });
4283
+ }
4284
+ heartbeatTick() {
4285
+ const hb = this.opts.heartbeat ?? {};
4286
+ const hbEnabled = hb.enable ?? DEFAULT_HEARTBEAT.enable;
4287
+ if (!hbEnabled) return;
4288
+ const idleThresholdMs = hb.idleThresholdMs ?? DEFAULT_HEARTBEAT.idleThresholdMs;
4289
+ const pingIntervalMs = hb.pingIntervalMs ?? DEFAULT_HEARTBEAT.pingIntervalMs;
4290
+ const pongTimeoutMs = hb.pongTimeoutMs ?? DEFAULT_HEARTBEAT.pongTimeoutMs;
4291
+ const maxMissedPongs = hb.maxMissedPongs ?? DEFAULT_HEARTBEAT.maxMissedPongs;
4292
+ const jitter = hb.jitter ?? DEFAULT_HEARTBEAT.jitter;
4293
+ const now = Date.now();
4294
+ for (const [conversationId, ws] of this.connections) {
4295
+ if (ws.readyState !== WebSocket.OPEN) continue;
4296
+ const state = this.heartbeatStates.get(conversationId);
4297
+ if (!state) continue;
4298
+ if (state.pongTimeout) continue;
4299
+ const idleFor = now - state.lastRxAt;
4300
+ if (idleFor < idleThresholdMs) continue;
4301
+ if (now < state.nextPingAt) continue;
4302
+ if (state.lastPingAt !== 0 && now - state.lastPingAt < pingIntervalMs * 0.5) {
4303
+ continue;
4304
+ }
4305
+ try {
4306
+ ws.send(JSON.stringify({ type: "hb" }));
4307
+ } catch {
4308
+ }
4309
+ ws.ping();
4310
+ state.lastPingAt = now;
4311
+ const jitterFactor = 1 + (Math.random() - 0.5) * 2 * jitter;
4312
+ state.nextPingAt = now + Math.max(1e3, pingIntervalMs * jitterFactor);
4313
+ state.pongTimeout = setTimeout(() => {
4314
+ state.missedPongs += 1;
4315
+ state.pongTimeout = null;
4316
+ if (state.missedPongs >= maxMissedPongs) {
4317
+ try {
4318
+ ws.close(1001, "pong timeout");
4319
+ } catch {
4320
+ }
4321
+ }
4322
+ }, pongTimeoutMs);
4323
+ }
4324
+ }
4325
+ scheduleReconnect(conversationId) {
4326
+ if (this.reconnectTimers.has(conversationId)) return;
4327
+ const attempt = this.reconnectAttempts.get(conversationId) ?? 0;
4328
+ this.reconnectAttempts.set(conversationId, attempt + 1);
4329
+ const tryConnect = () => {
4330
+ this.reconnectTimers.delete(conversationId);
4331
+ if (this.stopped) return;
4332
+ void this.connectAsync(conversationId);
4333
+ };
4334
+ const delay = Math.min(
4335
+ RECONNECT_BASE_MS * Math.pow(2, attempt) + (Math.random() - 0.5) * RECONNECT_JITTER * RECONNECT_BASE_MS,
4336
+ RECONNECT_MAX_MS
4337
+ );
4338
+ const timer = setTimeout(tryConnect, delay);
4339
+ this.reconnectTimers.set(conversationId, timer);
4340
+ }
4341
+ isSubscribableConversationType(type) {
4342
+ return type === "dm" || type === "pending_dm" || type === "channel" || type === "group";
4343
+ }
4344
+ async connectAll() {
4345
+ try {
4346
+ const convos = await this.opts.listConversations();
4347
+ const subscribable = convos.filter((c) => this.isSubscribableConversationType(c.type));
4348
+ for (const c of subscribable) {
4349
+ void this.connectAsync(c.conversation_id);
4350
+ }
4351
+ } catch (err) {
4352
+ this.opts.onError?.(err instanceof Error ? err : new Error(String(err)));
4353
+ }
4354
+ }
4355
+ async syncConnections() {
4356
+ if (this.stopped) return;
4357
+ try {
4358
+ const convos = await this.opts.listConversations();
4359
+ const subscribableIds = new Set(
4360
+ convos.filter((c) => this.isSubscribableConversationType(c.type)).map((c) => c.conversation_id)
4361
+ );
4362
+ for (const convId of subscribableIds) {
4363
+ if (!this.stoppedConversations.has(convId) && !this.connections.has(convId)) {
4364
+ void this.connectAsync(convId);
4365
+ }
4366
+ }
4367
+ for (const convId of this.connections.keys()) {
4368
+ if (!subscribableIds.has(convId) || this.stoppedConversations.has(convId)) {
4369
+ const ws = this.connections.get(convId);
4370
+ if (ws) {
4371
+ ws.removeAllListeners();
4372
+ ws.close();
4373
+ this.connections.delete(convId);
4374
+ }
4375
+ }
4376
+ }
4377
+ } catch {
4378
+ }
4379
+ }
4380
+ };
4381
+
4382
+ // src/user-wake-subscription.ts
4383
+ import WebSocket2 from "ws";
4384
+ var RECONNECT_BASE_MS2 = 1e3;
4385
+ var RECONNECT_MAX_MS2 = 3e4;
4386
+ var RECONNECT_JITTER2 = 0.2;
4387
+ var DEFAULT_HEARTBEAT2 = {
4388
+ enable: true,
4389
+ idleThresholdMs: 1e4,
4390
+ pingIntervalMs: 15e3,
4391
+ pongTimeoutMs: 1e4,
4392
+ maxMissedPongs: 2,
4393
+ tickMs: 5e3,
4394
+ jitter: 0.2
4395
+ };
4396
+ var UserWakeSubscription = class {
4397
+ constructor(opts) {
4398
+ this.opts = opts;
4399
+ }
4400
+ ws = null;
4401
+ reconnectTimer = null;
4402
+ reconnectAttempt = 0;
4403
+ heartbeatTimer = null;
4404
+ stopped = false;
4405
+ lastRxAt = 0;
4406
+ lastPingAt = 0;
4407
+ nextPingAt = 0;
4408
+ missedPongs = 0;
4409
+ pongTimeout = null;
4410
+ start() {
4411
+ this.stopped = false;
4412
+ void this.connectAsync();
4413
+ const hb = this.opts.heartbeat ?? {};
4414
+ if (hb.enable ?? DEFAULT_HEARTBEAT2.enable) {
4415
+ this.heartbeatTimer = setInterval(() => this.heartbeatTick(), hb.tickMs ?? DEFAULT_HEARTBEAT2.tickMs);
4416
+ }
4417
+ }
4418
+ stop() {
4419
+ this.stopped = true;
4420
+ if (this.reconnectTimer) {
4421
+ clearTimeout(this.reconnectTimer);
4422
+ this.reconnectTimer = null;
4423
+ }
4424
+ if (this.heartbeatTimer) {
4425
+ clearInterval(this.heartbeatTimer);
4426
+ this.heartbeatTimer = null;
4427
+ }
4428
+ if (this.pongTimeout) {
4429
+ clearTimeout(this.pongTimeout);
4430
+ this.pongTimeout = null;
4431
+ }
4432
+ if (this.ws) {
4433
+ this.ws.removeAllListeners();
4434
+ if (this.ws.readyState === WebSocket2.OPEN || this.ws.readyState === WebSocket2.CONNECTING) {
4435
+ this.ws.close();
4436
+ }
4437
+ this.ws = null;
4438
+ }
4439
+ }
4440
+ wakeUrl() {
4441
+ const base = this.opts.serverUrl.replace(/^http/, "ws").replace(/\/$/, "");
4442
+ return `${base}/v1/wake`;
4443
+ }
4444
+ async connectAsync() {
4445
+ if (this.stopped || this.ws) return;
4446
+ const token = await Promise.resolve(this.opts.getAccessToken());
4447
+ const ws = new WebSocket2(this.wakeUrl(), {
4448
+ headers: { Authorization: `Bearer ${token}` }
4449
+ });
4450
+ this.ws = ws;
4451
+ const hb = this.opts.heartbeat ?? {};
4452
+ if (hb.enable ?? DEFAULT_HEARTBEAT2.enable) {
4453
+ const now = Date.now();
4454
+ this.lastRxAt = now;
4455
+ this.lastPingAt = 0;
4456
+ this.nextPingAt = now + (hb.idleThresholdMs ?? DEFAULT_HEARTBEAT2.idleThresholdMs);
4457
+ this.missedPongs = 0;
4458
+ }
4459
+ ws.on("open", () => {
4460
+ this.reconnectAttempt = 0;
4461
+ this.opts.onOpen?.();
4462
+ this.opts.onDebug?.({ event: "wake_open" });
4463
+ });
4464
+ ws.on("message", (data) => {
4465
+ try {
4466
+ this.lastRxAt = Date.now();
4467
+ this.missedPongs = 0;
4468
+ if (this.pongTimeout) {
4469
+ clearTimeout(this.pongTimeout);
4470
+ this.pongTimeout = null;
4471
+ }
4472
+ const rawText = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString() : Array.isArray(data) ? Buffer.concat(data).toString() : data instanceof ArrayBuffer ? Buffer.from(new Uint8Array(data)).toString() : String(data);
4473
+ const msg = JSON.parse(rawText);
4474
+ if (msg.type === "wake_connected") {
4475
+ this.opts.onDebug?.({
4476
+ event: "wake_connected",
4477
+ detail: {
4478
+ your_did: msg.your_did,
4479
+ device_id: msg.device_id,
4480
+ server_ts_ms: msg.server_ts_ms
4481
+ }
4482
+ });
4483
+ return;
4484
+ }
4485
+ if (msg.type === "wake" && typeof msg.conversation_id === "string" && msg.conversation_id.trim()) {
4486
+ Promise.resolve(this.opts.onWake(msg)).catch((error) => {
4487
+ this.opts.onError?.(error instanceof Error ? error : new Error(String(error)));
4488
+ });
4489
+ return;
4490
+ }
4491
+ this.opts.onDebug?.({ event: "wake_unknown_type", detail: { type: msg?.type } });
4492
+ } catch (error) {
4493
+ this.opts.onError?.(error instanceof Error ? error : new Error(String(error)));
4494
+ }
4495
+ });
4496
+ ws.on("pong", () => {
4497
+ this.lastRxAt = Date.now();
4498
+ this.missedPongs = 0;
4499
+ if (this.pongTimeout) {
4500
+ clearTimeout(this.pongTimeout);
4501
+ this.pongTimeout = null;
4502
+ }
4503
+ });
4504
+ ws.on("close", () => {
4505
+ this.ws = null;
4506
+ if (this.pongTimeout) {
4507
+ clearTimeout(this.pongTimeout);
4508
+ this.pongTimeout = null;
4509
+ }
4510
+ if (!this.stopped) {
4511
+ this.scheduleReconnect();
4512
+ }
4513
+ });
4514
+ ws.on("error", (err) => {
4515
+ this.opts.onError?.(err);
4516
+ });
4517
+ }
4518
+ scheduleReconnect() {
4519
+ if (this.reconnectTimer) return;
4520
+ const attempt = this.reconnectAttempt++;
4521
+ const delay = Math.min(
4522
+ RECONNECT_BASE_MS2 * Math.pow(2, attempt) + (Math.random() - 0.5) * RECONNECT_JITTER2 * RECONNECT_BASE_MS2,
4523
+ RECONNECT_MAX_MS2
4524
+ );
4525
+ this.reconnectTimer = setTimeout(() => {
4526
+ this.reconnectTimer = null;
4527
+ if (!this.stopped) {
4528
+ void this.connectAsync();
4529
+ }
4530
+ }, delay);
4531
+ }
4532
+ heartbeatTick() {
4533
+ const ws = this.ws;
4534
+ const hb = this.opts.heartbeat ?? {};
4535
+ if (!ws || ws.readyState !== WebSocket2.OPEN || !(hb.enable ?? DEFAULT_HEARTBEAT2.enable)) return;
4536
+ const idleThresholdMs = hb.idleThresholdMs ?? DEFAULT_HEARTBEAT2.idleThresholdMs;
4537
+ const pingIntervalMs = hb.pingIntervalMs ?? DEFAULT_HEARTBEAT2.pingIntervalMs;
4538
+ const pongTimeoutMs = hb.pongTimeoutMs ?? DEFAULT_HEARTBEAT2.pongTimeoutMs;
4539
+ const maxMissedPongs = hb.maxMissedPongs ?? DEFAULT_HEARTBEAT2.maxMissedPongs;
4540
+ const jitter = hb.jitter ?? DEFAULT_HEARTBEAT2.jitter;
4541
+ const now = Date.now();
4542
+ if (this.pongTimeout) return;
4543
+ if (now - this.lastRxAt < idleThresholdMs) return;
4544
+ if (now < this.nextPingAt) return;
4545
+ if (this.lastPingAt !== 0 && now - this.lastPingAt < pingIntervalMs * 0.5) return;
4546
+ try {
4547
+ ws.send(JSON.stringify({ type: "hb" }));
4548
+ } catch {
4549
+ }
4550
+ ws.ping();
4551
+ this.lastPingAt = now;
4552
+ const jitterFactor = 1 + (Math.random() - 0.5) * 2 * jitter;
4553
+ this.nextPingAt = now + Math.max(1e3, pingIntervalMs * jitterFactor);
4554
+ this.pongTimeout = setTimeout(() => {
4555
+ this.missedPongs += 1;
4556
+ this.pongTimeout = null;
4557
+ if (this.missedPongs >= maxMissedPongs) {
4558
+ try {
4559
+ ws.close(1001, "pong timeout");
4560
+ } catch {
4561
+ }
4562
+ }
4563
+ }, pongTimeoutMs);
4564
+ }
4565
+ };
4566
+
4567
+ // src/openclaw-session-bindings.ts
4568
+ import * as fs4 from "fs";
4569
+ import * as os3 from "os";
4570
+ import * as path5 from "path";
4571
+ function resolvePath2(p) {
4572
+ if (!p) return null;
4573
+ if (p.startsWith("~")) return path5.join(os3.homedir(), p.slice(1));
4574
+ return path5.resolve(p);
4575
+ }
4576
+ function ensureDirForFile(filePath) {
4577
+ const dir = path5.dirname(filePath);
4578
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
4579
+ }
4580
+ function getDefaultActiveSessionFile() {
4581
+ return path5.join(os3.homedir(), ".openclaw", "workspace", "ACTIVE_GROUP.md");
4582
+ }
4583
+ function getDefaultIngressStorePath() {
4584
+ return path5.join(os3.homedir(), ".openclaw", "pingagent-im-ingress", "messages.jsonl");
4585
+ }
4586
+ function getDefaultSessionMapPath(storePath) {
4587
+ const base = resolvePath2(storePath) || getDefaultIngressStorePath();
4588
+ return path5.join(path5.dirname(base), "session-map.json");
4589
+ }
4590
+ function getDefaultSessionBindingAlertsPath(storePath) {
4591
+ const base = resolvePath2(storePath) || getDefaultIngressStorePath();
4592
+ return path5.join(path5.dirname(base), "session-map-alerts.json");
4593
+ }
4594
+ function getActiveSessionFilePath() {
4595
+ return resolvePath2(process.env.IM_INGRESS_ACTIVE_GROUP_FILE?.trim()) || getDefaultActiveSessionFile();
4596
+ }
4597
+ function getSessionMapFilePath() {
4598
+ return resolvePath2(process.env.IM_INGRESS_SESSION_MAP_FILE?.trim()) || getDefaultSessionMapPath(process.env.IM_INGRESS_STORE_PATH?.trim());
4599
+ }
4600
+ function getSessionBindingAlertsFilePath() {
4601
+ return resolvePath2(process.env.IM_INGRESS_SESSION_BINDING_ALERTS_FILE?.trim()) || getDefaultSessionBindingAlertsPath(process.env.IM_INGRESS_STORE_PATH?.trim());
4602
+ }
4603
+ function readCurrentActiveSessionKey(filePath = getActiveSessionFilePath()) {
4604
+ try {
4605
+ if (!fs4.existsSync(filePath)) return null;
4606
+ const raw = fs4.readFileSync(filePath, "utf-8");
4607
+ const first = String(raw ?? "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
4608
+ return first || null;
4609
+ } catch {
4610
+ return null;
4611
+ }
4612
+ }
4613
+ function readSessionBindings(filePath = getSessionMapFilePath()) {
4614
+ try {
4615
+ if (!fs4.existsSync(filePath)) return [];
4616
+ const raw = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
4617
+ return Object.entries(raw ?? {}).map(([conversation_id, session_key]) => ({
4618
+ conversation_id: String(conversation_id ?? "").trim(),
4619
+ session_key: String(session_key ?? "").trim()
4620
+ })).filter((row) => row.conversation_id && row.session_key).sort((a, b) => a.conversation_id.localeCompare(b.conversation_id));
4621
+ } catch {
4622
+ return [];
4623
+ }
4624
+ }
4625
+ function writeSessionBindings(entries, filePath = getSessionMapFilePath()) {
4626
+ ensureDirForFile(filePath);
4627
+ const obj = Object.fromEntries(
4628
+ entries.filter((row) => row.conversation_id && row.session_key).map((row) => [row.conversation_id, row.session_key])
4629
+ );
4630
+ fs4.writeFileSync(filePath, JSON.stringify(obj, null, 2), "utf-8");
4631
+ return filePath;
4632
+ }
4633
+ function setSessionBinding(conversationId, sessionKey, filePath = getSessionMapFilePath()) {
4634
+ const entries = readSessionBindings(filePath).filter((row) => row.conversation_id !== conversationId);
4635
+ const binding = { conversation_id: conversationId, session_key: sessionKey };
4636
+ entries.push(binding);
4637
+ writeSessionBindings(entries, filePath);
4638
+ clearSessionBindingAlert(conversationId);
4639
+ return { path: filePath, binding };
4640
+ }
4641
+ function removeSessionBinding(conversationId, filePath = getSessionMapFilePath()) {
4642
+ const entries = readSessionBindings(filePath);
4643
+ const next = entries.filter((row) => row.conversation_id !== conversationId);
4644
+ const removed = next.length !== entries.length;
4645
+ if (removed) writeSessionBindings(next, filePath);
4646
+ clearSessionBindingAlert(conversationId);
4647
+ return { path: filePath, removed };
4648
+ }
4649
+ function readSessionBindingAlerts(filePath = getSessionBindingAlertsFilePath()) {
4650
+ try {
4651
+ if (!fs4.existsSync(filePath)) return [];
4652
+ const raw = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
4653
+ return Object.entries(raw ?? {}).map(([conversation_id, value]) => {
4654
+ const row = value;
4655
+ return {
4656
+ conversation_id: String(conversation_id ?? "").trim(),
4657
+ session_key: String(row?.session_key ?? "").trim(),
4658
+ status: "missing_session",
4659
+ message: String(row?.message ?? "").trim(),
4660
+ detected_at: String(row?.detected_at ?? "").trim()
4661
+ };
4662
+ }).filter((row) => row.conversation_id && row.session_key && row.message).sort((a, b) => a.conversation_id.localeCompare(b.conversation_id));
4663
+ } catch {
4664
+ return [];
4665
+ }
4666
+ }
4667
+ function writeSessionBindingAlerts(entries, filePath = getSessionBindingAlertsFilePath()) {
4668
+ ensureDirForFile(filePath);
4669
+ const obj = Object.fromEntries(
4670
+ entries.map((row) => [row.conversation_id, {
4671
+ session_key: row.session_key,
4672
+ status: row.status,
4673
+ message: row.message,
4674
+ detected_at: row.detected_at
4675
+ }])
4676
+ );
4677
+ fs4.writeFileSync(filePath, JSON.stringify(obj, null, 2), "utf-8");
4678
+ return filePath;
4679
+ }
4680
+ function upsertSessionBindingAlert(alert, filePath = getSessionBindingAlertsFilePath()) {
4681
+ const entries = readSessionBindingAlerts(filePath).filter((row) => row.conversation_id !== alert.conversation_id);
4682
+ entries.push(alert);
4683
+ writeSessionBindingAlerts(entries, filePath);
4684
+ return { path: filePath, alert };
4685
+ }
4686
+ function clearSessionBindingAlert(conversationId, filePath = getSessionBindingAlertsFilePath()) {
4687
+ const entries = readSessionBindingAlerts(filePath);
4688
+ const next = entries.filter((row) => row.conversation_id !== conversationId);
4689
+ const removed = next.length !== entries.length;
4690
+ if (removed) writeSessionBindingAlerts(next, filePath);
4691
+ return { path: filePath, removed };
4692
+ }
4693
+
4694
+ // src/openclaw-runtime.ts
4695
+ import * as fs5 from "fs";
4696
+ import * as os4 from "os";
4697
+ import * as path6 from "path";
4698
+ function resolvePath3(p) {
4699
+ if (!p) return null;
4700
+ if (p.startsWith("~")) return path6.join(os4.homedir(), p.slice(1));
4701
+ return path6.resolve(p);
4702
+ }
4703
+ function getDefaultIngressRuntimeStatusPath(storePath) {
4704
+ const base = resolvePath3(storePath) || path6.join(os4.homedir(), ".openclaw", "pingagent-im-ingress", "messages.jsonl");
4705
+ return path6.join(path6.dirname(base), "runtime-status.json");
4706
+ }
4707
+ function getIngressRuntimeStatusFilePath() {
4708
+ return resolvePath3(process.env.IM_INGRESS_RUNTIME_STATUS_FILE?.trim()) || getDefaultIngressRuntimeStatusPath(process.env.IM_INGRESS_STORE_PATH?.trim());
4709
+ }
4710
+ function readIngressRuntimeStatus(filePath = getIngressRuntimeStatusFilePath()) {
4711
+ try {
4712
+ if (!fs5.existsSync(filePath)) return null;
4713
+ const raw = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
4714
+ return {
4715
+ receive_mode: raw?.receive_mode === "polling_degraded" ? "polling_degraded" : "webhook",
4716
+ reason: raw?.reason ? String(raw.reason) : null,
4717
+ hooks_url: raw?.hooks_url ? String(raw.hooks_url) : null,
4718
+ hooks_base_url: raw?.hooks_base_url ? String(raw.hooks_base_url) : null,
4719
+ hooks_last_error: raw?.hooks_last_error ? String(raw.hooks_last_error) : null,
4720
+ hooks_last_error_at: raw?.hooks_last_error_at ? String(raw.hooks_last_error_at) : null,
4721
+ hooks_last_probe_ok_at: raw?.hooks_last_probe_ok_at ? String(raw.hooks_last_probe_ok_at) : null,
4722
+ hooks_message_mode: raw?.hooks_message_mode ? String(raw.hooks_message_mode) : null,
4723
+ fallback_last_injected_at: raw?.fallback_last_injected_at ? String(raw.fallback_last_injected_at) : null,
4724
+ fallback_last_injected_conversation_id: raw?.fallback_last_injected_conversation_id ? String(raw.fallback_last_injected_conversation_id) : null,
4725
+ active_work_session: raw?.active_work_session ? String(raw.active_work_session) : null,
4726
+ sessions_send_available: raw?.sessions_send_available === true,
4727
+ hooks_fix_hint: raw?.hooks_fix_hint ? String(raw.hooks_fix_hint) : null,
4728
+ gateway_base_url: raw?.gateway_base_url ? String(raw.gateway_base_url) : null,
4729
+ transport_mode: raw?.transport_mode === "channel" ? "channel" : "bridge",
4730
+ preferred_transport_mode: raw?.preferred_transport_mode === "channel" ? "channel" : "bridge",
4731
+ transport_switch_recommended: raw?.transport_switch_recommended === true,
4732
+ transport_switch_reason: raw?.transport_switch_reason ? String(raw.transport_switch_reason) : null,
4733
+ channel_retry_queue_length: Number.isFinite(Number(raw?.channel_retry_queue_length)) ? Number(raw.channel_retry_queue_length) : 0,
4734
+ channel_last_inbound_at: raw?.channel_last_inbound_at ? String(raw.channel_last_inbound_at) : null,
4735
+ channel_last_outbound_at: raw?.channel_last_outbound_at ? String(raw.channel_last_outbound_at) : null,
4736
+ channel_last_degraded_at: raw?.channel_last_degraded_at ? String(raw.channel_last_degraded_at) : null,
4737
+ channel_last_repaired_at: raw?.channel_last_repaired_at ? String(raw.channel_last_repaired_at) : null,
4738
+ channel_last_error: raw?.channel_last_error ? String(raw.channel_last_error) : null,
4739
+ channel_consecutive_failures: Number.isFinite(Number(raw?.channel_consecutive_failures)) ? Number(raw.channel_consecutive_failures) : 0,
4740
+ status_updated_at: raw?.status_updated_at ? String(raw.status_updated_at) : null
4741
+ };
4742
+ } catch {
4743
+ return null;
4744
+ }
4745
+ }
4746
+
4747
+ export {
4748
+ HttpTransport,
4749
+ getRootDir,
4750
+ getProfile,
4751
+ getIdentityPath,
4752
+ getStorePath,
4753
+ ensureIdentityEncryptionKeys,
4754
+ generateIdentity,
4755
+ identityExists,
4756
+ loadIdentity,
4757
+ saveIdentity,
4758
+ updateStoredToken,
4759
+ ContactManager,
4760
+ HistoryManager,
4761
+ previewFromPayload,
4762
+ buildSessionKey,
4763
+ SessionManager,
4764
+ SESSION_SUMMARY_FIELDS,
4765
+ buildSessionSummaryHandoffText,
4766
+ SessionSummaryManager,
4767
+ TaskThreadManager,
4768
+ TaskHandoffManager,
4769
+ CollaborationProjectionOutboxManager,
4770
+ CollaborationEventManager,
4771
+ normalizeCapabilityCard,
4772
+ capabilityCardToLegacyCapabilities,
4773
+ formatCapabilityCardSummary,
4774
+ isEncryptedPayload,
4775
+ shouldEncryptConversationPayload,
4776
+ encryptPayloadForRecipients,
4777
+ decryptPayloadForIdentity,
4778
+ PingAgentClient,
4779
+ ensureTokenValid,
4780
+ LocalStore,
4781
+ defaultTrustPolicyDoc,
4782
+ normalizeTrustPolicyDoc,
4783
+ matchesTrustPolicyRule,
4784
+ decideContactPolicy,
4785
+ decideTaskPolicy,
4786
+ TrustPolicyAuditManager,
4787
+ buildTrustPolicyRecommendations,
4788
+ upsertTrustPolicyRecommendation,
4789
+ summarizeTrustPolicyAudit,
4790
+ getTrustRecommendationActionLabel,
4791
+ TrustRecommendationManager,
4792
+ OperatorSeenStateManager,
4793
+ getProjectedEventTypes,
4794
+ describeProjectionPreset,
4795
+ getProjectionDisposition,
4796
+ listPendingDecisionViews,
4797
+ summarizeSinceLastSeen,
4798
+ buildDeliveryTimeline,
4799
+ buildProjectionPreview,
4800
+ buildDecisionReminderOutboxMessage,
4801
+ hasDecisionPromptOutbox,
4802
+ normalizeTransportMode,
4803
+ getTransportPreferenceFilePath,
4804
+ readTransportPreference,
4805
+ writeTransportPreference,
4806
+ switchTransportPreference,
4807
+ deriveTransportHealth,
4808
+ A2AAdapter,
4809
+ WsSubscription,
4810
+ UserWakeSubscription,
4811
+ getActiveSessionFilePath,
4812
+ getSessionMapFilePath,
4813
+ getSessionBindingAlertsFilePath,
4814
+ readCurrentActiveSessionKey,
4815
+ readSessionBindings,
4816
+ writeSessionBindings,
4817
+ setSessionBinding,
4818
+ removeSessionBinding,
4819
+ readSessionBindingAlerts,
4820
+ writeSessionBindingAlerts,
4821
+ upsertSessionBindingAlert,
4822
+ clearSessionBindingAlert,
4823
+ getIngressRuntimeStatusFilePath,
4824
+ readIngressRuntimeStatus
4825
+ };