dsh-deeppilot 0.6.0 → 0.6.2
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/COMPATIBILITY.md +4 -4
- package/PRIVACY.md +23 -1
- package/README.md +19 -19
- package/README.zh-CN.md +17 -17
- package/lib/client.js +31 -6
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +102 -2
- package/lib/index.js +785 -23
- package/lib/index.js.map +1 -1
- package/package.json +16 -16
package/lib/index.js
CHANGED
|
@@ -3,18 +3,257 @@ 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.liveActivity.register" || type === "c2s.liveActivity.unregister") {
|
|
247
|
+
if (!text(p.activityId, 128)) return "invalid activityId";
|
|
248
|
+
if (type === "c2s.liveActivity.register" && (!text(p.sessionId) || typeof p.deviceToken !== "string" || !/^[0-9a-fA-F]{32,512}$/.test(p.deviceToken) || !["development", "production"].includes(p.environment) || !optional("enrollKey", (v) => text(v, 128)))) return "invalid live activity registration";
|
|
249
|
+
}
|
|
250
|
+
if (type === "c2s.push.register" || type === "c2s.widget.push.register") {
|
|
251
|
+
if (p.environment !== void 0 && p.environment !== "production" && p.environment !== "development") return "invalid APNs environment";
|
|
252
|
+
if (!optional("enrollKey", (v) => text(v, 128))) return "invalid enrollKey";
|
|
253
|
+
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";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
18
257
|
//#region src/device-auth.ts
|
|
19
258
|
function getPairingCodeTtlMs() {
|
|
20
259
|
return Number(process.env.DEEPPILOT_PAIRING_TTL_MS) || 3e5;
|
|
@@ -226,7 +465,9 @@ var DeviceStore = class DeviceStore {
|
|
|
226
465
|
scopes: normalizeDeviceScopes(record.scopes),
|
|
227
466
|
firstSeenTs: existing?.firstSeenTs ?? now,
|
|
228
467
|
lastSeenTs: now,
|
|
229
|
-
...existing?.apns ? { apns: existing.apns } : {}
|
|
468
|
+
...existing?.apns ? { apns: existing.apns } : {},
|
|
469
|
+
...existing?.widgetApns ? { widgetApns: existing.widgetApns } : {},
|
|
470
|
+
...existing?.liveActivity ? { liveActivity: existing.liveActivity } : {}
|
|
230
471
|
};
|
|
231
472
|
this.devices.set(deviceId, next);
|
|
232
473
|
this.flush();
|
|
@@ -251,6 +492,8 @@ var DeviceStore = class DeviceStore {
|
|
|
251
492
|
if (!record || record.revokedAt !== void 0) return false;
|
|
252
493
|
record.revokedAt = now;
|
|
253
494
|
delete record.apns;
|
|
495
|
+
delete record.widgetApns;
|
|
496
|
+
delete record.liveActivity;
|
|
254
497
|
this.flush();
|
|
255
498
|
return true;
|
|
256
499
|
}
|
|
@@ -296,6 +539,48 @@ var DeviceStore = class DeviceStore {
|
|
|
296
539
|
delete record.apns;
|
|
297
540
|
this.flush();
|
|
298
541
|
}
|
|
542
|
+
setLiveActivity(deviceId, registration) {
|
|
543
|
+
const record = this.authorized(deviceId);
|
|
544
|
+
if (!record || !isValidApnsToken(registration.token)) return;
|
|
545
|
+
const old = record.liveActivity;
|
|
546
|
+
record.liveActivity = old?.activityId === registration.activityId ? {
|
|
547
|
+
...registration,
|
|
548
|
+
expiresAt: old.expiresAt,
|
|
549
|
+
endedState: old.endedState
|
|
550
|
+
} : registration;
|
|
551
|
+
this.flush();
|
|
552
|
+
}
|
|
553
|
+
endLiveActivity(deviceId, token, state) {
|
|
554
|
+
const registration = this.devices.get(deviceId)?.liveActivity;
|
|
555
|
+
if (!registration || registration.token !== token || registration.endedState) return;
|
|
556
|
+
registration.endedState = state;
|
|
557
|
+
this.flush();
|
|
558
|
+
}
|
|
559
|
+
clearLiveActivity(deviceId, activityId, token) {
|
|
560
|
+
const record = this.devices.get(deviceId);
|
|
561
|
+
if (record?.liveActivity?.activityId !== activityId || token !== void 0 && record.liveActivity.token !== token) return;
|
|
562
|
+
delete record.liveActivity;
|
|
563
|
+
this.flush();
|
|
564
|
+
}
|
|
565
|
+
setWidgetPushToken(deviceId, token, environment, now) {
|
|
566
|
+
const record = this.authorized(deviceId);
|
|
567
|
+
if (!record || !isValidApnsToken(token)) return;
|
|
568
|
+
const previous = record.widgetApns;
|
|
569
|
+
if (previous?.token === token && previous.environment === environment && now - previous.updatedAt < 36e5) return;
|
|
570
|
+
record.widgetApns = {
|
|
571
|
+
token,
|
|
572
|
+
environment,
|
|
573
|
+
updatedAt: now
|
|
574
|
+
};
|
|
575
|
+
this.flush();
|
|
576
|
+
}
|
|
577
|
+
/** Compare-and-clear protects a rotated token from a delayed APNs rejection. */
|
|
578
|
+
clearWidgetPushToken(deviceId, token) {
|
|
579
|
+
const record = this.devices.get(deviceId);
|
|
580
|
+
if (record?.widgetApns?.token !== token) return;
|
|
581
|
+
delete record.widgetApns;
|
|
582
|
+
this.flush();
|
|
583
|
+
}
|
|
299
584
|
/** Serialized so concurrent touches can never interleave half-written JSON. */
|
|
300
585
|
flush() {
|
|
301
586
|
const next = this.flushTail.then(() => this.writeFile());
|
|
@@ -332,14 +617,55 @@ const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
|
332
617
|
function sanitizeDeviceField(value, maxChars) {
|
|
333
618
|
return (typeof value === "string" ? value : String(value ?? "")).replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, maxChars);
|
|
334
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Runtime shape guard for a frame after JSON.parse. The old `as Envelope`
|
|
622
|
+
* cast alone let JSON `null` reach `env.v` and let a missing or mistyped
|
|
623
|
+
* `type` reach `requiredScope()`'s string operations — one anonymous frame
|
|
624
|
+
* could crash the host process. Reject anything that is not a plain object
|
|
625
|
+
* with a numeric version and a non-empty string type, so field access below
|
|
626
|
+
* is always safe. The payload is intentionally opaque here; each handler
|
|
627
|
+
* validates its own payload shape.
|
|
628
|
+
*/
|
|
629
|
+
function isEnvelope(value) {
|
|
630
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
631
|
+
const frame = value;
|
|
632
|
+
if (typeof frame.v !== "number") return false;
|
|
633
|
+
if (typeof frame.type !== "string" || frame.type.length === 0) return false;
|
|
634
|
+
if (frame.id !== void 0 && typeof frame.id !== "string") return false;
|
|
635
|
+
if (frame.ts !== void 0 && typeof frame.ts !== "number") return false;
|
|
636
|
+
if (frame.seq !== void 0 && typeof frame.seq !== "number") return false;
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
335
639
|
function requiredScope(type) {
|
|
640
|
+
if (typeof type !== "string") return void 0;
|
|
336
641
|
if (type === "c2s.ping" || type === "c2s.resume") return void 0;
|
|
337
|
-
if (type === "c2s.session.sendPrompt") return "prompt.send";
|
|
642
|
+
if (type === "c2s.session.sendPrompt" || type === "c2s.session.delivery") return "prompt.send";
|
|
643
|
+
if (type === "c2s.pending.list") return "interactions.respond";
|
|
338
644
|
if (type === "c2s.approval.respond" || type === "c2s.question.respond") return "interactions.respond";
|
|
339
|
-
if (type === "c2s.
|
|
645
|
+
if (type === "c2s.liveActivity.register" || type === "c2s.liveActivity.unregister") return "notifications.register";
|
|
646
|
+
if (type === "c2s.push.register" || type === "c2s.widget.push.register") return "notifications.register";
|
|
340
647
|
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
648
|
if (type.startsWith("c2s.")) return "sessions.read";
|
|
342
649
|
}
|
|
650
|
+
/** Broadcast and replay authorization. Unknown frame types fail closed. */
|
|
651
|
+
const PUSH_SCOPE_BY_TYPE = {
|
|
652
|
+
"s2c.session.event": "sessions.read",
|
|
653
|
+
"s2c.sessions.delta": "sessions.read",
|
|
654
|
+
"s2c.session.tail": "sessions.read",
|
|
655
|
+
"s2c.history.page": "sessions.read",
|
|
656
|
+
"s2c.pending.approval": "interactions.respond",
|
|
657
|
+
"s2c.pending.question": "interactions.respond",
|
|
658
|
+
"s2c.pending.cleared": "interactions.respond"
|
|
659
|
+
};
|
|
660
|
+
function pushScopeFor(type, payload) {
|
|
661
|
+
const fixed = PUSH_SCOPE_BY_TYPE[type];
|
|
662
|
+
if (fixed !== void 0) return fixed;
|
|
663
|
+
if (type === "s2c.notify") {
|
|
664
|
+
const category = payload?.category;
|
|
665
|
+
if (category === "approval.required" || category === "question.asked") return "interactions.respond";
|
|
666
|
+
if (category === "turn.completed" || category === "session.error") return "sessions.read";
|
|
667
|
+
}
|
|
668
|
+
}
|
|
343
669
|
function sanitizeImageName(value) {
|
|
344
670
|
return value.replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, 120);
|
|
345
671
|
}
|
|
@@ -396,13 +722,20 @@ var BridgeConnection = class {
|
|
|
396
722
|
/** Sanitized device identity from hello; needed for push registration. */
|
|
397
723
|
deviceId;
|
|
398
724
|
scopes = /* @__PURE__ */ new Set();
|
|
725
|
+
widgetClient = false;
|
|
726
|
+
liveActivityRegistrationGeneration = 0;
|
|
727
|
+
pendingLiveActivityId;
|
|
399
728
|
authChallenge;
|
|
400
729
|
constructor(ws, deps) {
|
|
401
730
|
this.ws = ws;
|
|
402
731
|
this.deps = deps;
|
|
403
732
|
this.authChallenge = createAuthChallenge(deps.audience);
|
|
404
733
|
ws.on("message", (data) => {
|
|
405
|
-
this.onMessage(String(data))
|
|
734
|
+
this.onMessage(String(data)).catch((error) => {
|
|
735
|
+
if (this.deps.debug === true) this.deps.log("frame handler failed: " + String(error));
|
|
736
|
+
if (this.closed) return;
|
|
737
|
+
this.terminate();
|
|
738
|
+
});
|
|
406
739
|
});
|
|
407
740
|
ws.on("close", () => {
|
|
408
741
|
this.onClose();
|
|
@@ -439,7 +772,19 @@ var BridgeConnection = class {
|
|
|
439
772
|
get connectedDeviceId() {
|
|
440
773
|
return this.authenticated ? this.deviceId : void 0;
|
|
441
774
|
}
|
|
775
|
+
get suppressesAlertPush() {
|
|
776
|
+
return !this.widgetClient;
|
|
777
|
+
}
|
|
778
|
+
/** S→C permission gate consulted by the bridge for every broadcast/replay
|
|
779
|
+
* frame: a device only receives what its scopes grant (R1/P2). */
|
|
780
|
+
canReceive(scope) {
|
|
781
|
+
return this.scopes.has(scope);
|
|
782
|
+
}
|
|
442
783
|
push(type, payload, seq) {
|
|
784
|
+
if (type === "s2c.notify") payload = {
|
|
785
|
+
...payload,
|
|
786
|
+
hostAudience: this.deps.audience
|
|
787
|
+
};
|
|
443
788
|
if (type === "s2c.session.event") {
|
|
444
789
|
const sessionId = payload?.sessionId;
|
|
445
790
|
if (typeof sessionId === "string") {
|
|
@@ -528,7 +873,12 @@ var BridgeConnection = class {
|
|
|
528
873
|
}
|
|
529
874
|
let env;
|
|
530
875
|
try {
|
|
531
|
-
|
|
876
|
+
const parsed = JSON.parse(raw);
|
|
877
|
+
if (!isEnvelope(parsed)) {
|
|
878
|
+
this.fail(void 0, "E_PROTOCOL", "malformed frame");
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
env = parsed;
|
|
532
882
|
} catch {
|
|
533
883
|
this.fail(void 0, "E_PROTOCOL", "frame is not valid JSON");
|
|
534
884
|
return;
|
|
@@ -550,11 +900,19 @@ var BridgeConnection = class {
|
|
|
550
900
|
this.fail(env.id, "E_PROTOCOL", "authenticate first");
|
|
551
901
|
return;
|
|
552
902
|
}
|
|
903
|
+
if (this.widgetClient && ![
|
|
904
|
+
"c2s.ping",
|
|
905
|
+
"c2s.sessions.list",
|
|
906
|
+
"c2s.pending.list",
|
|
907
|
+
"c2s.widget.push.register"
|
|
908
|
+
].includes(env.type)) return this.fail(env.id, "E_FORBIDDEN", "widget connection is read-only");
|
|
553
909
|
const required = requiredScope(env.type);
|
|
554
910
|
if (required !== void 0 && !this.scopes.has(required)) {
|
|
555
911
|
this.fail(env.id, "E_FORBIDDEN", `scope ${required} required`);
|
|
556
912
|
return;
|
|
557
913
|
}
|
|
914
|
+
const invalid = validateRequest(env.type, env.payload);
|
|
915
|
+
if (invalid) return this.fail(env.id, "E_PROTOCOL", invalid);
|
|
558
916
|
switch (env.type) {
|
|
559
917
|
case "c2s.ping":
|
|
560
918
|
this.send("s2c.pong", { serverTime: Date.now() }, env.id);
|
|
@@ -727,6 +1085,11 @@ var BridgeConnection = class {
|
|
|
727
1085
|
}, env.id);
|
|
728
1086
|
return;
|
|
729
1087
|
}
|
|
1088
|
+
case "c2s.session.delivery": {
|
|
1089
|
+
const p = env.payload;
|
|
1090
|
+
this.send("s2c.ack", this.deps.bridge.promptDeliveries.lookup(this.deviceId, p.sessionId, p.clientSendId), env.id);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
730
1093
|
case "c2s.session.sendPrompt": {
|
|
731
1094
|
const p = env.payload;
|
|
732
1095
|
const text = typeof p?.text === "string" ? p.text : "";
|
|
@@ -758,6 +1121,15 @@ var BridgeConnection = class {
|
|
|
758
1121
|
...document.truncated === true ? { truncated: true } : {}
|
|
759
1122
|
});
|
|
760
1123
|
}
|
|
1124
|
+
if (p.clientSendId) {
|
|
1125
|
+
const receipt = await this.deps.bridge.promptDeliveries.dispatch(this.deviceId, p.sessionId, p.clientSendId, {
|
|
1126
|
+
text,
|
|
1127
|
+
images,
|
|
1128
|
+
documents
|
|
1129
|
+
}, () => this.deps.bridge.sendPrompt(p.sessionId, text, images, documents));
|
|
1130
|
+
this.send("s2c.ack", receipt, env.id);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
761
1133
|
const userSeq = await this.deps.bridge.sendPrompt(p.sessionId, text, images, documents);
|
|
762
1134
|
if (!userSeq.ok) return this.fail(env.id, managementErrorCode(userSeq.kind), userSeq.message);
|
|
763
1135
|
this.send("s2c.ack", { userSeq: userSeq.value }, env.id);
|
|
@@ -779,7 +1151,52 @@ var BridgeConnection = class {
|
|
|
779
1151
|
this.send("s2c.ack", {}, env.id);
|
|
780
1152
|
return;
|
|
781
1153
|
}
|
|
1154
|
+
case "c2s.liveActivity.unregister": {
|
|
1155
|
+
const p = env.payload;
|
|
1156
|
+
if (this.pendingLiveActivityId === p.activityId) {
|
|
1157
|
+
this.liveActivityRegistrationGeneration += 1;
|
|
1158
|
+
this.pendingLiveActivityId = void 0;
|
|
1159
|
+
}
|
|
1160
|
+
this.deps.devices.clearLiveActivity(this.deviceId, p.activityId);
|
|
1161
|
+
this.send("s2c.ack", {}, env.id);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
case "c2s.liveActivity.register": {
|
|
1165
|
+
const p = env.payload;
|
|
1166
|
+
const initial = this.deviceId && this.deps.devices.authorized(this.deviceId);
|
|
1167
|
+
if (!initial || ![
|
|
1168
|
+
"notifications.register",
|
|
1169
|
+
"sessions.read",
|
|
1170
|
+
"interactions.respond"
|
|
1171
|
+
].every((scope) => initial.scopes?.includes(scope))) return this.fail(env.id, "E_FORBIDDEN", "live activity permissions required");
|
|
1172
|
+
const generation = ++this.liveActivityRegistrationGeneration;
|
|
1173
|
+
this.pendingLiveActivityId = p.activityId;
|
|
1174
|
+
if (p.enrollKey) await this.deps.onPushEnrollKey?.(p.enrollKey);
|
|
1175
|
+
if (generation !== this.liveActivityRegistrationGeneration) return this.fail(env.id, "E_BUSY", "live activity registration superseded");
|
|
1176
|
+
const record = this.deviceId && this.deps.devices.authorized(this.deviceId);
|
|
1177
|
+
if (this.closed || !record || ![
|
|
1178
|
+
"notifications.register",
|
|
1179
|
+
"sessions.read",
|
|
1180
|
+
"interactions.respond"
|
|
1181
|
+
].every((scope) => record.scopes?.includes(scope))) return this.fail(env.id, "E_FORBIDDEN", "live activity permissions required");
|
|
1182
|
+
if (!this.deps.bridge.listSessions().some((row) => row.id === p.sessionId)) return this.fail(env.id, "E_NOT_FOUND", "session not found");
|
|
1183
|
+
const previous = record.liveActivity;
|
|
1184
|
+
if (previous?.activityId === p.activityId && previous.sessionId !== p.sessionId) return this.fail(env.id, "E_PROTOCOL", "activity is bound to another session");
|
|
1185
|
+
this.deps.devices.setLiveActivity(record.deviceId, {
|
|
1186
|
+
activityId: p.activityId,
|
|
1187
|
+
sessionId: p.sessionId,
|
|
1188
|
+
token: p.deviceToken.toLowerCase(),
|
|
1189
|
+
environment: p.environment,
|
|
1190
|
+
updatedAt: Date.now(),
|
|
1191
|
+
expiresAt: Date.now() + 288e5
|
|
1192
|
+
});
|
|
1193
|
+
this.deps.bridge.refreshLiveActivities();
|
|
1194
|
+
this.send("s2c.ack", { enabled: this.deps.bridge.capabilities.push }, env.id);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
case "c2s.widget.push.register":
|
|
782
1198
|
case "c2s.push.register": {
|
|
1199
|
+
const widget = env.type === "c2s.widget.push.register";
|
|
783
1200
|
const p = env.payload;
|
|
784
1201
|
const token = typeof p?.deviceToken === "string" ? p.deviceToken.trim() : "";
|
|
785
1202
|
if (!isValidApnsToken(token)) return this.fail(env.id, "E_PROTOCOL", "hex deviceToken (32-512 chars) required");
|
|
@@ -790,7 +1207,12 @@ var BridgeConnection = class {
|
|
|
790
1207
|
const enrollKey = p.enrollKey.trim().replace(/[^\x20-\x7e]/g, "").slice(0, 128);
|
|
791
1208
|
if (enrollKey.length >= 8 && enrollKey.length <= 128) await this.deps.onPushEnrollKey?.(enrollKey);
|
|
792
1209
|
}
|
|
793
|
-
this.deps.devices.
|
|
1210
|
+
const record = this.deps.devices.authorized(this.deviceId);
|
|
1211
|
+
if (this.closed || !record?.scopes?.includes("notifications.register")) return this.fail(env.id, "E_FORBIDDEN", "device authorization changed");
|
|
1212
|
+
if (widget) {
|
|
1213
|
+
if (!record.scopes.includes("sessions.read") || !record.scopes.includes("interactions.respond")) return this.fail(env.id, "E_FORBIDDEN", "widget overview permissions required");
|
|
1214
|
+
this.deps.devices.setWidgetPushToken(this.deviceId, token, environment, Date.now());
|
|
1215
|
+
} else this.deps.devices.setPushToken(this.deviceId, token, environment, categories, Date.now());
|
|
794
1216
|
if (!this.deps.bridge.capabilities.push) {
|
|
795
1217
|
if (this.deps.debug === true) this.deps.log("push register held: bridge not ready");
|
|
796
1218
|
return this.fail(env.id, "E_UNSUPPORTED", "push is not configured on this bridge");
|
|
@@ -836,11 +1258,12 @@ var BridgeConnection = class {
|
|
|
836
1258
|
this.settleAuthentication(true, "success");
|
|
837
1259
|
this.authenticated = true;
|
|
838
1260
|
this.deviceId = deviceId;
|
|
1261
|
+
this.widgetClient = p.clientRole === "widget";
|
|
839
1262
|
this.scopes = new Set(record.scopes ?? []);
|
|
840
1263
|
if (this.helloTimer !== void 0) clearTimeout(this.helloTimer);
|
|
841
|
-
this.deps.devices.markAuthenticated(deviceId, deviceName, appVersion, Date.now());
|
|
1264
|
+
if (!this.widgetClient) this.deps.devices.markAuthenticated(deviceId, deviceName, appVersion, Date.now());
|
|
842
1265
|
this.deps.onDeviceAuthenticated?.(deviceId);
|
|
843
|
-
const cursor = resumeCursor;
|
|
1266
|
+
const cursor = this.widgetClient ? void 0 : resumeCursor;
|
|
844
1267
|
const canResume = cursor !== void 0 && this.deps.bridge.canResumeFrom(cursor);
|
|
845
1268
|
this.send("s2c.welcome", {
|
|
846
1269
|
protocolVersion: 2,
|
|
@@ -851,7 +1274,7 @@ var BridgeConnection = class {
|
|
|
851
1274
|
cursor: this.deps.bridge.currentCursor(),
|
|
852
1275
|
resumed: canResume
|
|
853
1276
|
}, env.id);
|
|
854
|
-
this.deps.bridge.addSink(this);
|
|
1277
|
+
if (!this.widgetClient) this.deps.bridge.addSink(this);
|
|
855
1278
|
if (cursor !== void 0) {
|
|
856
1279
|
if (canResume) this.deps.bridge.resumeFrom(cursor, this);
|
|
857
1280
|
else this.resync();
|
|
@@ -950,6 +1373,7 @@ function projectEvent(sessionId, event) {
|
|
|
950
1373
|
kind: "turn.end",
|
|
951
1374
|
data: { ok: event.data?.reason?.kind === "completed" }
|
|
952
1375
|
};
|
|
1376
|
+
case "system/message": return null;
|
|
953
1377
|
case "user/message": return {
|
|
954
1378
|
kind: "message.final",
|
|
955
1379
|
data: { ...limitMessageProjection({
|
|
@@ -1212,6 +1636,7 @@ function projectHistory(events) {
|
|
|
1212
1636
|
ts: tsOf(event)
|
|
1213
1637
|
};
|
|
1214
1638
|
switch (event.type) {
|
|
1639
|
+
case "system/message": break;
|
|
1215
1640
|
case "user/message":
|
|
1216
1641
|
messages.push({
|
|
1217
1642
|
...base,
|
|
@@ -1401,11 +1826,14 @@ var HostBridge = class {
|
|
|
1401
1826
|
abort = new AbortController();
|
|
1402
1827
|
started = false;
|
|
1403
1828
|
disposed = false;
|
|
1404
|
-
|
|
1829
|
+
promptDeliveries;
|
|
1830
|
+
constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath) {
|
|
1405
1831
|
this.apiProxy = apiProxy;
|
|
1406
1832
|
this.historyBufferMax = historyBufferMax;
|
|
1833
|
+
this.promptDeliveries = openDeliveryJournal(deliveryJournalPath);
|
|
1407
1834
|
}
|
|
1408
1835
|
pushOutlet;
|
|
1836
|
+
widgetFingerprint = "";
|
|
1409
1837
|
/**
|
|
1410
1838
|
* Wire the offline-push fan-out. Present ⇒ welcome advertises the `push`
|
|
1411
1839
|
* capability and notify-worthy events are mirrored to APNs.
|
|
@@ -1420,11 +1848,14 @@ var HostBridge = class {
|
|
|
1420
1848
|
approvals: true,
|
|
1421
1849
|
questions: true,
|
|
1422
1850
|
pendingSnapshot: true,
|
|
1851
|
+
promptDelivery: true,
|
|
1423
1852
|
notifyAllCategories: true,
|
|
1424
1853
|
models: typeof this.apiProxy.sessions.models === "function" && typeof this.apiProxy.sessions.selectModel === "function",
|
|
1425
1854
|
sessionManagement: typeof this.apiProxy.sessions.rename === "function" && typeof this.apiProxy.workspace?.archiveSession === "function",
|
|
1426
1855
|
projectSelection: typeof this.apiProxy.workspace?.list === "function" && typeof this.apiProxy.workspace?.create === "function",
|
|
1427
|
-
push: this.pushOutlet?.isAvailable() === true
|
|
1856
|
+
push: this.pushOutlet?.isAvailable() === true,
|
|
1857
|
+
widgetPush: true,
|
|
1858
|
+
liveActivityPush: true
|
|
1428
1859
|
};
|
|
1429
1860
|
}
|
|
1430
1861
|
diagnostic(message) {
|
|
@@ -1521,17 +1952,52 @@ var HostBridge = class {
|
|
|
1521
1952
|
* Replay buffered pushes after the given cursor; false when the gap is
|
|
1522
1953
|
* unrecoverable. Frames go to `target` only — replaying into every sink
|
|
1523
1954
|
* duplicated the whole window onto devices that never asked for it.
|
|
1955
|
+
* Each frame is filtered by the S→C permission policy per sink, so a
|
|
1956
|
+
* reader without interactions.respond never gets the missed approval
|
|
1957
|
+
* frames back (R1/P2).
|
|
1524
1958
|
*/
|
|
1525
1959
|
resumeFrom(cursor, target) {
|
|
1526
1960
|
const oldest = this.ring.length > 0 ? this.ring[0].seq : this.cursor + 1;
|
|
1527
1961
|
if (cursor + 1 < oldest) return false;
|
|
1528
1962
|
const receivers = target !== void 0 ? [target] : [...this.sinks];
|
|
1529
|
-
for (const entry of this.ring) if (entry.seq > cursor) for (const sink of receivers)
|
|
1963
|
+
for (const entry of this.ring) if (entry.seq > cursor) for (const sink of receivers) {
|
|
1964
|
+
const need = pushScopeFor(entry.type, entry.payload);
|
|
1965
|
+
if (need === void 0 || !sink.canReceive(need)) continue;
|
|
1966
|
+
sink.replay([entry]);
|
|
1967
|
+
}
|
|
1530
1968
|
for (const sink of receivers) sink.replayDone();
|
|
1531
1969
|
return true;
|
|
1532
1970
|
}
|
|
1971
|
+
refreshLiveActivities() {
|
|
1972
|
+
try {
|
|
1973
|
+
this.pushOutlet?.liveActivityChanged?.(this.listSessions());
|
|
1974
|
+
} catch {}
|
|
1975
|
+
}
|
|
1533
1976
|
record(type, payload, except) {
|
|
1534
1977
|
if (this.disposed) return;
|
|
1978
|
+
if (type === "s2c.sessions.delta") this.refreshLiveActivities();
|
|
1979
|
+
if (type === "s2c.sessions.delta" || type.startsWith("s2c.pending.")) {
|
|
1980
|
+
const fingerprint = JSON.stringify([
|
|
1981
|
+
this.listSessions().map(({ id, title, status, lastActivityTs, todos, todoItems, pendingApproval, pendingQuestion }) => ({
|
|
1982
|
+
id,
|
|
1983
|
+
title,
|
|
1984
|
+
status,
|
|
1985
|
+
lastActivityTs,
|
|
1986
|
+
todos,
|
|
1987
|
+
todoItems,
|
|
1988
|
+
pendingApproval,
|
|
1989
|
+
pendingQuestion
|
|
1990
|
+
})),
|
|
1991
|
+
this.approvals.size,
|
|
1992
|
+
this.questions.size
|
|
1993
|
+
]);
|
|
1994
|
+
if (fingerprint !== this.widgetFingerprint) {
|
|
1995
|
+
this.widgetFingerprint = fingerprint;
|
|
1996
|
+
try {
|
|
1997
|
+
this.pushOutlet?.widgetChanged?.();
|
|
1998
|
+
} catch {}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
1535
2001
|
this.cursor += 1;
|
|
1536
2002
|
const entry = {
|
|
1537
2003
|
seq: this.cursor,
|
|
@@ -1540,7 +2006,9 @@ var HostBridge = class {
|
|
|
1540
2006
|
};
|
|
1541
2007
|
this.ring.push(entry);
|
|
1542
2008
|
if (this.ring.length > this.historyBufferMax) this.ring.splice(0, this.ring.length - this.historyBufferMax);
|
|
2009
|
+
const need = pushScopeFor(type, payload);
|
|
1543
2010
|
for (const sink of this.sinks) {
|
|
2011
|
+
if (need === void 0 || !sink.canReceive(need)) continue;
|
|
1544
2012
|
if (except && except(sink)) continue;
|
|
1545
2013
|
sink.push(type, payload, entry.seq);
|
|
1546
2014
|
}
|
|
@@ -1782,6 +2250,14 @@ var HostBridge = class {
|
|
|
1782
2250
|
} else if (key === "sessionListMetadata") {
|
|
1783
2251
|
const meta = value;
|
|
1784
2252
|
if (meta?.lastPromptAt) row.lastActivityTs = Math.max(row.lastActivityTs, meta.lastPromptAt);
|
|
2253
|
+
} else if (key === "sessionStats" || key === "tokenUsage") {
|
|
2254
|
+
const patch = sanitizeUsageStatsPatch(key, value);
|
|
2255
|
+
if (!patch) return;
|
|
2256
|
+
row.stats = {
|
|
2257
|
+
...emptyUsageStats,
|
|
2258
|
+
...row.stats ?? {},
|
|
2259
|
+
...patch
|
|
2260
|
+
};
|
|
1785
2261
|
} else return;
|
|
1786
2262
|
this.pushSummary(row);
|
|
1787
2263
|
}
|
|
@@ -2415,6 +2891,65 @@ function sanitizeTodoItems(items) {
|
|
|
2415
2891
|
status: i.status
|
|
2416
2892
|
}));
|
|
2417
2893
|
}
|
|
2894
|
+
/** Zero-value stats snapshot used as the merge base for partial patches. */
|
|
2895
|
+
const emptyUsageStats = {
|
|
2896
|
+
turns: 0,
|
|
2897
|
+
steps: 0,
|
|
2898
|
+
llmMs: 0,
|
|
2899
|
+
toolMs: 0,
|
|
2900
|
+
ttftMs: 0,
|
|
2901
|
+
ttftSteps: 0,
|
|
2902
|
+
decodeMs: 0,
|
|
2903
|
+
decodeTokens: 0,
|
|
2904
|
+
inputTokens: 0,
|
|
2905
|
+
outputTokens: 0,
|
|
2906
|
+
cacheReadTokens: 0,
|
|
2907
|
+
cacheWriteTokens: 0
|
|
2908
|
+
};
|
|
2909
|
+
/** Coerce one host counter to a non-negative integer; junk becomes 0. */
|
|
2910
|
+
function usageCounter(value) {
|
|
2911
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
2912
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
2913
|
+
return Math.floor(n);
|
|
2914
|
+
}
|
|
2915
|
+
const SESSION_STATS_COUNTERS = [
|
|
2916
|
+
"turns",
|
|
2917
|
+
"steps",
|
|
2918
|
+
"llmMs",
|
|
2919
|
+
"toolMs",
|
|
2920
|
+
"ttftMs",
|
|
2921
|
+
"ttftSteps",
|
|
2922
|
+
"decodeMs",
|
|
2923
|
+
"decodeTokens"
|
|
2924
|
+
];
|
|
2925
|
+
const TOKEN_USAGE_COUNTERS = [
|
|
2926
|
+
"outputTokens",
|
|
2927
|
+
"cacheReadTokens",
|
|
2928
|
+
"cacheWriteTokens"
|
|
2929
|
+
];
|
|
2930
|
+
/** Sanitize one `sessionStats` / `tokenUsage` projection value into the wire
|
|
2931
|
+
* stats fields; undefined when the payload carries nothing readable. Host
|
|
2932
|
+
* field names differ between the two projections (tokenUsage reports
|
|
2933
|
+
* `uncachedInputTokens`, the wire mirrors it as `inputTokens`). */
|
|
2934
|
+
function sanitizeUsageStatsPatch(key, value) {
|
|
2935
|
+
if (!value || typeof value !== "object") return void 0;
|
|
2936
|
+
const raw = value;
|
|
2937
|
+
const patch = {};
|
|
2938
|
+
if (key === "sessionStats") {
|
|
2939
|
+
for (const field of SESSION_STATS_COUNTERS) if (field in raw) patch[field] = usageCounter(raw[field]);
|
|
2940
|
+
} else {
|
|
2941
|
+
if ("uncachedInputTokens" in raw) patch.inputTokens = usageCounter(raw.uncachedInputTokens);
|
|
2942
|
+
for (const field of TOKEN_USAGE_COUNTERS) if (field in raw) patch[field] = usageCounter(raw[field]);
|
|
2943
|
+
}
|
|
2944
|
+
return Object.keys(patch).length > 0 ? patch : void 0;
|
|
2945
|
+
}
|
|
2946
|
+
/** Combine one session's `sessionStats` + `tokenUsage` projection values into
|
|
2947
|
+
* a complete wire stats object; undefined when neither carries anything. */
|
|
2948
|
+
function usageStatsOf(sessionStats, tokenUsage) {
|
|
2949
|
+
const patches = [sanitizeUsageStatsPatch("sessionStats", sessionStats), sanitizeUsageStatsPatch("tokenUsage", tokenUsage)].filter((patch) => patch !== void 0);
|
|
2950
|
+
if (patches.length === 0) return void 0;
|
|
2951
|
+
return Object.assign({}, emptyUsageStats, ...patches);
|
|
2952
|
+
}
|
|
2418
2953
|
function toSummary(row, approvals, questions, workspace) {
|
|
2419
2954
|
const values = row.projections?.values ?? {};
|
|
2420
2955
|
const todos = Array.isArray(values.todos) ? values.todos : null;
|
|
@@ -2425,6 +2960,7 @@ function toSummary(row, approvals, questions, workspace) {
|
|
|
2425
2960
|
const cwd = typeof row.cwd === "string" ? row.cwd : "";
|
|
2426
2961
|
const label = workspace?.title ?? (cwd ? cwd.split("/").filter(Boolean).pop() : void 0);
|
|
2427
2962
|
const todoItems = sanitizeTodoItems(todos);
|
|
2963
|
+
const stats = usageStatsOf(values.sessionStats, values.tokenUsage);
|
|
2428
2964
|
return {
|
|
2429
2965
|
id: row.sessionId,
|
|
2430
2966
|
title: typeof values.title === "string" ? values.title : "",
|
|
@@ -2437,6 +2973,7 @@ function toSummary(row, approvals, questions, workspace) {
|
|
|
2437
2973
|
todoItems: todoItems.length > 0 ? todoItems : null,
|
|
2438
2974
|
pendingApproval,
|
|
2439
2975
|
pendingQuestion,
|
|
2976
|
+
...stats ? { stats } : {},
|
|
2440
2977
|
workspaceLabel: label ?? null,
|
|
2441
2978
|
workspaceId: workspace?.workspaceId ?? null,
|
|
2442
2979
|
workspacePath: workspace?.path ?? (cwd || null)
|
|
@@ -3642,6 +4179,16 @@ function p8ToDer(pem) {
|
|
|
3642
4179
|
}
|
|
3643
4180
|
/** Pure payload builder so tests can assert the wire format without sockets. */
|
|
3644
4181
|
function apnsPayload(notification) {
|
|
4182
|
+
if ("kind" in notification && notification.kind === "liveactivity") return { aps: {
|
|
4183
|
+
timestamp: notification.timestamp,
|
|
4184
|
+
event: notification.event,
|
|
4185
|
+
"content-state": notification.contentState,
|
|
4186
|
+
...notification.event === "end" ? { "dismissal-date": notification.timestamp + 300 } : { "stale-date": notification.timestamp + 180 }
|
|
4187
|
+
} };
|
|
4188
|
+
if ("kind" in notification && notification.kind === "widget") return { aps: { "content-changed": true } };
|
|
4189
|
+
return alertPayload(notification);
|
|
4190
|
+
}
|
|
4191
|
+
function alertPayload(notification) {
|
|
3645
4192
|
const timeSensitive = notification.category === "approval.required" || notification.category === "question.asked";
|
|
3646
4193
|
return {
|
|
3647
4194
|
aps: {
|
|
@@ -3651,17 +4198,38 @@ function apnsPayload(notification) {
|
|
|
3651
4198
|
},
|
|
3652
4199
|
sound: "default",
|
|
3653
4200
|
category: notification.category,
|
|
3654
|
-
"thread-id": notification.sessionId.slice(0, 64),
|
|
4201
|
+
"thread-id": notification.hostAudience ? createHash("sha256").update(`${notification.hostAudience}:${notification.sessionId}`).digest("hex") : notification.sessionId.slice(0, 64),
|
|
3655
4202
|
...timeSensitive ? { "interruption-level": "time-sensitive" } : {}
|
|
3656
4203
|
},
|
|
4204
|
+
...notification.hostAudience ? { hostAudience: notification.hostAudience } : {},
|
|
3657
4205
|
sessionId: notification.sessionId,
|
|
3658
4206
|
notificationId: notification.notificationId,
|
|
3659
4207
|
kind: notification.category
|
|
3660
4208
|
};
|
|
3661
4209
|
}
|
|
4210
|
+
function pushHeaders(bundleId, notification) {
|
|
4211
|
+
if ("kind" in notification && notification.kind === "liveactivity") return {
|
|
4212
|
+
"apns-topic": bundleId + ".push-type.liveactivity",
|
|
4213
|
+
"apns-push-type": "liveactivity",
|
|
4214
|
+
"apns-priority": notification.event === "end" ? "10" : "5",
|
|
4215
|
+
"apns-expiration": String(notification.timestamp + 180)
|
|
4216
|
+
};
|
|
4217
|
+
if ("kind" in notification && notification.kind === "widget") return {
|
|
4218
|
+
"apns-topic": bundleId + ".push-type.widgets",
|
|
4219
|
+
"apns-push-type": "widgets",
|
|
4220
|
+
"apns-priority": "5",
|
|
4221
|
+
"apns-collapse-id": "widget-overview"
|
|
4222
|
+
};
|
|
4223
|
+
return {
|
|
4224
|
+
"apns-topic": bundleId,
|
|
4225
|
+
"apns-push-type": "alert",
|
|
4226
|
+
"apns-priority": "10",
|
|
4227
|
+
"apns-collapse-id": collapseIdFor(notification)
|
|
4228
|
+
};
|
|
4229
|
+
}
|
|
3662
4230
|
/** collapse-id accepts ≤64 bytes of ASCII; keep it stable per session+event. */
|
|
3663
4231
|
function collapseIdFor(notification) {
|
|
3664
|
-
const raw = `${notification.category}:${notification.sessionId}`;
|
|
4232
|
+
const raw = `${notification.hostAudience ? notification.hostAudience + ":" : ""}${notification.category}:${notification.sessionId}`;
|
|
3665
4233
|
const readable = raw.replace(/[^a-zA-Z0-9.:-]/g, "");
|
|
3666
4234
|
const digest = createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 12);
|
|
3667
4235
|
return `${readable.slice(0, 51)}:${digest}`;
|
|
@@ -3738,11 +4306,8 @@ var ApnsClient = class {
|
|
|
3738
4306
|
[":method"]: "POST",
|
|
3739
4307
|
[":path"]: "/3/device/" + deviceToken,
|
|
3740
4308
|
authorization: "bearer " + token,
|
|
3741
|
-
|
|
3742
|
-
"apns-push-type": "alert",
|
|
3743
|
-
"apns-priority": "10",
|
|
4309
|
+
...pushHeaders(this.opts.bundleId, notification),
|
|
3744
4310
|
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 3600),
|
|
3745
|
-
"apns-collapse-id": collapseIdFor(notification),
|
|
3746
4311
|
"content-type": "application/json",
|
|
3747
4312
|
"content-length": String(Buffer.byteLength(body))
|
|
3748
4313
|
});
|
|
@@ -4608,6 +5173,7 @@ const Config = z.object({
|
|
|
4608
5173
|
"apns",
|
|
4609
5174
|
"relay"
|
|
4610
5175
|
]).default("none"),
|
|
5176
|
+
contentMode: z.union(["preview", "generic"]).default("preview"),
|
|
4611
5177
|
teamId: z.string().default(""),
|
|
4612
5178
|
keyId: z.string().default(""),
|
|
4613
5179
|
keyPath: z.string().default(join(bridgeDataDir(), "apns", "AuthKey.p8")),
|
|
@@ -4616,6 +5182,7 @@ const Config = z.object({
|
|
|
4616
5182
|
relayToken: z.string().default("")
|
|
4617
5183
|
}).default({
|
|
4618
5184
|
provider: "none",
|
|
5185
|
+
contentMode: "preview",
|
|
4619
5186
|
teamId: "",
|
|
4620
5187
|
keyId: "",
|
|
4621
5188
|
keyPath: join(bridgeDataDir(), "apns", "AuthKey.p8"),
|
|
@@ -4793,6 +5360,11 @@ var AuthRateLimiter = class {
|
|
|
4793
5360
|
function shouldPrunePushToken(outcome, reason) {
|
|
4794
5361
|
return outcome === "invalid-token" && (reason === "Unregistered" || reason === "ExpiredToken");
|
|
4795
5362
|
}
|
|
5363
|
+
/** APNs requires both registration and the same content permission as WS. */
|
|
5364
|
+
function mayReceivePush(device, notification) {
|
|
5365
|
+
const scope = pushScopeFor("s2c.notify", notification);
|
|
5366
|
+
return scope !== void 0 && device.scopes?.includes("notifications.register") === true && device.scopes.includes(scope);
|
|
5367
|
+
}
|
|
4796
5368
|
/**
|
|
4797
5369
|
* Zero-touch relay self-heal: HTTP 401 means the relay no longer honors the
|
|
4798
5370
|
* cached credential. Only auto-enrolled cells with a still-current token may
|
|
@@ -4802,6 +5374,132 @@ function shouldPrunePushToken(outcome, reason) {
|
|
|
4802
5374
|
function shouldReEnrollRelayToken(transport, outcome, reason, opts) {
|
|
4803
5375
|
return transport === "relay" && outcome === "failed" && reason === "HTTP 401" && opts.hasEnrollKey && opts.usedCellToken && opts.tokenStillCurrent;
|
|
4804
5376
|
}
|
|
5377
|
+
/** Apply before either transport receives the payload, including the Relay. */
|
|
5378
|
+
function pushContent(notification, mode) {
|
|
5379
|
+
if (mode !== "generic") return notification;
|
|
5380
|
+
const bodies = {
|
|
5381
|
+
"turn.completed": "A task has completed.",
|
|
5382
|
+
"approval.required": "An approval needs your attention.",
|
|
5383
|
+
"question.asked": "A question needs your answer.",
|
|
5384
|
+
"session.error": "A task needs your attention."
|
|
5385
|
+
};
|
|
5386
|
+
return {
|
|
5387
|
+
...notification,
|
|
5388
|
+
title: "DeepPilot",
|
|
5389
|
+
body: bodies[notification.category] ?? "Open DeepPilot for an update."
|
|
5390
|
+
};
|
|
5391
|
+
}
|
|
5392
|
+
//#endregion
|
|
5393
|
+
//#region src/widget-push.ts
|
|
5394
|
+
/** Coalesce bursts and bound continuous updates, without starving the trailing
|
|
5395
|
+
* state. One scheduler per host, not per token or per streaming text frame. */
|
|
5396
|
+
var WidgetPushScheduler = class {
|
|
5397
|
+
send;
|
|
5398
|
+
intervalMs;
|
|
5399
|
+
timer;
|
|
5400
|
+
lastSent = 0;
|
|
5401
|
+
disposed = false;
|
|
5402
|
+
sending = false;
|
|
5403
|
+
dirty = false;
|
|
5404
|
+
constructor(send, intervalMs = 3e4) {
|
|
5405
|
+
this.send = send;
|
|
5406
|
+
this.intervalMs = intervalMs;
|
|
5407
|
+
}
|
|
5408
|
+
changed() {
|
|
5409
|
+
if (this.disposed) return;
|
|
5410
|
+
this.dirty = true;
|
|
5411
|
+
if (this.timer || this.sending) return;
|
|
5412
|
+
this.timer = setTimeout(() => {
|
|
5413
|
+
this.timer = void 0;
|
|
5414
|
+
this.dirty = false;
|
|
5415
|
+
this.sending = true;
|
|
5416
|
+
this.lastSent = Date.now();
|
|
5417
|
+
this.send().catch(() => {}).finally(() => {
|
|
5418
|
+
this.sending = false;
|
|
5419
|
+
if (this.dirty) this.changed();
|
|
5420
|
+
});
|
|
5421
|
+
}, Math.max(Math.min(1e3, this.intervalMs), this.intervalMs - (Date.now() - this.lastSent)));
|
|
5422
|
+
this.timer.unref();
|
|
5423
|
+
}
|
|
5424
|
+
dispose() {
|
|
5425
|
+
this.disposed = true;
|
|
5426
|
+
if (this.timer) clearTimeout(this.timer);
|
|
5427
|
+
this.timer = void 0;
|
|
5428
|
+
}
|
|
5429
|
+
};
|
|
5430
|
+
//#endregion
|
|
5431
|
+
//#region src/live-activity.ts
|
|
5432
|
+
function liveActivityState(session) {
|
|
5433
|
+
const total = Math.max(0, session?.todos?.total ?? 0);
|
|
5434
|
+
const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
|
|
5435
|
+
const task = session?.todoItems?.find((item) => item.status === "in_progress") ?? session?.todoItems?.find((item) => item.status === "pending");
|
|
5436
|
+
return {
|
|
5437
|
+
title: Array.from(session?.title ?? "").slice(0, 100).join(""),
|
|
5438
|
+
task: Array.from(task?.content ?? "").slice(0, 160).join(""),
|
|
5439
|
+
done,
|
|
5440
|
+
total,
|
|
5441
|
+
phase: !session ? "unavailable" : session.pendingApproval ? "approval" : session.pendingQuestion ? "question" : session.status === "running" ? "running" : session.status === "idle" ? "ended" : "unavailable"
|
|
5442
|
+
};
|
|
5443
|
+
}
|
|
5444
|
+
/** Persist terminal state before coalescing so a rapid next round cannot revive an activity. */
|
|
5445
|
+
var LiveActivityPushManager = class {
|
|
5446
|
+
devices;
|
|
5447
|
+
send;
|
|
5448
|
+
latest = /* @__PURE__ */ new Map();
|
|
5449
|
+
sent = /* @__PURE__ */ new Map();
|
|
5450
|
+
scheduler;
|
|
5451
|
+
constructor(devices, send, intervalMs = 15e3) {
|
|
5452
|
+
this.devices = devices;
|
|
5453
|
+
this.send = send;
|
|
5454
|
+
this.scheduler = new WidgetPushScheduler(() => this.flush(), intervalMs);
|
|
5455
|
+
}
|
|
5456
|
+
changed(sessions) {
|
|
5457
|
+
this.latest = new Map(sessions.map((session) => [session.id, session]));
|
|
5458
|
+
for (const device of this.devices()?.list() ?? []) {
|
|
5459
|
+
const r = device.liveActivity;
|
|
5460
|
+
if (!r) continue;
|
|
5461
|
+
const state = liveActivityState(this.latest.get(r.sessionId));
|
|
5462
|
+
if (state.phase === "ended" || state.phase === "unavailable") this.devices()?.endLiveActivity(device.deviceId, r.token, state);
|
|
5463
|
+
}
|
|
5464
|
+
this.scheduler.changed();
|
|
5465
|
+
}
|
|
5466
|
+
async flush() {
|
|
5467
|
+
const devices = this.devices();
|
|
5468
|
+
if (!devices) return;
|
|
5469
|
+
const valid = /* @__PURE__ */ new Set();
|
|
5470
|
+
for (const device of devices.list()) {
|
|
5471
|
+
const r = device.liveActivity;
|
|
5472
|
+
if (!r) continue;
|
|
5473
|
+
const key = device.deviceId + ":" + r.token;
|
|
5474
|
+
valid.add(key);
|
|
5475
|
+
if (device.revokedAt !== void 0 || ![
|
|
5476
|
+
"notifications.register",
|
|
5477
|
+
"sessions.read",
|
|
5478
|
+
"interactions.respond"
|
|
5479
|
+
].every((scope) => device.scopes?.some((value) => value === scope)) || r.expiresAt <= Date.now()) {
|
|
5480
|
+
devices.clearLiveActivity(device.deviceId, r.activityId, r.token);
|
|
5481
|
+
continue;
|
|
5482
|
+
}
|
|
5483
|
+
const state = r.endedState ?? liveActivityState(this.latest.get(r.sessionId));
|
|
5484
|
+
const fingerprint = JSON.stringify(state);
|
|
5485
|
+
if (this.sent.get(key) === fingerprint) continue;
|
|
5486
|
+
if (devices.authorized(device.deviceId)?.liveActivity?.token !== r.token) continue;
|
|
5487
|
+
const result = await this.send(r.token, r.environment, {
|
|
5488
|
+
kind: "liveactivity",
|
|
5489
|
+
event: r.endedState ? "end" : "update",
|
|
5490
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
5491
|
+
contentState: state
|
|
5492
|
+
}).catch(() => ({ outcome: "failed" }));
|
|
5493
|
+
if (result.outcome === "sent") this.sent.set(key, fingerprint);
|
|
5494
|
+
else if (result.outcome === "invalid-token") devices.clearLiveActivity(device.deviceId, r.activityId, r.token);
|
|
5495
|
+
else this.scheduler.changed();
|
|
5496
|
+
}
|
|
5497
|
+
for (const key of this.sent.keys()) if (!valid.has(key)) this.sent.delete(key);
|
|
5498
|
+
}
|
|
5499
|
+
dispose() {
|
|
5500
|
+
this.scheduler.dispose();
|
|
5501
|
+
}
|
|
5502
|
+
};
|
|
4805
5503
|
//#endregion
|
|
4806
5504
|
//#region src/phone-server.ts
|
|
4807
5505
|
/**
|
|
@@ -5250,20 +5948,78 @@ function apply(ctx, options) {
|
|
|
5250
5948
|
* token. Rules:
|
|
5251
5949
|
* - devices with a live WebSocket are skipped (they already got the WS
|
|
5252
5950
|
* frame and will raise the local notification themselves);
|
|
5951
|
+
* - only devices granted `notifications.register` are candidates — a
|
|
5952
|
+
* device whose scope was revoked must not receive offline pushes
|
|
5953
|
+
* (R1/P2 S→C permission policy);
|
|
5253
5954
|
* - each device is delivered on ITS registered environment (the build
|
|
5254
5955
|
* kind it self-reported), so sandbox and production devices coexist;
|
|
5255
5956
|
* - the device's per-category switches suppress muted categories;
|
|
5256
5957
|
* - only APNs' terminal Unregistered/ExpiredToken verdicts prune storage;
|
|
5257
5958
|
* BadDeviceToken may be an environment mismatch and stays diagnosable.
|
|
5258
5959
|
*/
|
|
5960
|
+
const widgetPush = new WidgetPushScheduler(async () => {
|
|
5961
|
+
if (!enabledNow()) return;
|
|
5962
|
+
const resolved = resolvePushConfig(currentConfig());
|
|
5963
|
+
if (!resolved.ok) return;
|
|
5964
|
+
const devices = auth.devices;
|
|
5965
|
+
const send = await senderFor(resolved.value);
|
|
5966
|
+
if (!devices || !send) return;
|
|
5967
|
+
const sent = /* @__PURE__ */ new Set();
|
|
5968
|
+
for (const device of devices.list()) {
|
|
5969
|
+
const registration = device.widgetApns;
|
|
5970
|
+
if (!registration || device.revokedAt !== void 0 || ![
|
|
5971
|
+
"notifications.register",
|
|
5972
|
+
"sessions.read",
|
|
5973
|
+
"interactions.respond"
|
|
5974
|
+
].every((scope) => device.scopes?.includes(scope)) || Date.now() - registration.updatedAt > 6048e5) continue;
|
|
5975
|
+
const key = registration.environment + ":" + registration.token;
|
|
5976
|
+
if (sent.has(key)) continue;
|
|
5977
|
+
sent.add(key);
|
|
5978
|
+
const { outcome, reason } = await send({
|
|
5979
|
+
deviceToken: registration.token,
|
|
5980
|
+
environment: registration.environment,
|
|
5981
|
+
notification: { kind: "widget" }
|
|
5982
|
+
});
|
|
5983
|
+
if (shouldPrunePushToken(outcome, reason)) devices.clearWidgetPushToken(device.deviceId, registration.token);
|
|
5984
|
+
if (resolved.value.kind === "relay" && reason === "HTTP 401" && enrollmentCell.token === resolved.value.token && enrollmentCell.enrollKey) {
|
|
5985
|
+
enrollmentCell.token = void 0;
|
|
5986
|
+
persistEnrollment();
|
|
5987
|
+
await ensureRelayEnrolled(resolved.value.url);
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
});
|
|
5991
|
+
const liveActivityPush = new LiveActivityPushManager(() => auth.devices ?? void 0, async (deviceToken, environment, notification) => {
|
|
5992
|
+
if (!enabledNow()) return { outcome: "failed" };
|
|
5993
|
+
const resolved = resolvePushConfig(currentConfig());
|
|
5994
|
+
if (!resolved.ok) return { outcome: "failed" };
|
|
5995
|
+
const send = await senderFor(resolved.value);
|
|
5996
|
+
if (!send) return { outcome: "failed" };
|
|
5997
|
+
const result = await send({
|
|
5998
|
+
deviceToken,
|
|
5999
|
+
environment,
|
|
6000
|
+
notification
|
|
6001
|
+
});
|
|
6002
|
+
if (resolved.value.kind === "relay" && result.reason === "HTTP 401" && enrollmentCell.token === resolved.value.token && enrollmentCell.enrollKey) {
|
|
6003
|
+
enrollmentCell.token = void 0;
|
|
6004
|
+
persistEnrollment();
|
|
6005
|
+
await ensureRelayEnrolled(resolved.value.url);
|
|
6006
|
+
}
|
|
6007
|
+
return result;
|
|
6008
|
+
});
|
|
5259
6009
|
const makePushOutlet = () => ({
|
|
6010
|
+
widgetChanged: () => widgetPush.changed(),
|
|
6011
|
+
liveActivityChanged: (sessions) => liveActivityPush.changed(sessions),
|
|
5260
6012
|
isAvailable: () => {
|
|
5261
6013
|
const resolved = resolvePushConfig(currentConfig());
|
|
5262
6014
|
if (!resolved.ok) return false;
|
|
5263
6015
|
if (resolved.value.kind === "relay" && !resolved.value.token) return false;
|
|
5264
6016
|
return true;
|
|
5265
6017
|
},
|
|
5266
|
-
fanOut: (
|
|
6018
|
+
fanOut: (sourceNotification) => {
|
|
6019
|
+
const notification = pushContent({
|
|
6020
|
+
...sourceNotification,
|
|
6021
|
+
hostAudience: auth.audience ?? void 0
|
|
6022
|
+
}, currentConfig().push?.contentMode);
|
|
5267
6023
|
(async () => {
|
|
5268
6024
|
let resolved = resolvePushConfig(currentConfig());
|
|
5269
6025
|
if (!resolved.ok && resolved.reason === "relay token not enrolled yet") {
|
|
@@ -5280,12 +6036,16 @@ function apply(ctx, options) {
|
|
|
5280
6036
|
const connectedIds = /* @__PURE__ */ new Set();
|
|
5281
6037
|
for (const connection of connections) {
|
|
5282
6038
|
const id = connection.connectedDeviceId;
|
|
5283
|
-
if (id) connectedIds.add(id);
|
|
6039
|
+
if (id && connection.suppressesAlertPush) connectedIds.add(id);
|
|
5284
6040
|
}
|
|
5285
6041
|
const candidates = devices.list().filter((device) => {
|
|
5286
6042
|
const registration = device.apns;
|
|
5287
6043
|
if (!registration) return false;
|
|
5288
6044
|
if (connectedIds.has(device.deviceId)) return false;
|
|
6045
|
+
if (!mayReceivePush(device, notification)) {
|
|
6046
|
+
if (currentConfig().debug === true) log(`push skip "${device.deviceName}": notification permission not granted`);
|
|
6047
|
+
return false;
|
|
6048
|
+
}
|
|
5289
6049
|
if (registration.categories?.[notification.category] === false) {
|
|
5290
6050
|
if (currentConfig().debug === true) log(`push skip "${device.deviceName}": category ${notification.category} muted`);
|
|
5291
6051
|
return false;
|
|
@@ -5840,7 +6600,7 @@ function apply(ctx, options) {
|
|
|
5840
6600
|
log("dsh 0.1.2 session bridge unavailable: " + String(error));
|
|
5841
6601
|
return;
|
|
5842
6602
|
}
|
|
5843
|
-
const bridge = new HostBridge(proxy, cfg.historyBufferMax);
|
|
6603
|
+
const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"));
|
|
5844
6604
|
bridge.setPushOutlet(makePushOutlet());
|
|
5845
6605
|
state.bridge = bridge;
|
|
5846
6606
|
bridge.start();
|
|
@@ -5883,6 +6643,8 @@ function apply(ctx, options) {
|
|
|
5883
6643
|
const sender = cachedSender;
|
|
5884
6644
|
cachedSender = void 0;
|
|
5885
6645
|
updateChecker.dispose();
|
|
6646
|
+
widgetPush.dispose();
|
|
6647
|
+
liveActivityPush.dispose();
|
|
5886
6648
|
const wssClosed = new Promise((resolve) => wss.close(() => resolve()));
|
|
5887
6649
|
await Promise.allSettled([
|
|
5888
6650
|
enrollmentWriteTail,
|
|
@@ -5892,6 +6654,6 @@ function apply(ctx, options) {
|
|
|
5892
6654
|
}, "deeppilot: process resources");
|
|
5893
6655
|
}
|
|
5894
6656
|
//#endregion
|
|
5895
|
-
export { Config, HostBridge, apply, inject, name, shouldPrunePushToken, shouldReEnrollRelayToken };
|
|
6657
|
+
export { Config, HostBridge, apply, inject, mayReceivePush, name, shouldPrunePushToken, shouldReEnrollRelayToken };
|
|
5896
6658
|
|
|
5897
6659
|
//# sourceMappingURL=index.js.map
|