dsh-deeppilot 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -3,18 +3,253 @@ import { createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID,
3
3
  import { access, chmod, mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { WebSocketServer } from "ws";
6
+ import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
6
7
  import { homedir, networkInterfaces } from "node:os";
7
8
  import * as Cordis from "@deepseek-ai/cordis";
8
9
  import { Context } from "@deepseek-ai/cordis";
9
10
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
10
11
  import { connect } from "node:http2";
11
12
  import { spawn } from "node:child_process";
12
- import { constants } from "node:fs";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { request } from "node:https";
15
15
  import z from "@deepseek-ai/schemastery";
16
16
  import { isIP } from "node:net";
17
17
  import { createServer } from "node:http";
18
+ function validSendId(id) {
19
+ return typeof id === "string" && /^\d{13}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
20
+ }
21
+ /** Durable at-most-once dispatch. Unknown outcomes are never automatically retried. */
22
+ var PromptDeliveryJournal = class {
23
+ path;
24
+ capacity;
25
+ entries = Object.create(null);
26
+ inFlight = /* @__PURE__ */ new Map();
27
+ healthy = true;
28
+ constructor(path, capacity = 1e4) {
29
+ this.path = path;
30
+ this.capacity = capacity;
31
+ if (!path || !existsSync(path)) return;
32
+ try {
33
+ if (statSync(path).size > 8388608) throw new Error("oversized journal");
34
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
35
+ if (parsed.version !== 1 || !Array.isArray(parsed.entries) || parsed.entries.length > capacity) throw new Error("invalid journal");
36
+ for (const [key, value] of parsed.entries) {
37
+ if (typeof key !== "string" || !/^[a-f0-9]{64}$/.test(key) || !value || typeof value.sessionId !== "string" || !Number.isFinite(value.createdAt) || typeof value.fingerprint !== "string" || !validSendId(value.receipt?.clientSendId) || ![
38
+ "accepted",
39
+ "rejected",
40
+ "unknown"
41
+ ].includes(value.receipt.status)) throw new Error("invalid entry");
42
+ if (!/^[a-f0-9]{64}$/.test(value.fingerprint) || value.createdAt !== Number(value.receipt.clientSendId.slice(0, 13)) || value.receipt.status === "accepted" && !Number.isSafeInteger(value.receipt.userSeq) || value.receipt.status === "rejected" && ![
43
+ "E_BUSY",
44
+ "E_NOT_FOUND",
45
+ "E_PROTOCOL",
46
+ "E_UNSUPPORTED"
47
+ ].includes(value.receipt.code)) throw new Error("invalid receipt");
48
+ this.entries[key] = value;
49
+ }
50
+ } catch {
51
+ this.healthy = false;
52
+ }
53
+ }
54
+ key(deviceId, id) {
55
+ return createHash("sha256").update(JSON.stringify([deviceId, id])).digest("hex");
56
+ }
57
+ expired(id, now = Date.now()) {
58
+ const age = now - Number(id.slice(0, 13));
59
+ return age > 6048e5 || age < -3e5;
60
+ }
61
+ save() {
62
+ if (!this.path) return;
63
+ mkdirSync(dirname(this.path), {
64
+ recursive: true,
65
+ mode: 448
66
+ });
67
+ const temp = this.path + "." + randomUUID() + ".tmp";
68
+ const fd = openSync(temp, "wx", 384);
69
+ try {
70
+ writeFileSync(fd, JSON.stringify({
71
+ version: 1,
72
+ entries: Object.entries(this.entries)
73
+ }));
74
+ fsyncSync(fd);
75
+ } finally {
76
+ closeSync(fd);
77
+ }
78
+ renameSync(temp, this.path);
79
+ const dir = openSync(dirname(this.path), "r");
80
+ try {
81
+ fsyncSync(dir);
82
+ } finally {
83
+ closeSync(dir);
84
+ }
85
+ }
86
+ lookup(deviceId, sessionId, id) {
87
+ if (!this.healthy) return {
88
+ clientSendId: id,
89
+ status: "unknown"
90
+ };
91
+ const entry = this.entries[this.key(deviceId, id)];
92
+ if (entry && entry.sessionId === sessionId) return entry.receipt;
93
+ return {
94
+ clientSendId: id,
95
+ status: this.expired(id) ? "expired" : "notFound"
96
+ };
97
+ }
98
+ async dispatch(deviceId, sessionId, id, content, operation) {
99
+ const key = this.key(deviceId, id);
100
+ const fingerprint = createHash("sha256").update(JSON.stringify([sessionId, content])).digest("hex");
101
+ const existing = this.entries[key];
102
+ if (existing) {
103
+ if (existing.fingerprint !== fingerprint) return {
104
+ clientSendId: id,
105
+ status: "rejected",
106
+ code: "E_PROTOCOL"
107
+ };
108
+ return this.inFlight.get(key) ?? existing.receipt;
109
+ }
110
+ if (!this.healthy) return {
111
+ clientSendId: id,
112
+ status: "unknown"
113
+ };
114
+ if (this.expired(id)) return {
115
+ clientSendId: id,
116
+ status: "expired"
117
+ };
118
+ for (const [key, value] of Object.entries(this.entries)) if (Date.now() - value.createdAt > 6051e5 && !this.inFlight.has(key)) delete this.entries[key];
119
+ if (Object.keys(this.entries).length >= this.capacity) return {
120
+ clientSendId: id,
121
+ status: "rejected",
122
+ code: "E_BUSY"
123
+ };
124
+ const entry = {
125
+ fingerprint,
126
+ sessionId,
127
+ createdAt: Number(id.slice(0, 13)),
128
+ receipt: {
129
+ clientSendId: id,
130
+ status: "unknown"
131
+ }
132
+ };
133
+ this.entries[key] = entry;
134
+ try {
135
+ this.save();
136
+ } catch {
137
+ this.healthy = false;
138
+ return entry.receipt;
139
+ }
140
+ const run = (async () => {
141
+ try {
142
+ const result = await operation();
143
+ if (result.ok) entry.receipt = {
144
+ clientSendId: id,
145
+ status: "accepted",
146
+ userSeq: result.value
147
+ };
148
+ else if ([
149
+ "busy",
150
+ "not-found",
151
+ "invalid",
152
+ "unsupported"
153
+ ].includes(result.kind)) {
154
+ const code = {
155
+ busy: "E_BUSY",
156
+ "not-found": "E_NOT_FOUND",
157
+ invalid: "E_PROTOCOL",
158
+ unsupported: "E_UNSUPPORTED"
159
+ }[result.kind];
160
+ entry.receipt = {
161
+ clientSendId: id,
162
+ status: "rejected",
163
+ code
164
+ };
165
+ }
166
+ } catch {}
167
+ try {
168
+ this.save();
169
+ } catch {
170
+ this.healthy = false;
171
+ }
172
+ return entry.receipt;
173
+ })();
174
+ this.inFlight.set(key, run);
175
+ try {
176
+ return await run;
177
+ } finally {
178
+ this.inFlight.delete(key);
179
+ }
180
+ }
181
+ };
182
+ const journals = /* @__PURE__ */ new Map();
183
+ function openDeliveryJournal(path) {
184
+ if (!path) return new PromptDeliveryJournal();
185
+ let journal = journals.get(path);
186
+ if (!journal) {
187
+ journal = new PromptDeliveryJournal(path);
188
+ journals.set(path, journal);
189
+ }
190
+ return journal;
191
+ }
192
+ //#endregion
193
+ //#region src/request-validation.ts
194
+ /** Shape and resource limits for authenticated client requests. Unknown fields
195
+ * remain accepted for additive protocol-v2 compatibility. Business validation
196
+ * (supported models, pending choices, permissions) stays with its owner. */
197
+ function validateRequest(type, value) {
198
+ const empty = /* @__PURE__ */ new Set([
199
+ "c2s.ping",
200
+ "c2s.sessions.list",
201
+ "c2s.pending.list",
202
+ "c2s.workspaces.list",
203
+ "c2s.directory.pick"
204
+ ]);
205
+ if (value === void 0 && (type === "c2s.session.create" || type === "c2s.directory.list")) value = {};
206
+ if (value == null && empty.has(type)) return;
207
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "payload must be an object";
208
+ const p = value;
209
+ const text = (v, max = 4096, nonempty = true) => typeof v === "string" && v.length <= max && (!nonempty || v.trim().length > 0);
210
+ const optional = (key, check) => p[key] === void 0 || check(p[key]);
211
+ const integer = (v, min, max) => typeof v === "number" && Number.isSafeInteger(v) && v >= min && v <= max;
212
+ if (type.startsWith("c2s.session.") && type !== "c2s.session.create" && !text(p.sessionId)) return "invalid sessionId";
213
+ if (type === "c2s.session.create" && (!optional("workspaceId", (v) => text(v)) || !optional("cwd", (v) => text(v, 32768)) || p.workspaceId !== void 0 && p.cwd !== void 0)) return "invalid workspace selection";
214
+ if (type === "c2s.directory.list" && !optional("path", (v) => text(v, 32768, false))) return "invalid path";
215
+ if (type === "c2s.workspace.create" && !text(p.path, 32768)) return "invalid path";
216
+ if (type === "c2s.session.open" && !optional("tailCount", (v) => integer(v, 1, 1e4))) return "invalid tailCount";
217
+ if (type === "c2s.session.history" && (!integer(p.beforeSeq, 0, Number.MAX_SAFE_INTEGER) || !optional("limit", (v) => integer(v, 1, 500)))) return "invalid history range";
218
+ if (type === "c2s.session.rename" && !text(p.title, 4096)) return "invalid title";
219
+ if (type === "c2s.session.attachment" && !text(p.attachmentId)) return "invalid attachmentId";
220
+ if (type === "c2s.session.selectModel" && (!text(p.provider, 256) || !text(p.model, 1024) || !optional("reasoningEffort", (v) => text(v, 128, false)))) return "invalid model selection";
221
+ if (type === "c2s.session.delivery" && !validSendId(p.clientSendId)) return "invalid clientSendId";
222
+ if (type === "c2s.session.sendPrompt") {
223
+ if (p.clientSendId !== void 0 && !validSendId(p.clientSendId)) return "invalid clientSendId";
224
+ if (!optional("text", (v) => text(v, 262144, false)) || !optional("images", Array.isArray) || !optional("documents", Array.isArray)) return "invalid prompt fields";
225
+ for (const key of ["images", "documents"]) {
226
+ const items = p[key];
227
+ if (items && items.some((v) => v === null || typeof v !== "object" || Array.isArray(v))) return "invalid attachment";
228
+ for (const item of items ?? []) {
229
+ if (item.name !== void 0 && !text(item.name, 4096, false)) return "invalid attachment name";
230
+ if (item.truncated !== void 0 && typeof item.truncated !== "boolean") return "invalid truncated flag";
231
+ }
232
+ }
233
+ }
234
+ if (type === "c2s.approval.respond" || type === "c2s.question.respond") {
235
+ if (!text(p.requestId)) return "invalid requestId";
236
+ }
237
+ if (type === "c2s.approval.respond" && (!["allow", "deny"].includes(p.decision) || !optional("reason", (v) => text(v, 65536, false)))) return "invalid approval response";
238
+ if (type === "c2s.question.respond") {
239
+ if (!Array.isArray(p.answers) || p.answers.length > 100) return "invalid answers";
240
+ const ids = /* @__PURE__ */ new Set();
241
+ for (const answer of p.answers) {
242
+ if (!answer || typeof answer !== "object" || Array.isArray(answer) || !text(answer.id) || ids.has(answer.id) || !Array.isArray(answer.selected) || answer.selected.length > 100 || !answer.selected.every((v) => text(v, 4096)) || answer.custom !== void 0 && !text(answer.custom, 65536)) return "invalid answer";
243
+ ids.add(answer.id);
244
+ }
245
+ }
246
+ if (type === "c2s.push.register" || type === "c2s.widget.push.register") {
247
+ if (p.environment !== void 0 && p.environment !== "production" && p.environment !== "development") return "invalid APNs environment";
248
+ if (!optional("enrollKey", (v) => text(v, 128))) return "invalid enrollKey";
249
+ if (p.categories !== void 0 && (!p.categories || typeof p.categories !== "object" || Array.isArray(p.categories) || Object.values(p.categories).some((v) => typeof v !== "boolean"))) return "invalid categories";
250
+ }
251
+ }
252
+ //#endregion
18
253
  //#region src/device-auth.ts
19
254
  function getPairingCodeTtlMs() {
20
255
  return Number(process.env.DEEPPILOT_PAIRING_TTL_MS) || 3e5;
@@ -226,7 +461,8 @@ var DeviceStore = class DeviceStore {
226
461
  scopes: normalizeDeviceScopes(record.scopes),
227
462
  firstSeenTs: existing?.firstSeenTs ?? now,
228
463
  lastSeenTs: now,
229
- ...existing?.apns ? { apns: existing.apns } : {}
464
+ ...existing?.apns ? { apns: existing.apns } : {},
465
+ ...existing?.widgetApns ? { widgetApns: existing.widgetApns } : {}
230
466
  };
231
467
  this.devices.set(deviceId, next);
232
468
  this.flush();
@@ -251,6 +487,7 @@ var DeviceStore = class DeviceStore {
251
487
  if (!record || record.revokedAt !== void 0) return false;
252
488
  record.revokedAt = now;
253
489
  delete record.apns;
490
+ delete record.widgetApns;
254
491
  this.flush();
255
492
  return true;
256
493
  }
@@ -296,6 +533,25 @@ var DeviceStore = class DeviceStore {
296
533
  delete record.apns;
297
534
  this.flush();
298
535
  }
536
+ setWidgetPushToken(deviceId, token, environment, now) {
537
+ const record = this.authorized(deviceId);
538
+ if (!record || !isValidApnsToken(token)) return;
539
+ const previous = record.widgetApns;
540
+ if (previous?.token === token && previous.environment === environment && now - previous.updatedAt < 36e5) return;
541
+ record.widgetApns = {
542
+ token,
543
+ environment,
544
+ updatedAt: now
545
+ };
546
+ this.flush();
547
+ }
548
+ /** Compare-and-clear protects a rotated token from a delayed APNs rejection. */
549
+ clearWidgetPushToken(deviceId, token) {
550
+ const record = this.devices.get(deviceId);
551
+ if (record?.widgetApns?.token !== token) return;
552
+ delete record.widgetApns;
553
+ this.flush();
554
+ }
299
555
  /** Serialized so concurrent touches can never interleave half-written JSON. */
300
556
  flush() {
301
557
  const next = this.flushTail.then(() => this.writeFile());
@@ -332,14 +588,54 @@ const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
332
588
  function sanitizeDeviceField(value, maxChars) {
333
589
  return (typeof value === "string" ? value : String(value ?? "")).replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, maxChars);
334
590
  }
591
+ /**
592
+ * Runtime shape guard for a frame after JSON.parse. The old `as Envelope`
593
+ * cast alone let JSON `null` reach `env.v` and let a missing or mistyped
594
+ * `type` reach `requiredScope()`'s string operations — one anonymous frame
595
+ * could crash the host process. Reject anything that is not a plain object
596
+ * with a numeric version and a non-empty string type, so field access below
597
+ * is always safe. The payload is intentionally opaque here; each handler
598
+ * validates its own payload shape.
599
+ */
600
+ function isEnvelope(value) {
601
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
602
+ const frame = value;
603
+ if (typeof frame.v !== "number") return false;
604
+ if (typeof frame.type !== "string" || frame.type.length === 0) return false;
605
+ if (frame.id !== void 0 && typeof frame.id !== "string") return false;
606
+ if (frame.ts !== void 0 && typeof frame.ts !== "number") return false;
607
+ if (frame.seq !== void 0 && typeof frame.seq !== "number") return false;
608
+ return true;
609
+ }
335
610
  function requiredScope(type) {
611
+ if (typeof type !== "string") return void 0;
336
612
  if (type === "c2s.ping" || type === "c2s.resume") return void 0;
337
- if (type === "c2s.session.sendPrompt") return "prompt.send";
613
+ if (type === "c2s.session.sendPrompt" || type === "c2s.session.delivery") return "prompt.send";
614
+ if (type === "c2s.pending.list") return "interactions.respond";
338
615
  if (type === "c2s.approval.respond" || type === "c2s.question.respond") return "interactions.respond";
339
- if (type === "c2s.push.register") return "notifications.register";
616
+ if (type === "c2s.push.register" || type === "c2s.widget.push.register") return "notifications.register";
340
617
  if (type === "c2s.workspace.create" || type === "c2s.session.create" || type === "c2s.session.rename" || type === "c2s.session.archive" || type === "c2s.session.cancel" || type === "c2s.session.selectModel") return "sessions.manage";
341
618
  if (type.startsWith("c2s.")) return "sessions.read";
342
619
  }
620
+ /** Broadcast and replay authorization. Unknown frame types fail closed. */
621
+ const PUSH_SCOPE_BY_TYPE = {
622
+ "s2c.session.event": "sessions.read",
623
+ "s2c.sessions.delta": "sessions.read",
624
+ "s2c.session.tail": "sessions.read",
625
+ "s2c.history.page": "sessions.read",
626
+ "s2c.pending.approval": "interactions.respond",
627
+ "s2c.pending.question": "interactions.respond",
628
+ "s2c.pending.cleared": "interactions.respond"
629
+ };
630
+ function pushScopeFor(type, payload) {
631
+ const fixed = PUSH_SCOPE_BY_TYPE[type];
632
+ if (fixed !== void 0) return fixed;
633
+ if (type === "s2c.notify") {
634
+ const category = payload?.category;
635
+ if (category === "approval.required" || category === "question.asked") return "interactions.respond";
636
+ if (category === "turn.completed" || category === "session.error") return "sessions.read";
637
+ }
638
+ }
343
639
  function sanitizeImageName(value) {
344
640
  return value.replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, 120);
345
641
  }
@@ -396,13 +692,18 @@ var BridgeConnection = class {
396
692
  /** Sanitized device identity from hello; needed for push registration. */
397
693
  deviceId;
398
694
  scopes = /* @__PURE__ */ new Set();
695
+ widgetClient = false;
399
696
  authChallenge;
400
697
  constructor(ws, deps) {
401
698
  this.ws = ws;
402
699
  this.deps = deps;
403
700
  this.authChallenge = createAuthChallenge(deps.audience);
404
701
  ws.on("message", (data) => {
405
- this.onMessage(String(data));
702
+ this.onMessage(String(data)).catch((error) => {
703
+ if (this.deps.debug === true) this.deps.log("frame handler failed: " + String(error));
704
+ if (this.closed) return;
705
+ this.terminate();
706
+ });
406
707
  });
407
708
  ws.on("close", () => {
408
709
  this.onClose();
@@ -439,7 +740,19 @@ var BridgeConnection = class {
439
740
  get connectedDeviceId() {
440
741
  return this.authenticated ? this.deviceId : void 0;
441
742
  }
743
+ get suppressesAlertPush() {
744
+ return !this.widgetClient;
745
+ }
746
+ /** S→C permission gate consulted by the bridge for every broadcast/replay
747
+ * frame: a device only receives what its scopes grant (R1/P2). */
748
+ canReceive(scope) {
749
+ return this.scopes.has(scope);
750
+ }
442
751
  push(type, payload, seq) {
752
+ if (type === "s2c.notify") payload = {
753
+ ...payload,
754
+ hostAudience: this.deps.audience
755
+ };
443
756
  if (type === "s2c.session.event") {
444
757
  const sessionId = payload?.sessionId;
445
758
  if (typeof sessionId === "string") {
@@ -528,7 +841,12 @@ var BridgeConnection = class {
528
841
  }
529
842
  let env;
530
843
  try {
531
- env = JSON.parse(raw);
844
+ const parsed = JSON.parse(raw);
845
+ if (!isEnvelope(parsed)) {
846
+ this.fail(void 0, "E_PROTOCOL", "malformed frame");
847
+ return;
848
+ }
849
+ env = parsed;
532
850
  } catch {
533
851
  this.fail(void 0, "E_PROTOCOL", "frame is not valid JSON");
534
852
  return;
@@ -550,11 +868,19 @@ var BridgeConnection = class {
550
868
  this.fail(env.id, "E_PROTOCOL", "authenticate first");
551
869
  return;
552
870
  }
871
+ if (this.widgetClient && ![
872
+ "c2s.ping",
873
+ "c2s.sessions.list",
874
+ "c2s.pending.list",
875
+ "c2s.widget.push.register"
876
+ ].includes(env.type)) return this.fail(env.id, "E_FORBIDDEN", "widget connection is read-only");
553
877
  const required = requiredScope(env.type);
554
878
  if (required !== void 0 && !this.scopes.has(required)) {
555
879
  this.fail(env.id, "E_FORBIDDEN", `scope ${required} required`);
556
880
  return;
557
881
  }
882
+ const invalid = validateRequest(env.type, env.payload);
883
+ if (invalid) return this.fail(env.id, "E_PROTOCOL", invalid);
558
884
  switch (env.type) {
559
885
  case "c2s.ping":
560
886
  this.send("s2c.pong", { serverTime: Date.now() }, env.id);
@@ -727,6 +1053,11 @@ var BridgeConnection = class {
727
1053
  }, env.id);
728
1054
  return;
729
1055
  }
1056
+ case "c2s.session.delivery": {
1057
+ const p = env.payload;
1058
+ this.send("s2c.ack", this.deps.bridge.promptDeliveries.lookup(this.deviceId, p.sessionId, p.clientSendId), env.id);
1059
+ return;
1060
+ }
730
1061
  case "c2s.session.sendPrompt": {
731
1062
  const p = env.payload;
732
1063
  const text = typeof p?.text === "string" ? p.text : "";
@@ -758,6 +1089,15 @@ var BridgeConnection = class {
758
1089
  ...document.truncated === true ? { truncated: true } : {}
759
1090
  });
760
1091
  }
1092
+ if (p.clientSendId) {
1093
+ const receipt = await this.deps.bridge.promptDeliveries.dispatch(this.deviceId, p.sessionId, p.clientSendId, {
1094
+ text,
1095
+ images,
1096
+ documents
1097
+ }, () => this.deps.bridge.sendPrompt(p.sessionId, text, images, documents));
1098
+ this.send("s2c.ack", receipt, env.id);
1099
+ return;
1100
+ }
761
1101
  const userSeq = await this.deps.bridge.sendPrompt(p.sessionId, text, images, documents);
762
1102
  if (!userSeq.ok) return this.fail(env.id, managementErrorCode(userSeq.kind), userSeq.message);
763
1103
  this.send("s2c.ack", { userSeq: userSeq.value }, env.id);
@@ -779,7 +1119,9 @@ var BridgeConnection = class {
779
1119
  this.send("s2c.ack", {}, env.id);
780
1120
  return;
781
1121
  }
1122
+ case "c2s.widget.push.register":
782
1123
  case "c2s.push.register": {
1124
+ const widget = env.type === "c2s.widget.push.register";
783
1125
  const p = env.payload;
784
1126
  const token = typeof p?.deviceToken === "string" ? p.deviceToken.trim() : "";
785
1127
  if (!isValidApnsToken(token)) return this.fail(env.id, "E_PROTOCOL", "hex deviceToken (32-512 chars) required");
@@ -790,7 +1132,12 @@ var BridgeConnection = class {
790
1132
  const enrollKey = p.enrollKey.trim().replace(/[^\x20-\x7e]/g, "").slice(0, 128);
791
1133
  if (enrollKey.length >= 8 && enrollKey.length <= 128) await this.deps.onPushEnrollKey?.(enrollKey);
792
1134
  }
793
- this.deps.devices.setPushToken(this.deviceId, token, environment, categories, Date.now());
1135
+ const record = this.deps.devices.authorized(this.deviceId);
1136
+ if (this.closed || !record?.scopes?.includes("notifications.register")) return this.fail(env.id, "E_FORBIDDEN", "device authorization changed");
1137
+ if (widget) {
1138
+ if (!record.scopes.includes("sessions.read") || !record.scopes.includes("interactions.respond")) return this.fail(env.id, "E_FORBIDDEN", "widget overview permissions required");
1139
+ this.deps.devices.setWidgetPushToken(this.deviceId, token, environment, Date.now());
1140
+ } else this.deps.devices.setPushToken(this.deviceId, token, environment, categories, Date.now());
794
1141
  if (!this.deps.bridge.capabilities.push) {
795
1142
  if (this.deps.debug === true) this.deps.log("push register held: bridge not ready");
796
1143
  return this.fail(env.id, "E_UNSUPPORTED", "push is not configured on this bridge");
@@ -836,11 +1183,12 @@ var BridgeConnection = class {
836
1183
  this.settleAuthentication(true, "success");
837
1184
  this.authenticated = true;
838
1185
  this.deviceId = deviceId;
1186
+ this.widgetClient = p.clientRole === "widget";
839
1187
  this.scopes = new Set(record.scopes ?? []);
840
1188
  if (this.helloTimer !== void 0) clearTimeout(this.helloTimer);
841
- this.deps.devices.markAuthenticated(deviceId, deviceName, appVersion, Date.now());
1189
+ if (!this.widgetClient) this.deps.devices.markAuthenticated(deviceId, deviceName, appVersion, Date.now());
842
1190
  this.deps.onDeviceAuthenticated?.(deviceId);
843
- const cursor = resumeCursor;
1191
+ const cursor = this.widgetClient ? void 0 : resumeCursor;
844
1192
  const canResume = cursor !== void 0 && this.deps.bridge.canResumeFrom(cursor);
845
1193
  this.send("s2c.welcome", {
846
1194
  protocolVersion: 2,
@@ -851,7 +1199,7 @@ var BridgeConnection = class {
851
1199
  cursor: this.deps.bridge.currentCursor(),
852
1200
  resumed: canResume
853
1201
  }, env.id);
854
- this.deps.bridge.addSink(this);
1202
+ if (!this.widgetClient) this.deps.bridge.addSink(this);
855
1203
  if (cursor !== void 0) {
856
1204
  if (canResume) this.deps.bridge.resumeFrom(cursor, this);
857
1205
  else this.resync();
@@ -1401,11 +1749,14 @@ var HostBridge = class {
1401
1749
  abort = new AbortController();
1402
1750
  started = false;
1403
1751
  disposed = false;
1404
- constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT) {
1752
+ promptDeliveries;
1753
+ constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath) {
1405
1754
  this.apiProxy = apiProxy;
1406
1755
  this.historyBufferMax = historyBufferMax;
1756
+ this.promptDeliveries = openDeliveryJournal(deliveryJournalPath);
1407
1757
  }
1408
1758
  pushOutlet;
1759
+ widgetFingerprint = "";
1409
1760
  /**
1410
1761
  * Wire the offline-push fan-out. Present ⇒ welcome advertises the `push`
1411
1762
  * capability and notify-worthy events are mirrored to APNs.
@@ -1420,11 +1771,13 @@ var HostBridge = class {
1420
1771
  approvals: true,
1421
1772
  questions: true,
1422
1773
  pendingSnapshot: true,
1774
+ promptDelivery: true,
1423
1775
  notifyAllCategories: true,
1424
1776
  models: typeof this.apiProxy.sessions.models === "function" && typeof this.apiProxy.sessions.selectModel === "function",
1425
1777
  sessionManagement: typeof this.apiProxy.sessions.rename === "function" && typeof this.apiProxy.workspace?.archiveSession === "function",
1426
1778
  projectSelection: typeof this.apiProxy.workspace?.list === "function" && typeof this.apiProxy.workspace?.create === "function",
1427
- push: this.pushOutlet?.isAvailable() === true
1779
+ push: this.pushOutlet?.isAvailable() === true,
1780
+ widgetPush: true
1428
1781
  };
1429
1782
  }
1430
1783
  diagnostic(message) {
@@ -1521,17 +1874,45 @@ var HostBridge = class {
1521
1874
  * Replay buffered pushes after the given cursor; false when the gap is
1522
1875
  * unrecoverable. Frames go to `target` only — replaying into every sink
1523
1876
  * duplicated the whole window onto devices that never asked for it.
1877
+ * Each frame is filtered by the S→C permission policy per sink, so a
1878
+ * reader without interactions.respond never gets the missed approval
1879
+ * frames back (R1/P2).
1524
1880
  */
1525
1881
  resumeFrom(cursor, target) {
1526
1882
  const oldest = this.ring.length > 0 ? this.ring[0].seq : this.cursor + 1;
1527
1883
  if (cursor + 1 < oldest) return false;
1528
1884
  const receivers = target !== void 0 ? [target] : [...this.sinks];
1529
- for (const entry of this.ring) if (entry.seq > cursor) for (const sink of receivers) sink.replay([entry]);
1885
+ for (const entry of this.ring) if (entry.seq > cursor) for (const sink of receivers) {
1886
+ const need = pushScopeFor(entry.type, entry.payload);
1887
+ if (need === void 0 || !sink.canReceive(need)) continue;
1888
+ sink.replay([entry]);
1889
+ }
1530
1890
  for (const sink of receivers) sink.replayDone();
1531
1891
  return true;
1532
1892
  }
1533
1893
  record(type, payload, except) {
1534
1894
  if (this.disposed) return;
1895
+ if (type === "s2c.sessions.delta" || type.startsWith("s2c.pending.")) {
1896
+ const fingerprint = JSON.stringify([
1897
+ this.listSessions().map(({ id, title, status, lastActivityTs, todos, pendingApproval, pendingQuestion }) => ({
1898
+ id,
1899
+ title,
1900
+ status,
1901
+ lastActivityTs,
1902
+ todos,
1903
+ pendingApproval,
1904
+ pendingQuestion
1905
+ })),
1906
+ this.approvals.size,
1907
+ this.questions.size
1908
+ ]);
1909
+ if (fingerprint !== this.widgetFingerprint) {
1910
+ this.widgetFingerprint = fingerprint;
1911
+ try {
1912
+ this.pushOutlet?.widgetChanged?.();
1913
+ } catch {}
1914
+ }
1915
+ }
1535
1916
  this.cursor += 1;
1536
1917
  const entry = {
1537
1918
  seq: this.cursor,
@@ -1540,7 +1921,9 @@ var HostBridge = class {
1540
1921
  };
1541
1922
  this.ring.push(entry);
1542
1923
  if (this.ring.length > this.historyBufferMax) this.ring.splice(0, this.ring.length - this.historyBufferMax);
1924
+ const need = pushScopeFor(type, payload);
1543
1925
  for (const sink of this.sinks) {
1926
+ if (need === void 0 || !sink.canReceive(need)) continue;
1544
1927
  if (except && except(sink)) continue;
1545
1928
  sink.push(type, payload, entry.seq);
1546
1929
  }
@@ -1782,6 +2165,14 @@ var HostBridge = class {
1782
2165
  } else if (key === "sessionListMetadata") {
1783
2166
  const meta = value;
1784
2167
  if (meta?.lastPromptAt) row.lastActivityTs = Math.max(row.lastActivityTs, meta.lastPromptAt);
2168
+ } else if (key === "sessionStats" || key === "tokenUsage") {
2169
+ const patch = sanitizeUsageStatsPatch(key, value);
2170
+ if (!patch) return;
2171
+ row.stats = {
2172
+ ...emptyUsageStats,
2173
+ ...row.stats ?? {},
2174
+ ...patch
2175
+ };
1785
2176
  } else return;
1786
2177
  this.pushSummary(row);
1787
2178
  }
@@ -2415,6 +2806,65 @@ function sanitizeTodoItems(items) {
2415
2806
  status: i.status
2416
2807
  }));
2417
2808
  }
2809
+ /** Zero-value stats snapshot used as the merge base for partial patches. */
2810
+ const emptyUsageStats = {
2811
+ turns: 0,
2812
+ steps: 0,
2813
+ llmMs: 0,
2814
+ toolMs: 0,
2815
+ ttftMs: 0,
2816
+ ttftSteps: 0,
2817
+ decodeMs: 0,
2818
+ decodeTokens: 0,
2819
+ inputTokens: 0,
2820
+ outputTokens: 0,
2821
+ cacheReadTokens: 0,
2822
+ cacheWriteTokens: 0
2823
+ };
2824
+ /** Coerce one host counter to a non-negative integer; junk becomes 0. */
2825
+ function usageCounter(value) {
2826
+ const n = typeof value === "number" ? value : Number(value);
2827
+ if (!Number.isFinite(n) || n < 0) return 0;
2828
+ return Math.floor(n);
2829
+ }
2830
+ const SESSION_STATS_COUNTERS = [
2831
+ "turns",
2832
+ "steps",
2833
+ "llmMs",
2834
+ "toolMs",
2835
+ "ttftMs",
2836
+ "ttftSteps",
2837
+ "decodeMs",
2838
+ "decodeTokens"
2839
+ ];
2840
+ const TOKEN_USAGE_COUNTERS = [
2841
+ "outputTokens",
2842
+ "cacheReadTokens",
2843
+ "cacheWriteTokens"
2844
+ ];
2845
+ /** Sanitize one `sessionStats` / `tokenUsage` projection value into the wire
2846
+ * stats fields; undefined when the payload carries nothing readable. Host
2847
+ * field names differ between the two projections (tokenUsage reports
2848
+ * `uncachedInputTokens`, the wire mirrors it as `inputTokens`). */
2849
+ function sanitizeUsageStatsPatch(key, value) {
2850
+ if (!value || typeof value !== "object") return void 0;
2851
+ const raw = value;
2852
+ const patch = {};
2853
+ if (key === "sessionStats") {
2854
+ for (const field of SESSION_STATS_COUNTERS) if (field in raw) patch[field] = usageCounter(raw[field]);
2855
+ } else {
2856
+ if ("uncachedInputTokens" in raw) patch.inputTokens = usageCounter(raw.uncachedInputTokens);
2857
+ for (const field of TOKEN_USAGE_COUNTERS) if (field in raw) patch[field] = usageCounter(raw[field]);
2858
+ }
2859
+ return Object.keys(patch).length > 0 ? patch : void 0;
2860
+ }
2861
+ /** Combine one session's `sessionStats` + `tokenUsage` projection values into
2862
+ * a complete wire stats object; undefined when neither carries anything. */
2863
+ function usageStatsOf(sessionStats, tokenUsage) {
2864
+ const patches = [sanitizeUsageStatsPatch("sessionStats", sessionStats), sanitizeUsageStatsPatch("tokenUsage", tokenUsage)].filter((patch) => patch !== void 0);
2865
+ if (patches.length === 0) return void 0;
2866
+ return Object.assign({}, emptyUsageStats, ...patches);
2867
+ }
2418
2868
  function toSummary(row, approvals, questions, workspace) {
2419
2869
  const values = row.projections?.values ?? {};
2420
2870
  const todos = Array.isArray(values.todos) ? values.todos : null;
@@ -2425,6 +2875,7 @@ function toSummary(row, approvals, questions, workspace) {
2425
2875
  const cwd = typeof row.cwd === "string" ? row.cwd : "";
2426
2876
  const label = workspace?.title ?? (cwd ? cwd.split("/").filter(Boolean).pop() : void 0);
2427
2877
  const todoItems = sanitizeTodoItems(todos);
2878
+ const stats = usageStatsOf(values.sessionStats, values.tokenUsage);
2428
2879
  return {
2429
2880
  id: row.sessionId,
2430
2881
  title: typeof values.title === "string" ? values.title : "",
@@ -2437,6 +2888,7 @@ function toSummary(row, approvals, questions, workspace) {
2437
2888
  todoItems: todoItems.length > 0 ? todoItems : null,
2438
2889
  pendingApproval,
2439
2890
  pendingQuestion,
2891
+ ...stats ? { stats } : {},
2440
2892
  workspaceLabel: label ?? null,
2441
2893
  workspaceId: workspace?.workspaceId ?? null,
2442
2894
  workspacePath: workspace?.path ?? (cwd || null)
@@ -3642,6 +4094,10 @@ function p8ToDer(pem) {
3642
4094
  }
3643
4095
  /** Pure payload builder so tests can assert the wire format without sockets. */
3644
4096
  function apnsPayload(notification) {
4097
+ if ("kind" in notification && notification.kind === "widget") return { aps: { "content-changed": true } };
4098
+ return alertPayload(notification);
4099
+ }
4100
+ function alertPayload(notification) {
3645
4101
  const timeSensitive = notification.category === "approval.required" || notification.category === "question.asked";
3646
4102
  return {
3647
4103
  aps: {
@@ -3651,17 +4107,32 @@ function apnsPayload(notification) {
3651
4107
  },
3652
4108
  sound: "default",
3653
4109
  category: notification.category,
3654
- "thread-id": notification.sessionId.slice(0, 64),
4110
+ "thread-id": notification.hostAudience ? createHash("sha256").update(`${notification.hostAudience}:${notification.sessionId}`).digest("hex") : notification.sessionId.slice(0, 64),
3655
4111
  ...timeSensitive ? { "interruption-level": "time-sensitive" } : {}
3656
4112
  },
4113
+ ...notification.hostAudience ? { hostAudience: notification.hostAudience } : {},
3657
4114
  sessionId: notification.sessionId,
3658
4115
  notificationId: notification.notificationId,
3659
4116
  kind: notification.category
3660
4117
  };
3661
4118
  }
4119
+ function pushHeaders(bundleId, notification) {
4120
+ if ("kind" in notification && notification.kind === "widget") return {
4121
+ "apns-topic": bundleId + ".push-type.widgets",
4122
+ "apns-push-type": "widgets",
4123
+ "apns-priority": "5",
4124
+ "apns-collapse-id": "widget-overview"
4125
+ };
4126
+ return {
4127
+ "apns-topic": bundleId,
4128
+ "apns-push-type": "alert",
4129
+ "apns-priority": "10",
4130
+ "apns-collapse-id": collapseIdFor(notification)
4131
+ };
4132
+ }
3662
4133
  /** collapse-id accepts ≤64 bytes of ASCII; keep it stable per session+event. */
3663
4134
  function collapseIdFor(notification) {
3664
- const raw = `${notification.category}:${notification.sessionId}`;
4135
+ const raw = `${notification.hostAudience ? notification.hostAudience + ":" : ""}${notification.category}:${notification.sessionId}`;
3665
4136
  const readable = raw.replace(/[^a-zA-Z0-9.:-]/g, "");
3666
4137
  const digest = createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 12);
3667
4138
  return `${readable.slice(0, 51)}:${digest}`;
@@ -3738,11 +4209,8 @@ var ApnsClient = class {
3738
4209
  [":method"]: "POST",
3739
4210
  [":path"]: "/3/device/" + deviceToken,
3740
4211
  authorization: "bearer " + token,
3741
- "apns-topic": this.opts.bundleId,
3742
- "apns-push-type": "alert",
3743
- "apns-priority": "10",
4212
+ ...pushHeaders(this.opts.bundleId, notification),
3744
4213
  "apns-expiration": String(Math.floor(Date.now() / 1e3) + 3600),
3745
- "apns-collapse-id": collapseIdFor(notification),
3746
4214
  "content-type": "application/json",
3747
4215
  "content-length": String(Buffer.byteLength(body))
3748
4216
  });
@@ -4608,6 +5076,7 @@ const Config = z.object({
4608
5076
  "apns",
4609
5077
  "relay"
4610
5078
  ]).default("none"),
5079
+ contentMode: z.union(["preview", "generic"]).default("preview"),
4611
5080
  teamId: z.string().default(""),
4612
5081
  keyId: z.string().default(""),
4613
5082
  keyPath: z.string().default(join(bridgeDataDir(), "apns", "AuthKey.p8")),
@@ -4616,6 +5085,7 @@ const Config = z.object({
4616
5085
  relayToken: z.string().default("")
4617
5086
  }).default({
4618
5087
  provider: "none",
5088
+ contentMode: "preview",
4619
5089
  teamId: "",
4620
5090
  keyId: "",
4621
5091
  keyPath: join(bridgeDataDir(), "apns", "AuthKey.p8"),
@@ -4793,6 +5263,11 @@ var AuthRateLimiter = class {
4793
5263
  function shouldPrunePushToken(outcome, reason) {
4794
5264
  return outcome === "invalid-token" && (reason === "Unregistered" || reason === "ExpiredToken");
4795
5265
  }
5266
+ /** APNs requires both registration and the same content permission as WS. */
5267
+ function mayReceivePush(device, notification) {
5268
+ const scope = pushScopeFor("s2c.notify", notification);
5269
+ return scope !== void 0 && device.scopes?.includes("notifications.register") === true && device.scopes.includes(scope);
5270
+ }
4796
5271
  /**
4797
5272
  * Zero-touch relay self-heal: HTTP 401 means the relay no longer honors the
4798
5273
  * cached credential. Only auto-enrolled cells with a still-current token may
@@ -4802,6 +5277,59 @@ function shouldPrunePushToken(outcome, reason) {
4802
5277
  function shouldReEnrollRelayToken(transport, outcome, reason, opts) {
4803
5278
  return transport === "relay" && outcome === "failed" && reason === "HTTP 401" && opts.hasEnrollKey && opts.usedCellToken && opts.tokenStillCurrent;
4804
5279
  }
5280
+ /** Apply before either transport receives the payload, including the Relay. */
5281
+ function pushContent(notification, mode) {
5282
+ if (mode !== "generic") return notification;
5283
+ const bodies = {
5284
+ "turn.completed": "A task has completed.",
5285
+ "approval.required": "An approval needs your attention.",
5286
+ "question.asked": "A question needs your answer.",
5287
+ "session.error": "A task needs your attention."
5288
+ };
5289
+ return {
5290
+ ...notification,
5291
+ title: "DeepPilot",
5292
+ body: bodies[notification.category] ?? "Open DeepPilot for an update."
5293
+ };
5294
+ }
5295
+ //#endregion
5296
+ //#region src/widget-push.ts
5297
+ /** Coalesce bursts and bound continuous updates, without starving the trailing
5298
+ * state. One scheduler per host, not per token or per streaming text frame. */
5299
+ var WidgetPushScheduler = class {
5300
+ send;
5301
+ intervalMs;
5302
+ timer;
5303
+ lastSent = 0;
5304
+ disposed = false;
5305
+ sending = false;
5306
+ dirty = false;
5307
+ constructor(send, intervalMs = 3e4) {
5308
+ this.send = send;
5309
+ this.intervalMs = intervalMs;
5310
+ }
5311
+ changed() {
5312
+ if (this.disposed) return;
5313
+ this.dirty = true;
5314
+ if (this.timer || this.sending) return;
5315
+ this.timer = setTimeout(() => {
5316
+ this.timer = void 0;
5317
+ this.dirty = false;
5318
+ this.sending = true;
5319
+ this.lastSent = Date.now();
5320
+ this.send().catch(() => {}).finally(() => {
5321
+ this.sending = false;
5322
+ if (this.dirty) this.changed();
5323
+ });
5324
+ }, Math.max(Math.min(1e3, this.intervalMs), this.intervalMs - (Date.now() - this.lastSent)));
5325
+ this.timer.unref();
5326
+ }
5327
+ dispose() {
5328
+ this.disposed = true;
5329
+ if (this.timer) clearTimeout(this.timer);
5330
+ this.timer = void 0;
5331
+ }
5332
+ };
4805
5333
  //#endregion
4806
5334
  //#region src/phone-server.ts
4807
5335
  /**
@@ -5250,20 +5778,59 @@ function apply(ctx, options) {
5250
5778
  * token. Rules:
5251
5779
  * - devices with a live WebSocket are skipped (they already got the WS
5252
5780
  * frame and will raise the local notification themselves);
5781
+ * - only devices granted `notifications.register` are candidates — a
5782
+ * device whose scope was revoked must not receive offline pushes
5783
+ * (R1/P2 S→C permission policy);
5253
5784
  * - each device is delivered on ITS registered environment (the build
5254
5785
  * kind it self-reported), so sandbox and production devices coexist;
5255
5786
  * - the device's per-category switches suppress muted categories;
5256
5787
  * - only APNs' terminal Unregistered/ExpiredToken verdicts prune storage;
5257
5788
  * BadDeviceToken may be an environment mismatch and stays diagnosable.
5258
5789
  */
5790
+ const widgetPush = new WidgetPushScheduler(async () => {
5791
+ if (!enabledNow()) return;
5792
+ const resolved = resolvePushConfig(currentConfig());
5793
+ if (!resolved.ok) return;
5794
+ const devices = auth.devices;
5795
+ const send = await senderFor(resolved.value);
5796
+ if (!devices || !send) return;
5797
+ const sent = /* @__PURE__ */ new Set();
5798
+ for (const device of devices.list()) {
5799
+ const registration = device.widgetApns;
5800
+ if (!registration || device.revokedAt !== void 0 || ![
5801
+ "notifications.register",
5802
+ "sessions.read",
5803
+ "interactions.respond"
5804
+ ].every((scope) => device.scopes?.includes(scope)) || Date.now() - registration.updatedAt > 6048e5) continue;
5805
+ const key = registration.environment + ":" + registration.token;
5806
+ if (sent.has(key)) continue;
5807
+ sent.add(key);
5808
+ const { outcome, reason } = await send({
5809
+ deviceToken: registration.token,
5810
+ environment: registration.environment,
5811
+ notification: { kind: "widget" }
5812
+ });
5813
+ if (shouldPrunePushToken(outcome, reason)) devices.clearWidgetPushToken(device.deviceId, registration.token);
5814
+ if (resolved.value.kind === "relay" && reason === "HTTP 401" && enrollmentCell.token === resolved.value.token && enrollmentCell.enrollKey) {
5815
+ enrollmentCell.token = void 0;
5816
+ persistEnrollment();
5817
+ await ensureRelayEnrolled(resolved.value.url);
5818
+ }
5819
+ }
5820
+ });
5259
5821
  const makePushOutlet = () => ({
5822
+ widgetChanged: () => widgetPush.changed(),
5260
5823
  isAvailable: () => {
5261
5824
  const resolved = resolvePushConfig(currentConfig());
5262
5825
  if (!resolved.ok) return false;
5263
5826
  if (resolved.value.kind === "relay" && !resolved.value.token) return false;
5264
5827
  return true;
5265
5828
  },
5266
- fanOut: (notification) => {
5829
+ fanOut: (sourceNotification) => {
5830
+ const notification = pushContent({
5831
+ ...sourceNotification,
5832
+ hostAudience: auth.audience ?? void 0
5833
+ }, currentConfig().push?.contentMode);
5267
5834
  (async () => {
5268
5835
  let resolved = resolvePushConfig(currentConfig());
5269
5836
  if (!resolved.ok && resolved.reason === "relay token not enrolled yet") {
@@ -5280,12 +5847,16 @@ function apply(ctx, options) {
5280
5847
  const connectedIds = /* @__PURE__ */ new Set();
5281
5848
  for (const connection of connections) {
5282
5849
  const id = connection.connectedDeviceId;
5283
- if (id) connectedIds.add(id);
5850
+ if (id && connection.suppressesAlertPush) connectedIds.add(id);
5284
5851
  }
5285
5852
  const candidates = devices.list().filter((device) => {
5286
5853
  const registration = device.apns;
5287
5854
  if (!registration) return false;
5288
5855
  if (connectedIds.has(device.deviceId)) return false;
5856
+ if (!mayReceivePush(device, notification)) {
5857
+ if (currentConfig().debug === true) log(`push skip "${device.deviceName}": notification permission not granted`);
5858
+ return false;
5859
+ }
5289
5860
  if (registration.categories?.[notification.category] === false) {
5290
5861
  if (currentConfig().debug === true) log(`push skip "${device.deviceName}": category ${notification.category} muted`);
5291
5862
  return false;
@@ -5840,7 +6411,7 @@ function apply(ctx, options) {
5840
6411
  log("dsh 0.1.2 session bridge unavailable: " + String(error));
5841
6412
  return;
5842
6413
  }
5843
- const bridge = new HostBridge(proxy, cfg.historyBufferMax);
6414
+ const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"));
5844
6415
  bridge.setPushOutlet(makePushOutlet());
5845
6416
  state.bridge = bridge;
5846
6417
  bridge.start();
@@ -5883,6 +6454,7 @@ function apply(ctx, options) {
5883
6454
  const sender = cachedSender;
5884
6455
  cachedSender = void 0;
5885
6456
  updateChecker.dispose();
6457
+ widgetPush.dispose();
5886
6458
  const wssClosed = new Promise((resolve) => wss.close(() => resolve()));
5887
6459
  await Promise.allSettled([
5888
6460
  enrollmentWriteTail,
@@ -5892,6 +6464,6 @@ function apply(ctx, options) {
5892
6464
  }, "deeppilot: process resources");
5893
6465
  }
5894
6466
  //#endregion
5895
- export { Config, HostBridge, apply, inject, name, shouldPrunePushToken, shouldReEnrollRelayToken };
6467
+ export { Config, HostBridge, apply, inject, mayReceivePush, name, shouldPrunePushToken, shouldReEnrollRelayToken };
5896
6468
 
5897
6469
  //# sourceMappingURL=index.js.map