@fitchmultz/pi-subagents 0.46.2 → 0.48.0

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/dist/extension/build-info.js +1 -1
  4. package/dist/pi-intercom/bridge-broadcast.js +60 -0
  5. package/dist/pi-intercom/bridge-catalog.js +372 -0
  6. package/dist/pi-intercom/bridge-config.js +47 -26
  7. package/dist/pi-intercom/bridge-control.js +104 -0
  8. package/dist/pi-intercom/bridge-files.js +47 -0
  9. package/dist/pi-intercom/bridge-groups.js +135 -0
  10. package/dist/pi-intercom/bridge-http.js +8 -1
  11. package/dist/pi-intercom/bridge-launch-journal.js +84 -0
  12. package/dist/pi-intercom/bridge-launch-policy.js +71 -5
  13. package/dist/pi-intercom/bridge-launch.js +186 -76
  14. package/dist/pi-intercom/bridge-mailbox.js +324 -0
  15. package/dist/pi-intercom/bridge-operations.js +127 -0
  16. package/dist/pi-intercom/bridge-presence.js +100 -0
  17. package/dist/pi-intercom/bridge-protocol.js +7 -0
  18. package/dist/pi-intercom/bridge-routes.js +190 -17
  19. package/dist/pi-intercom/bridge-rpc-process.js +43 -15
  20. package/dist/pi-intercom/bridge-session.js +83 -85
  21. package/dist/pi-intercom/bridge-target.js +22 -0
  22. package/dist/pi-intercom/bridge-wake.js +158 -0
  23. package/dist/pi-intercom/bridge.js +84 -44
  24. package/dist/pi-intercom/broker/client.js +1 -0
  25. package/dist/pi-intercom/connection.js +2 -3
  26. package/dist/pi-intercom/inbound.js +2 -4
  27. package/dist/pi-intercom/index.js +7 -1
  28. package/dist/pi-intercom/native-control-auth.js +80 -0
  29. package/dist/pi-intercom/native-control-protocol.js +111 -0
  30. package/dist/pi-intercom/native-control.js +130 -0
  31. package/dist/pi-intercom/saved-transcript.js +135 -0
  32. package/dist/pi-intercom/session-targets.js +3 -0
  33. package/dist/pi-intercom/transcript.js +175 -0
  34. package/docs/intercom-bridge.md +293 -161
  35. package/docs/intercom.md +1 -1
  36. package/docs/reference.md +1 -1
  37. package/package.json +1 -1
  38. package/scripts/intercom-bridge-credentials.mjs +54 -12
  39. package/scripts/intercom-remote.mjs +196 -31
  40. package/scripts/package-smoke.mjs +1 -1
  41. package/src/pi-intercom/bridge-broadcast.ts +70 -0
  42. package/src/pi-intercom/bridge-catalog.ts +489 -0
  43. package/src/pi-intercom/bridge-config.ts +64 -37
  44. package/src/pi-intercom/bridge-control.ts +179 -0
  45. package/src/pi-intercom/bridge-files.ts +59 -0
  46. package/src/pi-intercom/bridge-groups.ts +169 -0
  47. package/src/pi-intercom/bridge-http.ts +15 -1
  48. package/src/pi-intercom/bridge-launch-journal.ts +122 -0
  49. package/src/pi-intercom/bridge-launch-policy.ts +88 -7
  50. package/src/pi-intercom/bridge-launch.ts +296 -107
  51. package/src/pi-intercom/bridge-mailbox.ts +395 -0
  52. package/src/pi-intercom/bridge-operations.ts +169 -0
  53. package/src/pi-intercom/bridge-presence.ts +128 -0
  54. package/src/pi-intercom/bridge-protocol.ts +8 -0
  55. package/src/pi-intercom/bridge-routes.ts +281 -32
  56. package/src/pi-intercom/bridge-rpc-process.ts +53 -16
  57. package/src/pi-intercom/bridge-session.ts +106 -105
  58. package/src/pi-intercom/bridge-target.ts +35 -0
  59. package/src/pi-intercom/bridge-wake.ts +188 -0
  60. package/src/pi-intercom/bridge.ts +88 -54
  61. package/src/pi-intercom/broker/client.ts +3 -0
  62. package/src/pi-intercom/connection.ts +2 -3
  63. package/src/pi-intercom/inbound.ts +2 -4
  64. package/src/pi-intercom/index.ts +7 -1
  65. package/src/pi-intercom/native-control-auth.ts +89 -0
  66. package/src/pi-intercom/native-control-protocol.ts +180 -0
  67. package/src/pi-intercom/native-control.ts +160 -0
  68. package/src/pi-intercom/saved-transcript.ts +175 -0
  69. package/src/pi-intercom/session-targets.ts +4 -0
  70. package/src/pi-intercom/transcript.ts +226 -0
  71. package/src/pi-intercom/types.ts +2 -0
@@ -1,7 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { IntercomClient } from "./broker/client.js";
3
- import { formatSessionTarget, resolveSessionProjectId, resolveSessionTarget, } from "./session-targets.js";
4
- import { Fault, INBOX_BYTES, INBOX_COUNT, cancelled, log, wait } from "./bridge-protocol.js";
3
+ import { resolveSessionProjectId } from "./session-targets.js";
4
+ import { Fault, cancelled, log, wait } from "./bridge-protocol.js";
5
+ import { Mailbox } from "./bridge-mailbox.js";
6
+ import { resolveBridgeTarget, bridgeSessionList } from "./bridge-target.js";
7
+ import { ControlReplies } from "./bridge-control.js";
5
8
  import { WaitingAsk } from "./bridge-ask.js";
6
9
  const MAX_REQUESTS = 8;
7
10
  const IDLE_TIMEOUT = 5 * 60 * 1000;
@@ -17,9 +20,10 @@ export class Session {
17
20
  listTimeoutMs: LIST_TIMEOUT,
18
21
  });
19
22
  requests = new Set();
20
- inbox = new Map();
21
- inboxBytes = 0;
23
+ inbox;
24
+ replying = new Set();
22
25
  pending;
26
+ controls = new ControlReplies();
23
27
  closed = false;
24
28
  connecting;
25
29
  disconnecting;
@@ -30,6 +34,7 @@ export class Session {
30
34
  this.owner = owner;
31
35
  this.identity = identity;
32
36
  this.expires = expires;
37
+ this.inbox = new Mailbox(owner.stateDir, identity.fingerprint256, owner.notifications);
33
38
  this.client.on("error", () => this.close(new Fault(503, "broker_unavailable", "Broker connection failed.")));
34
39
  this.client.on("disconnected", () => this.close(new Fault(503, "broker_disconnected", "Broker disconnected.")));
35
40
  this.client.on("_registered", () => {
@@ -49,6 +54,9 @@ export class Session {
49
54
  if (this.closed) {
50
55
  throw this.closeReason;
51
56
  }
57
+ this.authorize();
58
+ }
59
+ authorize() {
52
60
  if (!this.owner.allowed(this.identity)) {
53
61
  throw new Fault(403, "revoked", "Identity is no longer authorized.");
54
62
  }
@@ -102,6 +110,7 @@ export class Session {
102
110
  this.close(fault);
103
111
  throw fault;
104
112
  }
113
+ this.presence();
105
114
  log("registered", this.fields());
106
115
  }
107
116
  async connect(signal) {
@@ -136,42 +145,33 @@ export class Session {
136
145
  // The original request receives the connection failure; teardown must still close a late socket.
137
146
  }
138
147
  await this.disconnect();
148
+ this.owner.lost(this.identity.fingerprint256, this.inbox.close());
139
149
  this.owner.retired(this.identity.fingerprint256, this.generation);
140
150
  log("session_closed", { ...this.fields(), result: this.closeReason.code });
141
151
  }
142
- close(error) {
152
+ close(fault) {
143
153
  if (this.closed) {
144
154
  return;
145
155
  }
146
156
  this.closed = true;
147
- this.closeReason = error;
157
+ this.closeReason = fault;
148
158
  clearTimeout(this.lease);
149
- this.pending?.reject(error);
159
+ this.pending?.reject(fault);
160
+ this.controls.close(fault);
150
161
  for (const controller of this.requests) {
151
- controller.abort(error);
162
+ controller.abort(fault);
152
163
  }
153
- this.owner.lost(this.identity.fingerprint256, this.inbox.size);
154
- this.inbox.clear();
155
- this.inboxBytes = 0;
156
164
  // Retain the generation until acquisition/registration AND final disconnect settle.
157
- this.startRetirement();
158
- }
159
- startRetirement() {
160
165
  this.retirement = this.retire().catch((caught) => this.cleanupFailed(caught));
161
166
  }
162
167
  async settled() {
163
168
  await this.retirement;
164
169
  }
165
170
  peerLeft(id) {
171
+ this.controls.peerLeft(id);
166
172
  if (this.pending?.peer === id && !this.pending.settled) {
167
173
  this.pending.reject(new Fault(409, "peer_offline", "Ask recipient disconnected.", { id: this.pending.id }));
168
174
  }
169
- for (const [messageId, entry] of this.inbox) {
170
- if (entry.from.id === id && entry.message.expectsReply === true) {
171
- this.remove(messageId);
172
- this.owner.lost(this.identity.fingerprint256);
173
- }
174
- }
175
175
  this.presence();
176
176
  }
177
177
  receive(from, message) {
@@ -187,48 +187,60 @@ export class Session {
187
187
  : new Fault(403, "revoked", "Identity is no longer authorized."));
188
188
  return;
189
189
  }
190
- log("received", { ...this.fields(), peer: from.id, messageId: message.id, result: "received" });
191
- const pending = this.pending;
192
- if (pending?.matches(from.id, message.replyTo) === true) {
193
- pending.resolve({ from, message });
190
+ if (this.controls.receive(from.id, message)) {
194
191
  return;
195
192
  }
196
- if (this.inbox.has(message.id)) {
197
- this.owner.lost(this.identity.fingerprint256);
193
+ if (!this.inbox.persistent && this.resolveReply(from, message)) {
198
194
  return;
199
195
  }
200
- const bytes = Buffer.byteLength(JSON.stringify({ from, message }));
201
- if (this.inbox.size >= INBOX_COUNT || this.inboxBytes + bytes > INBOX_BYTES) {
202
- this.owner.lost(this.identity.fingerprint256);
196
+ this.storeReceive(from, message);
197
+ }
198
+ storeReceive(from, message) {
199
+ let result;
200
+ try {
201
+ result = this.inbox.add({ from, message });
202
+ }
203
+ catch {
203
204
  log("inbox_overflow", {
204
205
  ...this.fields(),
205
206
  peer: from.id,
206
207
  messageId: message.id,
207
- result: "lost",
208
+ result: "storage_failed",
209
+ lossRecorded: false,
208
210
  });
211
+ this.close(new Fault(507, "mailbox_storage_failed", "Mailbox storage failed; inspect the metadata audit for unrecorded losses."));
209
212
  return;
210
213
  }
211
- // ponytail: bounded volatile inbox; use a private durable journal if restart/lease-loss recovery is required.
212
- this.inbox.set(message.id, { from, message, bytes, replying: false });
213
- this.inboxBytes += bytes;
214
+ if (result === "lost" || (result === "duplicate" && !this.inbox.persistent)) {
215
+ this.owner.lost(this.identity.fingerprint256);
216
+ }
217
+ this.recordReceive(from.id, message.id, result);
218
+ if (result !== "lost") {
219
+ this.resolveReply(from, message);
220
+ }
214
221
  this.presence();
215
222
  }
216
- remove(id) {
217
- const entry = this.inbox.get(id);
218
- if (entry) {
219
- this.inboxBytes -= entry.bytes;
220
- this.inbox.delete(id);
223
+ resolveReply(from, message) {
224
+ const pending = this.pending;
225
+ if (pending?.matches(from.id, message.replyTo) !== true) {
226
+ return false;
221
227
  }
228
+ pending.resolve({ from, message });
229
+ return true;
230
+ }
231
+ recordReceive(peer, messageId, result) {
232
+ log(result === "lost" ? "inbox_overflow" : "received", {
233
+ ...this.fields(),
234
+ peer,
235
+ messageId,
236
+ result: result === "lost" && this.inbox.persistent ? "storage_failed" : result,
237
+ });
222
238
  }
223
239
  presence() {
224
240
  if (this.closed) {
225
241
  return;
226
242
  }
227
- const pendingAsks = [...this.inbox.values()].filter((entry) => entry.message.expectsReply === true).length;
228
- this.client.updatePresence({
229
- pendingAsks,
230
- acceptsAsks: this.inbox.size < INBOX_COUNT && this.inboxBytes < INBOX_BYTES,
231
- });
243
+ this.client.updatePresence(this.inbox.presence());
232
244
  }
233
245
  async peers(signal) {
234
246
  this.guard(signal);
@@ -250,20 +262,16 @@ export class Session {
250
262
  await this.connect(signal);
251
263
  return this.peers(signal);
252
264
  }
265
+ async nativeControl(operation, certificate, context) {
266
+ await this.connect(context.signal);
267
+ const peer = await this.target(operation.to, context.signal);
268
+ return this.controls.request(this.client, { ...operation, peer: peer.id, certificate }, { ...context, guard: () => this.guard(context.signal) });
269
+ }
253
270
  async target(to, signal) {
254
- const resolution = resolveSessionTarget(await this.peers(signal), to);
255
- if (resolution.status === "none") {
256
- throw new Fault(404, "peer_offline", "Peer is not connected.");
257
- }
258
- if (resolution.status !== "found" || !resolution.target) {
259
- throw new Fault(409, "ambiguous_target", "Use an unambiguous peer name or session ID (prefixes need at least 8 characters).");
260
- }
261
- if (resolution.target.id === this.client.sessionId) {
262
- throw new Fault(400, "self_target", "Choose another peer.");
263
- }
264
- return resolution.target;
271
+ return resolveBridgeTarget(await this.peers(signal), to, this.client.sessionId);
265
272
  }
266
273
  async send(peer, message, context, options = {}) {
274
+ await this.connect(context.signal);
267
275
  const { signal, requestId } = context;
268
276
  context.onPeer(peer);
269
277
  this.guard(signal);
@@ -337,30 +345,19 @@ export class Session {
337
345
  }
338
346
  }
339
347
  ack(ids) {
340
- const acked = [];
341
- const retained = [];
342
- for (const id of ids) {
343
- const entry = this.inbox.get(id);
344
- if (entry?.message.expectsReply === true) {
345
- retained.push(id);
346
- }
347
- else if (entry) {
348
- this.remove(id);
349
- acked.push(id);
350
- }
351
- }
348
+ const result = this.inbox.ack(ids);
352
349
  this.presence();
353
- return { acked, retained };
350
+ return result;
354
351
  }
355
352
  async answer(replyTo, message, context) {
356
353
  const entry = this.inbox.get(replyTo);
357
354
  if (entry?.message.expectsReply !== true) {
358
355
  throw new Fault(404, "unknown_ask", "replyTo must identify an unanswered inbound ask.");
359
356
  }
360
- if (entry.replying) {
357
+ if (this.replying.has(replyTo)) {
361
358
  throw new Fault(409, "reply_in_progress", "This ask already has a reply in flight.");
362
359
  }
363
- entry.replying = true;
360
+ this.replying.add(replyTo);
364
361
  const peer = entry.from.id;
365
362
  context.onPeer(peer);
366
363
  try {
@@ -369,16 +366,16 @@ export class Session {
369
366
  throw new Fault(409, "peer_offline", "Original ask sender is disconnected.");
370
367
  }
371
368
  this.guard(context.signal);
372
- if (this.inbox.get(replyTo) !== entry) {
369
+ if (!this.inbox.get(replyTo)) {
373
370
  throw new Fault(409, "ask_retired", "Inbound ask is no longer replyable.");
374
371
  }
375
372
  const data = { ...(await this.send(peer, message, context, { replyTo })) };
376
- this.remove(replyTo);
373
+ this.inbox.remove(replyTo);
377
374
  this.presence();
378
375
  return data;
379
376
  }
380
377
  finally {
381
- entry.replying = false;
378
+ this.replying.delete(replyTo);
382
379
  }
383
380
  }
384
381
  async execute(operation, context) {
@@ -386,6 +383,19 @@ export class Session {
386
383
  if (operation.route === "POST /v1/ask") {
387
384
  return this.ask(operation, context);
388
385
  }
386
+ if (operation.route === "GET /v1/inbox" || operation.route === "POST /v1/ack") {
387
+ // Durable reads/acks remain available while the local broker is down.
388
+ if (!this.inbox.persistent) {
389
+ await this.connect(context.signal);
390
+ }
391
+ if (operation.route === "POST /v1/ack") {
392
+ return this.ack(operation.ids);
393
+ }
394
+ return {
395
+ ...this.inbox.page(operation.after, operation.limit),
396
+ ...this.inbox.status(this.owner.losses(this.identity.fingerprint256)),
397
+ };
398
+ }
389
399
  await this.connect(context.signal);
390
400
  switch (operation.route) {
391
401
  case "POST /v1/register":
@@ -394,21 +404,9 @@ export class Session {
394
404
  const sessions = await this.peers(context.signal);
395
405
  return {
396
406
  sessionId: this.client.sessionId,
397
- sessions: sessions.map(({ topics: _topics, subscriptions: _subscriptions, ...entry }) => {
398
- return Object.assign(entry, { target: formatSessionTarget(entry, sessions) });
399
- }),
407
+ sessions: bridgeSessionList(sessions),
400
408
  };
401
409
  }
402
- case "GET /v1/inbox": {
403
- const lostMessages = this.owner.losses(this.identity.fingerprint256);
404
- return {
405
- messages: [...this.inbox.values()].map(({ from, message }) => ({ from, message })),
406
- overflow: lostMessages > 0,
407
- lostMessages,
408
- };
409
- }
410
- case "POST /v1/ack":
411
- return this.ack(operation.ids);
412
410
  case "POST /v1/send":
413
411
  return {
414
412
  ...(await this.send((await this.target(operation.to, context.signal)).id, operation.message, context)),
@@ -0,0 +1,22 @@
1
+ import { resolveSessionTarget, formatSessionTarget, nativePeerId } from "./session-targets.js";
2
+ import { Fault } from "./bridge-protocol.js";
3
+ export function bridgeSessionList(peers) {
4
+ return peers.map(({ topics: _topics, subscriptions: _subscriptions, ...entry }) => ({
5
+ ...entry,
6
+ target: formatSessionTarget(entry, peers),
7
+ }));
8
+ }
9
+ export function resolveBridgeTarget(peers, to, self) {
10
+ const exactNative = peers.find((peer) => peer.id === nativePeerId(to));
11
+ const resolution = resolveSessionTarget(peers, exactNative?.id ?? to);
12
+ if (resolution.status === "none") {
13
+ throw new Fault(404, "peer_offline", "Peer is not connected.");
14
+ }
15
+ if (resolution.status !== "found" || !resolution.target) {
16
+ throw new Fault(409, "ambiguous_target", "Use an unambiguous peer name or session ID (prefixes need at least 8 characters).");
17
+ }
18
+ if (resolution.target.id === self) {
19
+ throw new Fault(400, "self_target", "Choose another peer.");
20
+ }
21
+ return resolution.target;
22
+ }
@@ -0,0 +1,158 @@
1
+ import { log } from "./bridge-protocol.js";
2
+ import { Mailbox } from "./bridge-mailbox.js";
3
+ const POLL_MS = 15000;
4
+ const FALLBACK_AFTER_MS = 120000;
5
+ const MAX_BACKOFF_MS = 300000;
6
+ export class WakeDelivery {
7
+ controller = new AbortController();
8
+ timer;
9
+ running;
10
+ backoff = POLL_MS;
11
+ name;
12
+ mailbox;
13
+ policy;
14
+ authorized;
15
+ constructor(name, mailbox, policy, authorized) {
16
+ this.name = name;
17
+ this.mailbox = mailbox;
18
+ this.policy = policy;
19
+ this.authorized = authorized;
20
+ }
21
+ start() {
22
+ if (!this.running && !this.controller.signal.aborted) {
23
+ this.schedule(0);
24
+ }
25
+ }
26
+ schedule(delay) {
27
+ clearTimeout(this.timer);
28
+ this.timer = setTimeout(() => {
29
+ this.running = this.deliver()
30
+ .catch(() => {
31
+ log("wake_delivery", { name: this.name, result: "storage_unavailable" });
32
+ this.backoff = Math.min(this.backoff * 2, MAX_BACKOFF_MS);
33
+ })
34
+ .finally(() => {
35
+ this.running = undefined;
36
+ if (!this.controller.signal.aborted) {
37
+ this.schedule(this.backoff);
38
+ }
39
+ });
40
+ }, delay);
41
+ }
42
+ async deliver() {
43
+ if (!this.authorized()) {
44
+ return;
45
+ }
46
+ const entries = this.mailbox.pendingNotifications().messages;
47
+ for (const entry of entries) {
48
+ if (entry.acknowledged) {
49
+ // The box's durable worker owns primary wake/replay after acknowledgement.
50
+ this.mailbox.notificationAccepted(entry.message.id);
51
+ }
52
+ }
53
+ const next = entries.find((entry) => !entry.acknowledged && Date.now() - entry.receivedAt >= FALLBACK_AFTER_MS);
54
+ if (next) {
55
+ // One fallback wakes the box to drain the remaining mailbox; never flood a reset VM.
56
+ await this.post(next);
57
+ return;
58
+ }
59
+ this.backoff = POLL_MS;
60
+ }
61
+ async post(entry) {
62
+ if (!this.authorized()) {
63
+ return;
64
+ }
65
+ const fields = { name: this.name, messageId: entry.message.id };
66
+ try {
67
+ const response = await fetch(this.policy.url, {
68
+ method: "POST",
69
+ redirect: "error",
70
+ headers: {
71
+ Authorization: `Bearer ${this.policy.key}`,
72
+ "Content-Type": "application/json",
73
+ },
74
+ body: JSON.stringify({
75
+ from: entry.from.name ?? entry.from.id,
76
+ from_id: entry.from.id,
77
+ to: this.name,
78
+ message_id: entry.message.id,
79
+ ask_id: entry.message.expectsReply === true ? entry.message.id : null,
80
+ text: entry.message.content.text,
81
+ }),
82
+ signal: AbortSignal.any([this.controller.signal, AbortSignal.timeout(10000)]),
83
+ });
84
+ await response.body?.cancel();
85
+ if (response.ok && !this.controller.signal.aborted && this.authorized()) {
86
+ // HTTP acceptance cannot prove durable intake; the same ID remains pending until inbox ACK.
87
+ this.backoff = MAX_BACKOFF_MS;
88
+ log("wake_delivery", { ...fields, result: "accepted" });
89
+ }
90
+ else {
91
+ this.backoff = Math.min(this.backoff * 2, MAX_BACKOFF_MS);
92
+ log("wake_delivery", { ...fields, result: "http_rejected", status: response.status });
93
+ }
94
+ }
95
+ catch {
96
+ this.backoff = Math.min(this.backoff * 2, MAX_BACKOFF_MS);
97
+ if (!this.controller.signal.aborted) {
98
+ log("wake_delivery", { ...fields, result: "transport_unconfirmed" });
99
+ }
100
+ }
101
+ }
102
+ async stop() {
103
+ this.controller.abort();
104
+ clearTimeout(this.timer);
105
+ await this.running;
106
+ }
107
+ }
108
+ export class WakeDeliveries {
109
+ stateDir;
110
+ allowed;
111
+ deliveries = new Map();
112
+ cleanup = new Set();
113
+ constructor(stateDir, allowed) {
114
+ this.stateDir = stateDir;
115
+ this.allowed = allowed;
116
+ }
117
+ session(identity, expires) {
118
+ if (!identity.wake ||
119
+ this.stateDir === undefined ||
120
+ this.deliveries.has(identity.fingerprint256)) {
121
+ return;
122
+ }
123
+ const mailbox = new Mailbox(this.stateDir, identity.fingerprint256, true);
124
+ const worker = new WakeDelivery(identity.name, mailbox, identity.wake, () => this.allowed(identity) && expires > Date.now());
125
+ this.deliveries.set(identity.fingerprint256, { identity, mailbox, worker });
126
+ worker.start();
127
+ }
128
+ drop(fp) {
129
+ const delivery = this.deliveries.get(fp);
130
+ if (!delivery) {
131
+ return;
132
+ }
133
+ this.deliveries.delete(fp);
134
+ const cleanup = delivery.worker
135
+ .stop()
136
+ .then(() => delivery.mailbox.close())
137
+ .catch(() => {
138
+ log("wake_delivery", { name: delivery.identity.name, result: "cleanup_failed" });
139
+ })
140
+ .finally(() => {
141
+ this.cleanup.delete(cleanup);
142
+ });
143
+ this.cleanup.add(cleanup);
144
+ }
145
+ reload() {
146
+ for (const [fp, delivery] of this.deliveries) {
147
+ if (!this.allowed(delivery.identity)) {
148
+ this.drop(fp);
149
+ }
150
+ }
151
+ }
152
+ async stop() {
153
+ for (const fp of this.deliveries.keys()) {
154
+ this.drop(fp);
155
+ }
156
+ await Promise.all(this.cleanup);
157
+ }
158
+ }