dsh-deeppilot 0.8.2 → 0.8.3
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 +9 -3
- package/README.md +9 -6
- package/README.zh-CN.md +8 -6
- package/SKILL.md +87 -0
- package/docs/DSH_RELEASE_MEMORY.md +98 -0
- package/lib/client.js +426 -126
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +170 -2
- package/lib/index.js +964 -119
- package/lib/index.js.map +1 -1
- package/package.json +18 -16
package/lib/index.js
CHANGED
|
@@ -180,13 +180,13 @@ var PromptDeliveryJournal = class {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
};
|
|
183
|
-
const journals = /* @__PURE__ */ new Map();
|
|
183
|
+
const journals$1 = /* @__PURE__ */ new Map();
|
|
184
184
|
function openDeliveryJournal(path) {
|
|
185
185
|
if (!path) return new PromptDeliveryJournal();
|
|
186
|
-
let journal = journals.get(path);
|
|
186
|
+
let journal = journals$1.get(path);
|
|
187
187
|
if (!journal) {
|
|
188
188
|
journal = new PromptDeliveryJournal(path);
|
|
189
|
-
journals.set(path, journal);
|
|
189
|
+
journals$1.set(path, journal);
|
|
190
190
|
}
|
|
191
191
|
return journal;
|
|
192
192
|
}
|
|
@@ -213,6 +213,29 @@ function validateRequest(type, value) {
|
|
|
213
213
|
if (type.startsWith("c2s.session.") && type !== "c2s.session.create" && !text(p.sessionId)) return "invalid sessionId";
|
|
214
214
|
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";
|
|
215
215
|
if (type === "c2s.directory.list" && !optional("path", (v) => text(v, 32768, false))) return "invalid path";
|
|
216
|
+
if (type === "c2s.schedule.list" && !text(p.sessionId)) return "invalid sessionId";
|
|
217
|
+
if (type === "c2s.schedule.history" && (!text(p.sessionId) || !text(p.id) || !integer(p.limit, 1, 100) || !optional("before", (v) => text(v)))) return "invalid schedule history";
|
|
218
|
+
if (type === "c2s.schedule.create") {
|
|
219
|
+
if (!validSendId(p.clientRequestId) || !text(p.sessionId) || !text(p.title, 120) || !text(p.prompt, 262144)) return "invalid schedule create fields";
|
|
220
|
+
if ([
|
|
221
|
+
"after_seconds",
|
|
222
|
+
"at",
|
|
223
|
+
"every_seconds",
|
|
224
|
+
"daily",
|
|
225
|
+
"weekly",
|
|
226
|
+
"cron"
|
|
227
|
+
].filter((key) => p[key] !== void 0).length !== 1) return "schedule requires exactly one selector";
|
|
228
|
+
if (p.after_seconds !== void 0 && !integer(p.after_seconds, 1, Number.MAX_SAFE_INTEGER)) return "invalid after_seconds";
|
|
229
|
+
if (p.every_seconds !== void 0 && !integer(p.every_seconds, 60, Number.MAX_SAFE_INTEGER)) return "invalid every_seconds";
|
|
230
|
+
if (p.at !== void 0 && typeof p.at !== "string" && (typeof p.at !== "object" || p.at === null || Array.isArray(p.at))) return "invalid at selector";
|
|
231
|
+
}
|
|
232
|
+
if (type === "c2s.schedule.update") {
|
|
233
|
+
if (!validSendId(p.clientRequestId) || !text(p.sessionId) || !text(p.id) || !p.expected || typeof p.expected !== "object" || Array.isArray(p.expected)) return "invalid schedule update fields";
|
|
234
|
+
if (!optional("title", (v) => text(v, 120)) || !optional("prompt", (v) => text(v, 262144))) return "invalid schedule update content";
|
|
235
|
+
if (p.change !== void 0 && (!p.change || typeof p.change !== "object" || Array.isArray(p.change))) return "invalid schedule timing change";
|
|
236
|
+
}
|
|
237
|
+
if (type === "c2s.schedule.delete" && (!validSendId(p.clientRequestId) || !text(p.sessionId) || !text(p.id))) return "invalid schedule delete fields";
|
|
238
|
+
if (type === "c2s.session.fork" && (!validSendId(p.clientRequestId) || !optional("atSeq", (v) => integer(v, 0, Number.MAX_SAFE_INTEGER)))) return "invalid session fork fields";
|
|
216
239
|
if (type === "c2s.device.revoke" && !optional("deviceId", (v) => text(v))) return "invalid deviceId";
|
|
217
240
|
if (type === "c2s.workspace.create" && !text(p.path, 32768)) return "invalid path";
|
|
218
241
|
if (type === "c2s.session.open" && !optional("tailCount", (v) => integer(v, 1, 1e4))) return "invalid tailCount";
|
|
@@ -256,6 +279,112 @@ function validateRequest(type, value) {
|
|
|
256
279
|
}
|
|
257
280
|
}
|
|
258
281
|
//#endregion
|
|
282
|
+
//#region src/connection-policy.ts
|
|
283
|
+
const AUTH_TIMEOUT_MS = 35e3;
|
|
284
|
+
const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
285
|
+
"image/png",
|
|
286
|
+
"image/jpeg",
|
|
287
|
+
"image/webp",
|
|
288
|
+
"image/gif"
|
|
289
|
+
]);
|
|
290
|
+
function sanitizeDeviceField(value, maxChars) {
|
|
291
|
+
return (typeof value === "string" ? value : String(value ?? "")).replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, maxChars);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Runtime shape guard for a frame after JSON.parse. The old `as Envelope`
|
|
295
|
+
* cast alone let JSON `null` reach `env.v` and let a missing or mistyped
|
|
296
|
+
* `type` reach `requiredScope()`'s string operations — one anonymous frame
|
|
297
|
+
* could crash the host process. Reject anything that is not a plain object
|
|
298
|
+
* with a numeric version and a non-empty string type, so field access below
|
|
299
|
+
* is always safe. The payload is intentionally opaque here; each handler
|
|
300
|
+
* validates its own payload shape.
|
|
301
|
+
*/
|
|
302
|
+
function isEnvelope(value) {
|
|
303
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
304
|
+
const frame = value;
|
|
305
|
+
if (typeof frame.v !== "number") return false;
|
|
306
|
+
if (typeof frame.type !== "string" || frame.type.length === 0) return false;
|
|
307
|
+
if (frame.id !== void 0 && typeof frame.id !== "string") return false;
|
|
308
|
+
if (frame.ts !== void 0 && typeof frame.ts !== "number") return false;
|
|
309
|
+
if (frame.seq !== void 0 && typeof frame.seq !== "number") return false;
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function requiredScope(type) {
|
|
313
|
+
if (typeof type !== "string") return void 0;
|
|
314
|
+
if (type === "c2s.ping" || type === "c2s.resume") return void 0;
|
|
315
|
+
if (type === "c2s.device.revoke") return void 0;
|
|
316
|
+
if (type === "c2s.session.sendPrompt" || type === "c2s.session.delivery") return "prompt.send";
|
|
317
|
+
if (type === "c2s.pending.list") return "interactions.respond";
|
|
318
|
+
if (type === "c2s.approval.respond" || type === "c2s.question.respond") return "interactions.respond";
|
|
319
|
+
if (type === "c2s.liveActivity.register" || type === "c2s.liveActivity.unregister") return "notifications.register";
|
|
320
|
+
if (type === "c2s.push.register" || type === "c2s.widget.push.register") return "notifications.register";
|
|
321
|
+
if (type.startsWith("c2s.schedule.")) return "schedule.manage";
|
|
322
|
+
if (type === "c2s.workspace.create" || type === "c2s.session.create" || type === "c2s.session.fork" || type === "c2s.session.rename" || type === "c2s.session.archive" || type === "c2s.session.unarchive" || type === "c2s.session.cancel" || type === "c2s.session.selectModel") return "sessions.manage";
|
|
323
|
+
if (type.startsWith("c2s.")) return "sessions.read";
|
|
324
|
+
}
|
|
325
|
+
/** Broadcast and replay authorization. Unknown frame types fail closed. */
|
|
326
|
+
const PUSH_SCOPE_BY_TYPE = {
|
|
327
|
+
"s2c.session.event": "sessions.read",
|
|
328
|
+
"s2c.sessions.delta": "sessions.read",
|
|
329
|
+
"s2c.session.tail": "sessions.read",
|
|
330
|
+
"s2c.history.page": "sessions.read",
|
|
331
|
+
"s2c.pending.approval": "interactions.respond",
|
|
332
|
+
"s2c.pending.question": "interactions.respond",
|
|
333
|
+
"s2c.pending.cleared": "interactions.respond",
|
|
334
|
+
"s2c.schedule.changed": "schedule.manage"
|
|
335
|
+
};
|
|
336
|
+
function pushScopeFor(type, payload) {
|
|
337
|
+
const fixed = PUSH_SCOPE_BY_TYPE[type];
|
|
338
|
+
if (fixed !== void 0) return fixed;
|
|
339
|
+
if (type === "s2c.notify") {
|
|
340
|
+
const category = payload?.category;
|
|
341
|
+
if (category === "approval.required" || category === "question.asked") return "interactions.respond";
|
|
342
|
+
if (category === "turn.completed" || category === "session.error") return "sessions.read";
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function sanitizeImageName(value) {
|
|
346
|
+
return value.replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, 120);
|
|
347
|
+
}
|
|
348
|
+
function sanitizeDocumentField(value, maxChars) {
|
|
349
|
+
return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
|
|
350
|
+
}
|
|
351
|
+
/** Error code for a failed approval/question response outcome. */
|
|
352
|
+
function pendingResponseErrorCode(reason) {
|
|
353
|
+
switch (reason) {
|
|
354
|
+
case "not-pending": return "E_NOT_FOUND";
|
|
355
|
+
case "bad-response": return "E_PROTOCOL";
|
|
356
|
+
case "transport": return "E_INTERNAL";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Human-readable failure detail; `question not pending` must only ever mean
|
|
360
|
+
* "nothing pending", never "the host rejected the answer". */
|
|
361
|
+
function pendingResponseMessage(kind, reason) {
|
|
362
|
+
switch (reason) {
|
|
363
|
+
case "not-pending": return kind + " not pending";
|
|
364
|
+
case "bad-response": return kind + " answer rejected by host: answer does not match the asked questions";
|
|
365
|
+
case "transport": return "host connection failed while answering " + kind;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function scheduleManagementErrorCode(kind) {
|
|
369
|
+
switch (kind) {
|
|
370
|
+
case "unsupported": return "E_UNSUPPORTED";
|
|
371
|
+
case "not-found": return "E_NOT_FOUND";
|
|
372
|
+
case "invalid": return "E_PROTOCOL";
|
|
373
|
+
case "conflict":
|
|
374
|
+
case "busy": return "E_BUSY";
|
|
375
|
+
default: return "E_INTERNAL";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function managementErrorCode(kind) {
|
|
379
|
+
switch (kind) {
|
|
380
|
+
case "unsupported": return "E_UNSUPPORTED";
|
|
381
|
+
case "not-found": return "E_NOT_FOUND";
|
|
382
|
+
case "busy": return "E_BUSY";
|
|
383
|
+
case "invalid": return "E_PROTOCOL";
|
|
384
|
+
case "internal": return "E_INTERNAL";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
259
388
|
//#region src/device-auth.ts
|
|
260
389
|
function getPairingCodeTtlMs() {
|
|
261
390
|
return Number(process.env.DEEPPILOT_PAIRING_TTL_MS) || 3e5;
|
|
@@ -266,7 +395,8 @@ const DEVICE_SCOPES = [
|
|
|
266
395
|
"prompt.send",
|
|
267
396
|
"sessions.manage",
|
|
268
397
|
"interactions.respond",
|
|
269
|
-
"notifications.register"
|
|
398
|
+
"notifications.register",
|
|
399
|
+
"schedule.manage"
|
|
270
400
|
];
|
|
271
401
|
const DEFAULT_DEVICE_SCOPES = DEVICE_SCOPES;
|
|
272
402
|
async function loadOrCreateHostAudience(path) {
|
|
@@ -426,6 +556,10 @@ async function migrateLegacyBridgeDataDir() {
|
|
|
426
556
|
throw error;
|
|
427
557
|
}
|
|
428
558
|
}
|
|
559
|
+
/** Name shown to the operator: a user label when present, otherwise the app-reported model name. */
|
|
560
|
+
function deviceDisplayName(record) {
|
|
561
|
+
return record.customName?.trim() ? record.customName : record.deviceName;
|
|
562
|
+
}
|
|
429
563
|
/** Hex shape of an APNs device token as delivered by iOS (usually 64 chars). */
|
|
430
564
|
const APNS_TOKEN_PATTERN = /^[0-9a-f]{32,512}$/;
|
|
431
565
|
function isValidApnsToken(token) {
|
|
@@ -461,6 +595,7 @@ var DeviceStore = class DeviceStore {
|
|
|
461
595
|
const next = {
|
|
462
596
|
deviceId,
|
|
463
597
|
deviceName: record.deviceName,
|
|
598
|
+
...existing?.customName ? { customName: existing.customName } : {},
|
|
464
599
|
appVersion: record.appVersion,
|
|
465
600
|
publicKey: record.publicKey,
|
|
466
601
|
fingerprint: fingerprintForPublicKey(record.publicKey),
|
|
@@ -489,6 +624,28 @@ var DeviceStore = class DeviceStore {
|
|
|
489
624
|
record.lastSeenTs = now;
|
|
490
625
|
this.flush();
|
|
491
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* Set or clear the operator-facing device label. The phone-reported name is
|
|
629
|
+
* intentionally left untouched so a reconnect cannot erase this value.
|
|
630
|
+
* Returns the effective display name, or null when the device is not active.
|
|
631
|
+
*/
|
|
632
|
+
async setCustomName(deviceId, customName) {
|
|
633
|
+
const record = this.authorized(deviceId);
|
|
634
|
+
if (!record) return null;
|
|
635
|
+
const normalized = customName === null ? "" : sanitizeDeviceField(customName, 64);
|
|
636
|
+
if (normalized === (record.customName ?? "")) return deviceDisplayName(record);
|
|
637
|
+
const previous = record.customName;
|
|
638
|
+
if (normalized === "") delete record.customName;
|
|
639
|
+
else record.customName = normalized;
|
|
640
|
+
try {
|
|
641
|
+
await this.flush();
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (previous === void 0) delete record.customName;
|
|
644
|
+
else record.customName = previous;
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
return deviceDisplayName(record);
|
|
648
|
+
}
|
|
492
649
|
revoke(deviceId, now) {
|
|
493
650
|
const record = this.devices.get(deviceId);
|
|
494
651
|
if (!record || record.revokedAt !== void 0) return false;
|
|
@@ -627,100 +784,6 @@ var DeviceStore = class DeviceStore {
|
|
|
627
784
|
}
|
|
628
785
|
};
|
|
629
786
|
//#endregion
|
|
630
|
-
//#region src/connection-policy.ts
|
|
631
|
-
const AUTH_TIMEOUT_MS = 35e3;
|
|
632
|
-
const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
633
|
-
"image/png",
|
|
634
|
-
"image/jpeg",
|
|
635
|
-
"image/webp",
|
|
636
|
-
"image/gif"
|
|
637
|
-
]);
|
|
638
|
-
function sanitizeDeviceField(value, maxChars) {
|
|
639
|
-
return (typeof value === "string" ? value : String(value ?? "")).replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, maxChars);
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Runtime shape guard for a frame after JSON.parse. The old `as Envelope`
|
|
643
|
-
* cast alone let JSON `null` reach `env.v` and let a missing or mistyped
|
|
644
|
-
* `type` reach `requiredScope()`'s string operations — one anonymous frame
|
|
645
|
-
* could crash the host process. Reject anything that is not a plain object
|
|
646
|
-
* with a numeric version and a non-empty string type, so field access below
|
|
647
|
-
* is always safe. The payload is intentionally opaque here; each handler
|
|
648
|
-
* validates its own payload shape.
|
|
649
|
-
*/
|
|
650
|
-
function isEnvelope(value) {
|
|
651
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
652
|
-
const frame = value;
|
|
653
|
-
if (typeof frame.v !== "number") return false;
|
|
654
|
-
if (typeof frame.type !== "string" || frame.type.length === 0) return false;
|
|
655
|
-
if (frame.id !== void 0 && typeof frame.id !== "string") return false;
|
|
656
|
-
if (frame.ts !== void 0 && typeof frame.ts !== "number") return false;
|
|
657
|
-
if (frame.seq !== void 0 && typeof frame.seq !== "number") return false;
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
function requiredScope(type) {
|
|
661
|
-
if (typeof type !== "string") return void 0;
|
|
662
|
-
if (type === "c2s.ping" || type === "c2s.resume") return void 0;
|
|
663
|
-
if (type === "c2s.device.revoke") return void 0;
|
|
664
|
-
if (type === "c2s.session.sendPrompt" || type === "c2s.session.delivery") return "prompt.send";
|
|
665
|
-
if (type === "c2s.pending.list") return "interactions.respond";
|
|
666
|
-
if (type === "c2s.approval.respond" || type === "c2s.question.respond") return "interactions.respond";
|
|
667
|
-
if (type === "c2s.liveActivity.register" || type === "c2s.liveActivity.unregister") return "notifications.register";
|
|
668
|
-
if (type === "c2s.push.register" || type === "c2s.widget.push.register") return "notifications.register";
|
|
669
|
-
if (type === "c2s.workspace.create" || type === "c2s.session.create" || type === "c2s.session.rename" || type === "c2s.session.archive" || type === "c2s.session.unarchive" || type === "c2s.session.cancel" || type === "c2s.session.selectModel") return "sessions.manage";
|
|
670
|
-
if (type.startsWith("c2s.")) return "sessions.read";
|
|
671
|
-
}
|
|
672
|
-
/** Broadcast and replay authorization. Unknown frame types fail closed. */
|
|
673
|
-
const PUSH_SCOPE_BY_TYPE = {
|
|
674
|
-
"s2c.session.event": "sessions.read",
|
|
675
|
-
"s2c.sessions.delta": "sessions.read",
|
|
676
|
-
"s2c.session.tail": "sessions.read",
|
|
677
|
-
"s2c.history.page": "sessions.read",
|
|
678
|
-
"s2c.pending.approval": "interactions.respond",
|
|
679
|
-
"s2c.pending.question": "interactions.respond",
|
|
680
|
-
"s2c.pending.cleared": "interactions.respond"
|
|
681
|
-
};
|
|
682
|
-
function pushScopeFor(type, payload) {
|
|
683
|
-
const fixed = PUSH_SCOPE_BY_TYPE[type];
|
|
684
|
-
if (fixed !== void 0) return fixed;
|
|
685
|
-
if (type === "s2c.notify") {
|
|
686
|
-
const category = payload?.category;
|
|
687
|
-
if (category === "approval.required" || category === "question.asked") return "interactions.respond";
|
|
688
|
-
if (category === "turn.completed" || category === "session.error") return "sessions.read";
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
function sanitizeImageName(value) {
|
|
692
|
-
return value.replace(/[\u0000-\u001F\u007F]/g, "").trim().slice(0, 120);
|
|
693
|
-
}
|
|
694
|
-
function sanitizeDocumentField(value, maxChars) {
|
|
695
|
-
return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
|
|
696
|
-
}
|
|
697
|
-
/** Error code for a failed approval/question response outcome. */
|
|
698
|
-
function pendingResponseErrorCode(reason) {
|
|
699
|
-
switch (reason) {
|
|
700
|
-
case "not-pending": return "E_NOT_FOUND";
|
|
701
|
-
case "bad-response": return "E_PROTOCOL";
|
|
702
|
-
case "transport": return "E_INTERNAL";
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
/** Human-readable failure detail; `question not pending` must only ever mean
|
|
706
|
-
* "nothing pending", never "the host rejected the answer". */
|
|
707
|
-
function pendingResponseMessage(kind, reason) {
|
|
708
|
-
switch (reason) {
|
|
709
|
-
case "not-pending": return kind + " not pending";
|
|
710
|
-
case "bad-response": return kind + " answer rejected by host: answer does not match the asked questions";
|
|
711
|
-
case "transport": return "host connection failed while answering " + kind;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
function managementErrorCode(kind) {
|
|
715
|
-
switch (kind) {
|
|
716
|
-
case "unsupported": return "E_UNSUPPORTED";
|
|
717
|
-
case "not-found": return "E_NOT_FOUND";
|
|
718
|
-
case "busy": return "E_BUSY";
|
|
719
|
-
case "invalid": return "E_PROTOCOL";
|
|
720
|
-
case "internal": return "E_INTERNAL";
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
//#endregion
|
|
724
787
|
//#region src/connection.ts
|
|
725
788
|
/**
|
|
726
789
|
* One connected phone. Implements BridgeSink so the HostBridge can push
|
|
@@ -942,6 +1005,7 @@ var BridgeConnection = class {
|
|
|
942
1005
|
this.fail(env.id, "E_FORBIDDEN", `scope ${required} required`);
|
|
943
1006
|
return;
|
|
944
1007
|
}
|
|
1008
|
+
if (env.type.startsWith("c2s.schedule.") && !this.scopes.has("sessions.read")) return this.fail(env.id, "E_FORBIDDEN", "scope sessions.read required");
|
|
945
1009
|
const invalid = validateRequest(env.type, env.payload);
|
|
946
1010
|
if (invalid) return this.fail(env.id, "E_PROTOCOL", invalid);
|
|
947
1011
|
switch (env.type) {
|
|
@@ -1027,6 +1091,24 @@ var BridgeConnection = class {
|
|
|
1027
1091
|
this.send("s2c.ack", { sessionId: newId }, env.id);
|
|
1028
1092
|
return;
|
|
1029
1093
|
}
|
|
1094
|
+
case "c2s.session.fork": {
|
|
1095
|
+
const p = env.payload;
|
|
1096
|
+
if (!this.deps.bridge.capabilities.sessionFork) return this.fail(env.id, "E_UNSUPPORTED", "session fork unavailable on this host version");
|
|
1097
|
+
const result = await this.deps.bridge.forkSession(this.deviceId, {
|
|
1098
|
+
sessionId: p.sessionId,
|
|
1099
|
+
clientRequestId: p.clientRequestId,
|
|
1100
|
+
...p.atSeq !== void 0 ? { atSeq: p.atSeq } : {}
|
|
1101
|
+
});
|
|
1102
|
+
if (!result.ok) return this.fail(env.id, scheduleManagementErrorCode(result.kind), result.message);
|
|
1103
|
+
this.send("s2c.session.forked", {
|
|
1104
|
+
clientRequestId: p.clientRequestId,
|
|
1105
|
+
sourceSessionId: p.sessionId,
|
|
1106
|
+
sessionId: result.value.sessionId,
|
|
1107
|
+
...p.atSeq !== void 0 ? { atSeq: p.atSeq } : {},
|
|
1108
|
+
...result.replayed ? { replayed: true } : {}
|
|
1109
|
+
}, env.id);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1030
1112
|
case "c2s.session.rename": {
|
|
1031
1113
|
const p = env.payload;
|
|
1032
1114
|
const title = typeof p?.title === "string" ? p.title.trim() : "";
|
|
@@ -1133,6 +1215,67 @@ var BridgeConnection = class {
|
|
|
1133
1215
|
this.send("s2c.ack", this.deps.bridge.promptDeliveries.lookup(this.deviceId, p.sessionId, p.clientSendId), env.id);
|
|
1134
1216
|
return;
|
|
1135
1217
|
}
|
|
1218
|
+
case "c2s.schedule.list": {
|
|
1219
|
+
const p = env.payload;
|
|
1220
|
+
if (!this.deps.bridge.capabilities.schedules) return this.fail(env.id, "E_UNSUPPORTED", "schedules unavailable on this host version");
|
|
1221
|
+
const result = await this.deps.bridge.listSchedules(p.sessionId);
|
|
1222
|
+
if (!result.ok) return this.fail(env.id, scheduleManagementErrorCode(result.kind), result.message);
|
|
1223
|
+
this.send("s2c.schedule.snapshot", {
|
|
1224
|
+
sessionId: p.sessionId,
|
|
1225
|
+
tasks: result.value
|
|
1226
|
+
}, env.id);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
case "c2s.schedule.history": {
|
|
1230
|
+
const p = env.payload;
|
|
1231
|
+
if (!this.deps.bridge.capabilities.schedules) return this.fail(env.id, "E_UNSUPPORTED", "schedules unavailable on this host version");
|
|
1232
|
+
const result = await this.deps.bridge.scheduleHistory(p.sessionId, p.id, p.limit, p.before);
|
|
1233
|
+
if (!result.ok) return this.fail(env.id, scheduleManagementErrorCode(result.kind), result.message);
|
|
1234
|
+
this.send("s2c.schedule.history", {
|
|
1235
|
+
sessionId: p.sessionId,
|
|
1236
|
+
history: result.value
|
|
1237
|
+
}, env.id);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
case "c2s.schedule.create": {
|
|
1241
|
+
if (!this.deps.bridge.capabilities.schedules) return this.fail(env.id, "E_UNSUPPORTED", "schedules unavailable on this host version");
|
|
1242
|
+
const p = env.payload;
|
|
1243
|
+
const result = await this.deps.bridge.createSchedule(this.deviceId, p);
|
|
1244
|
+
if (!result.ok) return this.fail(env.id, scheduleManagementErrorCode(result.kind), result.message);
|
|
1245
|
+
this.send("s2c.schedule.updated", {
|
|
1246
|
+
clientRequestId: p.clientRequestId,
|
|
1247
|
+
sessionId: p.sessionId,
|
|
1248
|
+
task: result.value,
|
|
1249
|
+
...result.replayed ? { replayed: true } : {}
|
|
1250
|
+
}, env.id);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
case "c2s.schedule.update": {
|
|
1254
|
+
if (!this.deps.bridge.capabilities.schedules) return this.fail(env.id, "E_UNSUPPORTED", "schedules unavailable on this host version");
|
|
1255
|
+
const p = env.payload;
|
|
1256
|
+
const result = await this.deps.bridge.updateSchedule(this.deviceId, p);
|
|
1257
|
+
if (!result.ok) return this.fail(env.id, scheduleManagementErrorCode(result.kind), result.message);
|
|
1258
|
+
this.send("s2c.schedule.updated", {
|
|
1259
|
+
clientRequestId: p.clientRequestId,
|
|
1260
|
+
sessionId: p.sessionId,
|
|
1261
|
+
task: result.value,
|
|
1262
|
+
...result.replayed ? { replayed: true } : {}
|
|
1263
|
+
}, env.id);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
case "c2s.schedule.delete": {
|
|
1267
|
+
if (!this.deps.bridge.capabilities.schedules) return this.fail(env.id, "E_UNSUPPORTED", "schedules unavailable on this host version");
|
|
1268
|
+
const p = env.payload;
|
|
1269
|
+
const result = await this.deps.bridge.deleteSchedule(this.deviceId, p);
|
|
1270
|
+
if (!result.ok) return this.fail(env.id, scheduleManagementErrorCode(result.kind), result.message);
|
|
1271
|
+
this.send("s2c.schedule.updated", {
|
|
1272
|
+
clientRequestId: p.clientRequestId,
|
|
1273
|
+
sessionId: p.sessionId,
|
|
1274
|
+
deleted: true,
|
|
1275
|
+
...result.replayed ? { replayed: true } : {}
|
|
1276
|
+
}, env.id);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1136
1279
|
case "c2s.session.sendPrompt": {
|
|
1137
1280
|
const p = env.payload;
|
|
1138
1281
|
const text = typeof p?.text === "string" ? p.text : "";
|
|
@@ -1873,12 +2016,219 @@ function truncateUtf8(value, maxBytes) {
|
|
|
1873
2016
|
if (Buffer.byteLength(candidate, "utf8") <= maxBytes) low = mid;
|
|
1874
2017
|
else high = mid - 1;
|
|
1875
2018
|
}
|
|
1876
|
-
let end = low;
|
|
1877
|
-
if (end > 0 && /[\uD800-\uDBFF]/.test(value[end - 1])) end -= 1;
|
|
1878
|
-
return value.slice(0, end);
|
|
2019
|
+
let end = low;
|
|
2020
|
+
if (end > 0 && /[\uD800-\uDBFF]/.test(value[end - 1])) end -= 1;
|
|
2021
|
+
return value.slice(0, end);
|
|
2022
|
+
}
|
|
2023
|
+
function summarizeResult(content) {
|
|
2024
|
+
return truncate(contentText(content).replace(/\s+/g, " ").trim(), 90);
|
|
2025
|
+
}
|
|
2026
|
+
//#endregion
|
|
2027
|
+
//#region src/mutation-journal.ts
|
|
2028
|
+
const RETENTION_MS = 6048e5;
|
|
2029
|
+
const CAPACITY = 1e4;
|
|
2030
|
+
/**
|
|
2031
|
+
* Small durable at-most-once journal for non-prompt mutations.
|
|
2032
|
+
*
|
|
2033
|
+
* Only a hash of the request content is persisted. The successful value is
|
|
2034
|
+
* kept in memory for the current process so a concurrent retry can receive the
|
|
2035
|
+
* same response; after a restart a replay is acknowledged and the client
|
|
2036
|
+
* refetches the authoritative resource. This avoids duplicating reminder
|
|
2037
|
+
* prompts or conversation data in a second journal.
|
|
2038
|
+
*/
|
|
2039
|
+
var MutationJournal = class {
|
|
2040
|
+
path;
|
|
2041
|
+
persistValues;
|
|
2042
|
+
entries = Object.create(null);
|
|
2043
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
2044
|
+
healthy = true;
|
|
2045
|
+
constructor(path, persistValues = false) {
|
|
2046
|
+
this.path = path;
|
|
2047
|
+
this.persistValues = persistValues;
|
|
2048
|
+
if (!path || !existsSync(path)) return;
|
|
2049
|
+
try {
|
|
2050
|
+
if (statSync(path).size > 4194304) throw new Error("oversized mutation journal");
|
|
2051
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2052
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.entries) || parsed.entries.length > CAPACITY) throw new Error("invalid mutation journal");
|
|
2053
|
+
for (const value of parsed.entries) {
|
|
2054
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid mutation entry");
|
|
2055
|
+
const entry = value;
|
|
2056
|
+
if (typeof entry.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(entry.fingerprint) || typeof entry.createdAt !== "number" || !Number.isFinite(entry.createdAt) || ![
|
|
2057
|
+
"accepted",
|
|
2058
|
+
"rejected",
|
|
2059
|
+
"unknown"
|
|
2060
|
+
].includes(String(entry.status))) throw new Error("invalid mutation entry");
|
|
2061
|
+
if (entry.status === "rejected" && !isMutationErrorCode(entry.code)) throw new Error("invalid mutation code");
|
|
2062
|
+
const key = String(value.key);
|
|
2063
|
+
if (!/^[a-f0-9]{64}$/.test(key)) throw new Error("invalid mutation key");
|
|
2064
|
+
this.entries[key] = {
|
|
2065
|
+
fingerprint: entry.fingerprint,
|
|
2066
|
+
createdAt: entry.createdAt,
|
|
2067
|
+
status: entry.status,
|
|
2068
|
+
...isMutationErrorCode(entry.code) ? { code: entry.code } : {},
|
|
2069
|
+
...this.persistValues && entry.value !== void 0 ? { value: entry.value } : {}
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
} catch {
|
|
2073
|
+
this.healthy = false;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
key(deviceId, id) {
|
|
2077
|
+
return createHash("sha256").update(JSON.stringify([deviceId, id])).digest("hex");
|
|
2078
|
+
}
|
|
2079
|
+
expired(id, now = Date.now()) {
|
|
2080
|
+
const age = now - Number(id.slice(0, 13));
|
|
2081
|
+
return age > RETENTION_MS || age < -3e5;
|
|
2082
|
+
}
|
|
2083
|
+
save() {
|
|
2084
|
+
if (!this.path) return;
|
|
2085
|
+
mkdirSync(dirname(this.path), {
|
|
2086
|
+
recursive: true,
|
|
2087
|
+
mode: 448
|
|
2088
|
+
});
|
|
2089
|
+
const temp = this.path + "." + randomUUID() + ".tmp";
|
|
2090
|
+
const fd = openSync(temp, "wx", 384);
|
|
2091
|
+
try {
|
|
2092
|
+
writeFileSync(fd, JSON.stringify({
|
|
2093
|
+
version: 1,
|
|
2094
|
+
entries: Object.entries(this.entries).map(([key, entry]) => ({
|
|
2095
|
+
key,
|
|
2096
|
+
...entry
|
|
2097
|
+
}))
|
|
2098
|
+
}));
|
|
2099
|
+
fsyncSync(fd);
|
|
2100
|
+
} finally {
|
|
2101
|
+
closeSync(fd);
|
|
2102
|
+
}
|
|
2103
|
+
renameSync(temp, this.path);
|
|
2104
|
+
const dir = openSync(dirname(this.path), "r");
|
|
2105
|
+
try {
|
|
2106
|
+
fsyncSync(dir);
|
|
2107
|
+
} finally {
|
|
2108
|
+
closeSync(dir);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
async dispatch(deviceId, id, content, operation) {
|
|
2112
|
+
if (!validSendId(id)) return {
|
|
2113
|
+
ok: false,
|
|
2114
|
+
code: "E_PROTOCOL",
|
|
2115
|
+
message: "invalid clientRequestId"
|
|
2116
|
+
};
|
|
2117
|
+
const key = this.key(deviceId, id);
|
|
2118
|
+
const fingerprint = createHash("sha256").update(JSON.stringify(content)).digest("hex");
|
|
2119
|
+
const existing = this.entries[key];
|
|
2120
|
+
if (existing !== void 0) {
|
|
2121
|
+
if (existing.fingerprint !== fingerprint) return {
|
|
2122
|
+
ok: false,
|
|
2123
|
+
code: "E_PROTOCOL",
|
|
2124
|
+
message: "clientRequestId was reused with different content",
|
|
2125
|
+
replayed: true
|
|
2126
|
+
};
|
|
2127
|
+
if (existing.status === "accepted") return {
|
|
2128
|
+
ok: true,
|
|
2129
|
+
value: existing.value,
|
|
2130
|
+
replayed: true
|
|
2131
|
+
};
|
|
2132
|
+
if (existing.status === "rejected" && existing.code !== void 0) return {
|
|
2133
|
+
ok: false,
|
|
2134
|
+
code: existing.code,
|
|
2135
|
+
replayed: true
|
|
2136
|
+
};
|
|
2137
|
+
return {
|
|
2138
|
+
ok: false,
|
|
2139
|
+
code: "E_INTERNAL",
|
|
2140
|
+
message: "mutation outcome is unknown; do not retry automatically",
|
|
2141
|
+
replayed: true
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
if (!this.healthy) return {
|
|
2145
|
+
ok: false,
|
|
2146
|
+
code: "E_INTERNAL",
|
|
2147
|
+
message: "mutation journal unavailable"
|
|
2148
|
+
};
|
|
2149
|
+
if (this.expired(id)) return {
|
|
2150
|
+
ok: false,
|
|
2151
|
+
code: "E_PROTOCOL",
|
|
2152
|
+
message: "clientRequestId is outside the retry window"
|
|
2153
|
+
};
|
|
2154
|
+
const now = Date.now();
|
|
2155
|
+
for (const [entryKey, entry] of Object.entries(this.entries)) if (now - entry.createdAt > 6051e5 && !this.inFlight.has(entryKey)) delete this.entries[entryKey];
|
|
2156
|
+
if (Object.keys(this.entries).length >= CAPACITY) return {
|
|
2157
|
+
ok: false,
|
|
2158
|
+
code: "E_BUSY",
|
|
2159
|
+
message: "mutation journal is full"
|
|
2160
|
+
};
|
|
2161
|
+
const entry = {
|
|
2162
|
+
fingerprint,
|
|
2163
|
+
createdAt: Number(id.slice(0, 13)),
|
|
2164
|
+
status: "unknown"
|
|
2165
|
+
};
|
|
2166
|
+
this.entries[key] = entry;
|
|
2167
|
+
try {
|
|
2168
|
+
this.save();
|
|
2169
|
+
} catch {
|
|
2170
|
+
this.healthy = false;
|
|
2171
|
+
return {
|
|
2172
|
+
ok: false,
|
|
2173
|
+
code: "E_INTERNAL",
|
|
2174
|
+
message: "mutation journal could not persist the request"
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
const run = (async () => {
|
|
2178
|
+
try {
|
|
2179
|
+
const result = await operation();
|
|
2180
|
+
if (result.ok) {
|
|
2181
|
+
entry.status = "accepted";
|
|
2182
|
+
if (this.persistValues) entry.value = result.value;
|
|
2183
|
+
return result;
|
|
2184
|
+
}
|
|
2185
|
+
entry.status = "rejected";
|
|
2186
|
+
entry.code = result.code;
|
|
2187
|
+
return {
|
|
2188
|
+
ok: false,
|
|
2189
|
+
code: result.code,
|
|
2190
|
+
message: result.message
|
|
2191
|
+
};
|
|
2192
|
+
} catch {
|
|
2193
|
+
return {
|
|
2194
|
+
ok: false,
|
|
2195
|
+
code: "E_INTERNAL",
|
|
2196
|
+
message: "mutation outcome is unknown; do not retry automatically"
|
|
2197
|
+
};
|
|
2198
|
+
} finally {
|
|
2199
|
+
try {
|
|
2200
|
+
this.save();
|
|
2201
|
+
} catch {
|
|
2202
|
+
this.healthy = false;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
})();
|
|
2206
|
+
this.inFlight.set(key, run);
|
|
2207
|
+
try {
|
|
2208
|
+
return await run;
|
|
2209
|
+
} finally {
|
|
2210
|
+
this.inFlight.delete(key);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
function isMutationErrorCode(value) {
|
|
2215
|
+
return typeof value === "string" && [
|
|
2216
|
+
"E_PROTOCOL",
|
|
2217
|
+
"E_NOT_FOUND",
|
|
2218
|
+
"E_BUSY",
|
|
2219
|
+
"E_UNSUPPORTED",
|
|
2220
|
+
"E_INTERNAL"
|
|
2221
|
+
].includes(value);
|
|
1879
2222
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
2223
|
+
const journals = /* @__PURE__ */ new Map();
|
|
2224
|
+
function openMutationJournal(path, persistValues = false) {
|
|
2225
|
+
if (!path) return new MutationJournal(void 0, persistValues);
|
|
2226
|
+
let journal = journals.get(path);
|
|
2227
|
+
if (!journal) {
|
|
2228
|
+
journal = new MutationJournal(path, persistValues);
|
|
2229
|
+
journals.set(path, journal);
|
|
2230
|
+
}
|
|
2231
|
+
return journal;
|
|
1882
2232
|
}
|
|
1883
2233
|
//#endregion
|
|
1884
2234
|
//#region src/host-bridge.ts
|
|
@@ -1910,10 +2260,14 @@ var HostBridge = class {
|
|
|
1910
2260
|
started = false;
|
|
1911
2261
|
disposed = false;
|
|
1912
2262
|
promptDeliveries;
|
|
1913
|
-
|
|
2263
|
+
scheduleMutations;
|
|
2264
|
+
forkMutations;
|
|
2265
|
+
constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath, scheduleJournalPath, forkJournalPath) {
|
|
1914
2266
|
this.apiProxy = apiProxy;
|
|
1915
2267
|
this.historyBufferMax = historyBufferMax;
|
|
1916
2268
|
this.promptDeliveries = openDeliveryJournal(deliveryJournalPath);
|
|
2269
|
+
this.scheduleMutations = openMutationJournal(scheduleJournalPath);
|
|
2270
|
+
this.forkMutations = openMutationJournal(forkJournalPath, true);
|
|
1917
2271
|
}
|
|
1918
2272
|
pushOutlet;
|
|
1919
2273
|
widgetFingerprint = "";
|
|
@@ -1935,8 +2289,10 @@ var HostBridge = class {
|
|
|
1935
2289
|
notifyAllCategories: true,
|
|
1936
2290
|
models: typeof this.apiProxy.sessions.models === "function" && typeof this.apiProxy.sessions.selectModel === "function",
|
|
1937
2291
|
sessionManagement: typeof this.apiProxy.sessions.rename === "function" && typeof this.apiProxy.workspace?.archiveSession === "function",
|
|
2292
|
+
sessionFork: typeof this.apiProxy.sessions.fork === "function",
|
|
1938
2293
|
sessionRestore: typeof this.apiProxy.workspace?.unarchiveSession === "function",
|
|
1939
2294
|
projectSelection: typeof this.apiProxy.workspace?.list === "function" && typeof this.apiProxy.workspace?.create === "function",
|
|
2295
|
+
schedules: typeof this.apiProxy.schedule?.list === "function" && typeof this.apiProxy.schedule?.create === "function",
|
|
1940
2296
|
push: this.pushOutlet?.isAvailable() === true,
|
|
1941
2297
|
widgetPush: true,
|
|
1942
2298
|
liveActivityPush: true,
|
|
@@ -2145,6 +2501,9 @@ var HostBridge = class {
|
|
|
2145
2501
|
this.refreshSummaries();
|
|
2146
2502
|
break;
|
|
2147
2503
|
}
|
|
2504
|
+
case "host/schedule-changed":
|
|
2505
|
+
this.record("s2c.schedule.changed", {});
|
|
2506
|
+
break;
|
|
2148
2507
|
case "host/session-status": {
|
|
2149
2508
|
const p = frame;
|
|
2150
2509
|
const row = this.summaries.get(String(p.sessionId));
|
|
@@ -2901,6 +3260,267 @@ var HostBridge = class {
|
|
|
2901
3260
|
return null;
|
|
2902
3261
|
}
|
|
2903
3262
|
}
|
|
3263
|
+
async forkSession(deviceId, payload) {
|
|
3264
|
+
const fork = this.apiProxy.sessions.fork;
|
|
3265
|
+
if (typeof fork !== "function") return {
|
|
3266
|
+
ok: false,
|
|
3267
|
+
kind: "unsupported",
|
|
3268
|
+
message: "session fork unavailable on this host version"
|
|
3269
|
+
};
|
|
3270
|
+
if (!this.summaries.has(payload.sessionId)) return {
|
|
3271
|
+
ok: false,
|
|
3272
|
+
kind: "not-found",
|
|
3273
|
+
message: "session not found"
|
|
3274
|
+
};
|
|
3275
|
+
if (this.archivedSessionIds.has(payload.sessionId) || this.subagentSessionIds.has(payload.sessionId)) return {
|
|
3276
|
+
ok: false,
|
|
3277
|
+
kind: "invalid",
|
|
3278
|
+
message: "archived or subagent sessions cannot be forked"
|
|
3279
|
+
};
|
|
3280
|
+
const result = await this.forkMutations.dispatch(deviceId, payload.clientRequestId, {
|
|
3281
|
+
sessionId: payload.sessionId,
|
|
3282
|
+
...payload.atSeq !== void 0 ? { atSeq: payload.atSeq } : {}
|
|
3283
|
+
}, async () => {
|
|
3284
|
+
const response = await fork.call(this.apiProxy.sessions, {
|
|
3285
|
+
rpcId: randomUUID(),
|
|
3286
|
+
payload: {
|
|
3287
|
+
sessionId: payload.sessionId,
|
|
3288
|
+
...payload.atSeq !== void 0 ? { atSeq: payload.atSeq } : {}
|
|
3289
|
+
}
|
|
3290
|
+
});
|
|
3291
|
+
if (!response.result) return {
|
|
3292
|
+
ok: false,
|
|
3293
|
+
code: "E_INTERNAL",
|
|
3294
|
+
message: "session fork returned no result"
|
|
3295
|
+
};
|
|
3296
|
+
if (!response.result.ok) return scheduleMutationError(response.result.error);
|
|
3297
|
+
return {
|
|
3298
|
+
ok: true,
|
|
3299
|
+
value: { sessionId: response.result.value.sessionId }
|
|
3300
|
+
};
|
|
3301
|
+
});
|
|
3302
|
+
if (result.ok) {
|
|
3303
|
+
await this.refreshSummaries();
|
|
3304
|
+
return {
|
|
3305
|
+
ok: true,
|
|
3306
|
+
value: result.value,
|
|
3307
|
+
replayed: result.replayed
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
return {
|
|
3311
|
+
ok: false,
|
|
3312
|
+
kind: scheduleErrorKind(result.code),
|
|
3313
|
+
message: result.message ?? result.code,
|
|
3314
|
+
replayed: result.replayed
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
async listSchedules(sessionId) {
|
|
3318
|
+
const schedule = this.apiProxy.schedule;
|
|
3319
|
+
if (schedule === void 0) return {
|
|
3320
|
+
ok: false,
|
|
3321
|
+
kind: "unsupported",
|
|
3322
|
+
message: "schedules unavailable on this host version"
|
|
3323
|
+
};
|
|
3324
|
+
try {
|
|
3325
|
+
const response = await schedule.list({
|
|
3326
|
+
rpcId: randomUUID(),
|
|
3327
|
+
payload: { sessionId }
|
|
3328
|
+
});
|
|
3329
|
+
if (!response.result) return {
|
|
3330
|
+
ok: false,
|
|
3331
|
+
kind: "internal",
|
|
3332
|
+
message: "schedule list returned no result"
|
|
3333
|
+
};
|
|
3334
|
+
if (!response.result.ok) return scheduleHostError(response.result.error);
|
|
3335
|
+
return {
|
|
3336
|
+
ok: true,
|
|
3337
|
+
value: response.result.value.tasks
|
|
3338
|
+
};
|
|
3339
|
+
} catch (error) {
|
|
3340
|
+
return {
|
|
3341
|
+
ok: false,
|
|
3342
|
+
kind: "internal",
|
|
3343
|
+
message: String(error)
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
async scheduleHistory(sessionId, id, limit, before) {
|
|
3348
|
+
const schedule = this.apiProxy.schedule;
|
|
3349
|
+
if (schedule === void 0) return {
|
|
3350
|
+
ok: false,
|
|
3351
|
+
kind: "unsupported",
|
|
3352
|
+
message: "schedules unavailable on this host version"
|
|
3353
|
+
};
|
|
3354
|
+
try {
|
|
3355
|
+
const response = await schedule.history({
|
|
3356
|
+
rpcId: randomUUID(),
|
|
3357
|
+
payload: {
|
|
3358
|
+
sessionId,
|
|
3359
|
+
id,
|
|
3360
|
+
limit,
|
|
3361
|
+
...before !== void 0 ? { before } : {}
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3364
|
+
if (!response.result) return {
|
|
3365
|
+
ok: false,
|
|
3366
|
+
kind: "internal",
|
|
3367
|
+
message: "schedule history returned no result"
|
|
3368
|
+
};
|
|
3369
|
+
if (!response.result.ok) return scheduleHostError(response.result.error);
|
|
3370
|
+
return {
|
|
3371
|
+
ok: true,
|
|
3372
|
+
value: response.result.value.history
|
|
3373
|
+
};
|
|
3374
|
+
} catch (error) {
|
|
3375
|
+
return {
|
|
3376
|
+
ok: false,
|
|
3377
|
+
kind: "internal",
|
|
3378
|
+
message: String(error)
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
async createSchedule(deviceId, payload) {
|
|
3383
|
+
const schedule = this.apiProxy.schedule;
|
|
3384
|
+
if (schedule === void 0) return {
|
|
3385
|
+
ok: false,
|
|
3386
|
+
kind: "unsupported",
|
|
3387
|
+
message: "schedules unavailable on this host version"
|
|
3388
|
+
};
|
|
3389
|
+
const { clientRequestId, sessionId, ...request } = payload;
|
|
3390
|
+
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, {
|
|
3391
|
+
sessionId,
|
|
3392
|
+
request
|
|
3393
|
+
}, async () => {
|
|
3394
|
+
const response = await schedule.create({
|
|
3395
|
+
rpcId: randomUUID(),
|
|
3396
|
+
payload: {
|
|
3397
|
+
sessionId,
|
|
3398
|
+
...request
|
|
3399
|
+
}
|
|
3400
|
+
});
|
|
3401
|
+
if (!response.result) return {
|
|
3402
|
+
ok: false,
|
|
3403
|
+
code: "E_INTERNAL",
|
|
3404
|
+
message: "schedule create returned no result"
|
|
3405
|
+
};
|
|
3406
|
+
if (!response.result.ok) return scheduleMutationError(response.result.error);
|
|
3407
|
+
return {
|
|
3408
|
+
ok: true,
|
|
3409
|
+
value: response.result.value
|
|
3410
|
+
};
|
|
3411
|
+
});
|
|
3412
|
+
if (result.ok) {
|
|
3413
|
+
this.record("s2c.schedule.changed", {});
|
|
3414
|
+
return {
|
|
3415
|
+
ok: true,
|
|
3416
|
+
value: result.value,
|
|
3417
|
+
replayed: result.replayed
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
return {
|
|
3421
|
+
ok: false,
|
|
3422
|
+
kind: scheduleErrorKind(result.code),
|
|
3423
|
+
message: result.message ?? result.code,
|
|
3424
|
+
replayed: result.replayed
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
async updateSchedule(deviceId, payload) {
|
|
3428
|
+
const schedule = this.apiProxy.schedule;
|
|
3429
|
+
if (schedule === void 0) return {
|
|
3430
|
+
ok: false,
|
|
3431
|
+
kind: "unsupported",
|
|
3432
|
+
message: "schedules unavailable on this host version"
|
|
3433
|
+
};
|
|
3434
|
+
const { clientRequestId, ...request } = payload;
|
|
3435
|
+
const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
|
|
3436
|
+
const response = await schedule.update({
|
|
3437
|
+
rpcId: randomUUID(),
|
|
3438
|
+
payload: request
|
|
3439
|
+
});
|
|
3440
|
+
if (!response.result) return {
|
|
3441
|
+
ok: false,
|
|
3442
|
+
code: "E_INTERNAL",
|
|
3443
|
+
message: "schedule update returned no result"
|
|
3444
|
+
};
|
|
3445
|
+
if (!response.result.ok) return scheduleMutationError(response.result.error);
|
|
3446
|
+
const value = response.result.value;
|
|
3447
|
+
if (value.updated !== true || value.record === void 0) return {
|
|
3448
|
+
ok: false,
|
|
3449
|
+
code: scheduleCodeToError(value.code),
|
|
3450
|
+
message: value.code ?? "schedule update rejected"
|
|
3451
|
+
};
|
|
3452
|
+
return {
|
|
3453
|
+
ok: true,
|
|
3454
|
+
value: value.record
|
|
3455
|
+
};
|
|
3456
|
+
});
|
|
3457
|
+
if (result.ok) {
|
|
3458
|
+
this.record("s2c.schedule.changed", {});
|
|
3459
|
+
return {
|
|
3460
|
+
ok: true,
|
|
3461
|
+
value: result.value,
|
|
3462
|
+
replayed: result.replayed
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
return {
|
|
3466
|
+
ok: false,
|
|
3467
|
+
kind: scheduleErrorKind(result.code),
|
|
3468
|
+
message: result.message ?? result.code,
|
|
3469
|
+
replayed: result.replayed
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
async deleteSchedule(deviceId, payload) {
|
|
3473
|
+
const schedule = this.apiProxy.schedule;
|
|
3474
|
+
if (schedule === void 0) return {
|
|
3475
|
+
ok: false,
|
|
3476
|
+
kind: "unsupported",
|
|
3477
|
+
message: "schedules unavailable on this host version"
|
|
3478
|
+
};
|
|
3479
|
+
const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
|
|
3480
|
+
sessionId: payload.sessionId,
|
|
3481
|
+
id: payload.id
|
|
3482
|
+
}, async () => {
|
|
3483
|
+
const response = await schedule.delete({
|
|
3484
|
+
rpcId: randomUUID(),
|
|
3485
|
+
payload: {
|
|
3486
|
+
sessionId: payload.sessionId,
|
|
3487
|
+
id: payload.id
|
|
3488
|
+
}
|
|
3489
|
+
});
|
|
3490
|
+
if (!response.result) return {
|
|
3491
|
+
ok: false,
|
|
3492
|
+
code: "E_INTERNAL",
|
|
3493
|
+
message: "schedule delete returned no result"
|
|
3494
|
+
};
|
|
3495
|
+
if (!response.result.ok) return scheduleMutationError(response.result.error);
|
|
3496
|
+
if (response.result.value.deleted !== true) return {
|
|
3497
|
+
ok: false,
|
|
3498
|
+
code: "E_NOT_FOUND",
|
|
3499
|
+
message: response.result.value.code ?? "schedule not found"
|
|
3500
|
+
};
|
|
3501
|
+
return {
|
|
3502
|
+
ok: true,
|
|
3503
|
+
value: {
|
|
3504
|
+
id: response.result.value.id,
|
|
3505
|
+
deleted: true
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
});
|
|
3509
|
+
if (result.ok) {
|
|
3510
|
+
this.record("s2c.schedule.changed", {});
|
|
3511
|
+
return {
|
|
3512
|
+
ok: true,
|
|
3513
|
+
value: result.value,
|
|
3514
|
+
replayed: result.replayed
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
return {
|
|
3518
|
+
ok: false,
|
|
3519
|
+
kind: scheduleErrorKind(result.code),
|
|
3520
|
+
message: result.message ?? result.code,
|
|
3521
|
+
replayed: result.replayed
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
2904
3524
|
async sendPrompt(sessionId, text, images = [], documents = []) {
|
|
2905
3525
|
try {
|
|
2906
3526
|
const content = [];
|
|
@@ -3228,6 +3848,72 @@ function hostModelError(error) {
|
|
|
3228
3848
|
};
|
|
3229
3849
|
}
|
|
3230
3850
|
}
|
|
3851
|
+
function scheduleHostError(error) {
|
|
3852
|
+
const message = error.message ?? error.code;
|
|
3853
|
+
switch (error.code) {
|
|
3854
|
+
case "schedule_not_found":
|
|
3855
|
+
case "delivery_cursor_not_found": return {
|
|
3856
|
+
ok: false,
|
|
3857
|
+
kind: "not-found",
|
|
3858
|
+
message
|
|
3859
|
+
};
|
|
3860
|
+
case "schedule_conflict": return {
|
|
3861
|
+
ok: false,
|
|
3862
|
+
kind: "conflict",
|
|
3863
|
+
message
|
|
3864
|
+
};
|
|
3865
|
+
case "invalid_prompt":
|
|
3866
|
+
case "invalid_selector":
|
|
3867
|
+
case "invalid_rule":
|
|
3868
|
+
case "invalid_time_zone":
|
|
3869
|
+
case "not_future":
|
|
3870
|
+
case "time_out_of_range":
|
|
3871
|
+
case "frequency_too_high": return {
|
|
3872
|
+
ok: false,
|
|
3873
|
+
kind: "invalid",
|
|
3874
|
+
message
|
|
3875
|
+
};
|
|
3876
|
+
case "schedule_ended": return {
|
|
3877
|
+
ok: false,
|
|
3878
|
+
kind: "invalid",
|
|
3879
|
+
message
|
|
3880
|
+
};
|
|
3881
|
+
default: return {
|
|
3882
|
+
ok: false,
|
|
3883
|
+
kind: "internal",
|
|
3884
|
+
message
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
function scheduleMutationError(error) {
|
|
3889
|
+
const result = scheduleHostError(error);
|
|
3890
|
+
if (result.ok) return {
|
|
3891
|
+
ok: false,
|
|
3892
|
+
code: "E_INTERNAL",
|
|
3893
|
+
message: "unexpected schedule result"
|
|
3894
|
+
};
|
|
3895
|
+
return {
|
|
3896
|
+
ok: false,
|
|
3897
|
+
code: result.kind === "not-found" ? "E_NOT_FOUND" : result.kind === "conflict" ? "E_BUSY" : result.kind === "invalid" ? "E_PROTOCOL" : result.kind === "unsupported" ? "E_UNSUPPORTED" : "E_INTERNAL",
|
|
3898
|
+
message: result.message
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
function scheduleCodeToError(code) {
|
|
3902
|
+
if (code === "schedule_not_found") return "E_NOT_FOUND";
|
|
3903
|
+
if (code === "schedule_conflict") return "E_BUSY";
|
|
3904
|
+
if (code === "schedule_ended") return "E_PROTOCOL";
|
|
3905
|
+
if (code === "invalid_prompt" || code === "invalid_selector" || code === "invalid_rule" || code === "invalid_time_zone" || code === "not_future" || code === "time_out_of_range" || code === "frequency_too_high") return "E_PROTOCOL";
|
|
3906
|
+
return "E_INTERNAL";
|
|
3907
|
+
}
|
|
3908
|
+
function scheduleErrorKind(code) {
|
|
3909
|
+
switch (code) {
|
|
3910
|
+
case "E_NOT_FOUND": return "not-found";
|
|
3911
|
+
case "E_BUSY": return "conflict";
|
|
3912
|
+
case "E_PROTOCOL": return "invalid";
|
|
3913
|
+
case "E_UNSUPPORTED": return "unsupported";
|
|
3914
|
+
default: return "internal";
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3231
3917
|
function hostSessionManagementError(error) {
|
|
3232
3918
|
const message = error.message ?? error.code;
|
|
3233
3919
|
switch (error.code) {
|
|
@@ -3313,7 +3999,7 @@ function projectSessionModels(value) {
|
|
|
3313
3999
|
* that official Client plane in-process instead of registering a competing
|
|
3314
4000
|
* Host waterfall listener or replacing the official Web composer.
|
|
3315
4001
|
*/
|
|
3316
|
-
/** The rc.
|
|
4002
|
+
/** The rc.2 Client sends relative RPC paths to its in-process carrier. */
|
|
3317
4003
|
function inProcessRemoteRequest(input, init) {
|
|
3318
4004
|
return new Request(new URL(input, "http://localhost/"), init);
|
|
3319
4005
|
}
|
|
@@ -3443,7 +4129,7 @@ async function startDshRemoteInteractions(hostCtx, handlers) {
|
|
|
3443
4129
|
//#endregion
|
|
3444
4130
|
//#region src/dsh-api-proxy.ts
|
|
3445
4131
|
/**
|
|
3446
|
-
* Adapter for the DSH rc.
|
|
4132
|
+
* Adapter for the DSH rc.2 controller API.
|
|
3447
4133
|
*
|
|
3448
4134
|
* DeepPilot's phone protocol deliberately speaks one stable in-process
|
|
3449
4135
|
* `apiProxy` vocabulary. This adapter maps it to the current public
|
|
@@ -3455,18 +4141,75 @@ var DshApiProxy = class {
|
|
|
3455
4141
|
ctx;
|
|
3456
4142
|
session;
|
|
3457
4143
|
workspaceController;
|
|
4144
|
+
scheduleController;
|
|
3458
4145
|
directoryPicker;
|
|
3459
4146
|
interactions = /* @__PURE__ */ new Map();
|
|
3460
4147
|
shouldSurfaceInteraction;
|
|
4148
|
+
scheduleApi;
|
|
3461
4149
|
constructor(ctx, options = {}) {
|
|
3462
4150
|
this.ctx = ctx;
|
|
3463
4151
|
const session = ctx.get("sessionController");
|
|
3464
4152
|
if (session === void 0) throw new Error("DSH sessionController is unavailable");
|
|
3465
4153
|
this.session = session;
|
|
3466
4154
|
this.workspaceController = ctx.get("workspaceController");
|
|
4155
|
+
this.scheduleController = ctx.get("schedule");
|
|
3467
4156
|
if (this.workspaceController === void 0) delete this.workspace?.unarchiveSession;
|
|
3468
4157
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
3469
4158
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
4159
|
+
this.scheduleApi = this.scheduleController === void 0 ? void 0 : this.createScheduleApi();
|
|
4160
|
+
}
|
|
4161
|
+
get schedule() {
|
|
4162
|
+
return this.scheduleApi;
|
|
4163
|
+
}
|
|
4164
|
+
createScheduleApi() {
|
|
4165
|
+
const controller = this.scheduleController;
|
|
4166
|
+
return {
|
|
4167
|
+
list: async (request) => this.call(async () => ({
|
|
4168
|
+
sessionId: request.payload.sessionId,
|
|
4169
|
+
tasks: (await controller.list({ sessionId: request.payload.sessionId })).map(toScheduleTask)
|
|
4170
|
+
})),
|
|
4171
|
+
history: async (request) => this.call(async () => {
|
|
4172
|
+
const value = await controller.history({
|
|
4173
|
+
sessionId: request.payload.sessionId,
|
|
4174
|
+
id: request.payload.id,
|
|
4175
|
+
limit: request.payload.limit,
|
|
4176
|
+
...request.payload.before !== void 0 ? { before: request.payload.before } : {}
|
|
4177
|
+
});
|
|
4178
|
+
if (typeof value.code === "string") throw Object.assign(new Error(String(value.code)), { code: value.code });
|
|
4179
|
+
return {
|
|
4180
|
+
sessionId: request.payload.sessionId,
|
|
4181
|
+
history: toScheduleHistory(value)
|
|
4182
|
+
};
|
|
4183
|
+
}),
|
|
4184
|
+
create: async (request) => this.call(async () => {
|
|
4185
|
+
const { sessionId, ...createRequest } = request.payload;
|
|
4186
|
+
return toScheduleTask(await controller.create(sessionId, createRequest));
|
|
4187
|
+
}),
|
|
4188
|
+
update: async (request) => this.call(async () => {
|
|
4189
|
+
const payload = request.payload;
|
|
4190
|
+
const result = await controller.update({
|
|
4191
|
+
...payload,
|
|
4192
|
+
expected: toScheduleExpected(payload.expected)
|
|
4193
|
+
});
|
|
4194
|
+
return {
|
|
4195
|
+
id: String(result.id ?? payload.id),
|
|
4196
|
+
updated: result.updated === true,
|
|
4197
|
+
...result.record !== void 0 ? { record: toScheduleTask(result.record) } : {},
|
|
4198
|
+
...typeof result.code === "string" ? { code: result.code } : {}
|
|
4199
|
+
};
|
|
4200
|
+
}),
|
|
4201
|
+
delete: async (request) => this.call(async () => {
|
|
4202
|
+
const result = await controller.delete({
|
|
4203
|
+
sessionId: request.payload.sessionId,
|
|
4204
|
+
id: request.payload.id
|
|
4205
|
+
});
|
|
4206
|
+
return {
|
|
4207
|
+
id: String(result.id),
|
|
4208
|
+
deleted: result.deleted === true,
|
|
4209
|
+
...result.code !== void 0 ? { code: String(result.code) } : {}
|
|
4210
|
+
};
|
|
4211
|
+
})
|
|
4212
|
+
};
|
|
3470
4213
|
}
|
|
3471
4214
|
/**
|
|
3472
4215
|
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
@@ -3518,6 +4261,7 @@ var DshApiProxy = class {
|
|
|
3518
4261
|
requestId: request.rpcId ?? randomUUID()
|
|
3519
4262
|
}, new AbortController().signal)),
|
|
3520
4263
|
create: async (request) => this.call(() => this.session.create(request.payload ?? {})),
|
|
4264
|
+
fork: async (request) => this.call(() => this.session.fork(request.payload)),
|
|
3521
4265
|
models: async (request) => this.call(async () => projectModels(await this.session.modelCatalog(), String(request.payload?.sessionId ?? ""), await this.session.list({}, new AbortController().signal))),
|
|
3522
4266
|
selectModel: async (request) => this.call(() => this.session.selectModel(request.payload ?? {})),
|
|
3523
4267
|
rename: async (request) => this.call(() => this.session.rename(request.payload)),
|
|
@@ -3684,7 +4428,8 @@ var DshApiProxy = class {
|
|
|
3684
4428
|
sessionId: String(sessionId),
|
|
3685
4429
|
running: running === true
|
|
3686
4430
|
})),
|
|
3687
|
-
listen("api-session/activity", "host/session-added")
|
|
4431
|
+
listen("api-session/activity", "host/session-added"),
|
|
4432
|
+
listen("schedule/changed", "host/schedule-changed")
|
|
3688
4433
|
];
|
|
3689
4434
|
const workspaceAbort = new AbortController();
|
|
3690
4435
|
const stop = () => workspaceAbort.abort();
|
|
@@ -3798,6 +4543,60 @@ function toWorkspaceView(value) {
|
|
|
3798
4543
|
sessionIds: Array.isArray(row.sessionIds) ? row.sessionIds.map(String) : []
|
|
3799
4544
|
};
|
|
3800
4545
|
}
|
|
4546
|
+
function toScheduleExpected(value) {
|
|
4547
|
+
const { state: _state, deliveryMode: _deliveryMode, ...record } = value !== null && typeof value === "object" ? value : {};
|
|
4548
|
+
return record;
|
|
4549
|
+
}
|
|
4550
|
+
function toScheduleTask(value) {
|
|
4551
|
+
const row = value;
|
|
4552
|
+
const kind = [
|
|
4553
|
+
"after",
|
|
4554
|
+
"at",
|
|
4555
|
+
"every",
|
|
4556
|
+
"daily",
|
|
4557
|
+
"weekly",
|
|
4558
|
+
"cron"
|
|
4559
|
+
].includes(String(row.kind)) ? String(row.kind) : "at";
|
|
4560
|
+
const optionalNumber = (key) => typeof row[key] === "number" && Number.isFinite(row[key]) ? row[key] : void 0;
|
|
4561
|
+
return {
|
|
4562
|
+
id: String(row.id ?? ""),
|
|
4563
|
+
kind,
|
|
4564
|
+
title: String(row.title ?? ""),
|
|
4565
|
+
prompt: String(row.prompt ?? ""),
|
|
4566
|
+
scheduledAt: String(row.scheduledAt ?? ""),
|
|
4567
|
+
state: row.state === "overdue" ? "overdue" : "scheduled",
|
|
4568
|
+
deliveryMode: "host",
|
|
4569
|
+
...optionalNumber("afterSeconds") !== void 0 ? { afterSeconds: optionalNumber("afterSeconds") } : {},
|
|
4570
|
+
...optionalNumber("everySeconds") !== void 0 ? { everySeconds: optionalNumber("everySeconds") } : {},
|
|
4571
|
+
...typeof row.time === "string" ? { time: row.time } : {},
|
|
4572
|
+
...typeof row.timeZone === "string" ? { timeZone: row.timeZone } : {},
|
|
4573
|
+
...Array.isArray(row.weekdays) ? { weekdays: row.weekdays.filter((day) => typeof day === "number") } : {},
|
|
4574
|
+
...typeof row.expression === "string" ? { expression: row.expression } : {}
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4577
|
+
function toScheduleHistory(value) {
|
|
4578
|
+
const row = value;
|
|
4579
|
+
const records = Array.isArray(row.records) ? row.records : [];
|
|
4580
|
+
return {
|
|
4581
|
+
id: String(row.id ?? ""),
|
|
4582
|
+
records: records.map((entry) => {
|
|
4583
|
+
const item = entry;
|
|
4584
|
+
return {
|
|
4585
|
+
scheduledAt: String(item.scheduledAt ?? ""),
|
|
4586
|
+
deliveredAt: String(item.deliveredAt ?? ""),
|
|
4587
|
+
messageId: String(item.messageId ?? ""),
|
|
4588
|
+
...typeof item.prompt === "string" ? { prompt: item.prompt } : {}
|
|
4589
|
+
};
|
|
4590
|
+
}),
|
|
4591
|
+
earlierRecordsUnavailable: row.earlierRecordsUnavailable === true,
|
|
4592
|
+
...typeof row.earlierRecordsPruned === "boolean" ? { earlierRecordsPruned: row.earlierRecordsPruned } : {},
|
|
4593
|
+
retention: {
|
|
4594
|
+
days: Number(row.retention?.days ?? 0),
|
|
4595
|
+
records: Number(row.retention?.records ?? 0)
|
|
4596
|
+
},
|
|
4597
|
+
...typeof row.nextBefore === "string" ? { nextBefore: row.nextBefore } : {}
|
|
4598
|
+
};
|
|
4599
|
+
}
|
|
3801
4600
|
async function readWorkspaceBaseline(controller) {
|
|
3802
4601
|
const abort = new AbortController();
|
|
3803
4602
|
const iterator = controller.follow(abort.signal)[Symbol.asyncIterator]();
|
|
@@ -3838,14 +4637,16 @@ var DeepPilotReportService = class extends TypertRemoteService {
|
|
|
3838
4637
|
snapshot;
|
|
3839
4638
|
pairingStarter;
|
|
3840
4639
|
deviceRevoker;
|
|
4640
|
+
deviceRenamer;
|
|
3841
4641
|
deviceScopeUpdater;
|
|
3842
4642
|
relayTester;
|
|
3843
4643
|
pushTester;
|
|
3844
|
-
constructor(ctx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester) {
|
|
4644
|
+
constructor(ctx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester) {
|
|
3845
4645
|
super(ctx, "deeppilotReport", { namespace: "deeppilot" });
|
|
3846
4646
|
this.snapshot = snapshot;
|
|
3847
4647
|
this.pairingStarter = pairingStarter;
|
|
3848
4648
|
this.deviceRevoker = deviceRevoker;
|
|
4649
|
+
this.deviceRenamer = deviceRenamer;
|
|
3849
4650
|
this.deviceScopeUpdater = deviceScopeUpdater;
|
|
3850
4651
|
this.relayTester = relayTester;
|
|
3851
4652
|
this.pushTester = pushTester;
|
|
@@ -3859,6 +4660,9 @@ var DeepPilotReportService = class extends TypertRemoteService {
|
|
|
3859
4660
|
async revokeDevice(deviceId) {
|
|
3860
4661
|
return this.deviceRevoker(deviceId);
|
|
3861
4662
|
}
|
|
4663
|
+
async setDeviceName(deviceId, customName) {
|
|
4664
|
+
return this.deviceRenamer(deviceId, customName);
|
|
4665
|
+
}
|
|
3862
4666
|
async setDeviceScopes(deviceId, scopes) {
|
|
3863
4667
|
return this.deviceScopeUpdater(deviceId, scopes);
|
|
3864
4668
|
}
|
|
@@ -3877,6 +4681,7 @@ const REPORT_REMOTE_PACKAGE = "dsh-deeppilot";
|
|
|
3877
4681
|
const REPORT_ENDPOINT = "deeppilot/report";
|
|
3878
4682
|
const BEGIN_PAIRING_ENDPOINT = "deeppilot/beginPairing";
|
|
3879
4683
|
const REVOKE_DEVICE_ENDPOINT = "deeppilot/revokeDevice";
|
|
4684
|
+
const SET_DEVICE_NAME_ENDPOINT = "deeppilot/setDeviceName";
|
|
3880
4685
|
const SET_DEVICE_SCOPES_ENDPOINT = "deeppilot/setDeviceScopes";
|
|
3881
4686
|
function reject(field) {
|
|
3882
4687
|
throw new TypeError(`deeppilot/report result: invalid ${field}`);
|
|
@@ -3919,9 +4724,12 @@ function parseDevice(value) {
|
|
|
3919
4724
|
updatedAt: int(a, "updatedAt", "device.apns.updatedAt")
|
|
3920
4725
|
};
|
|
3921
4726
|
}
|
|
4727
|
+
const customName = s.customName;
|
|
4728
|
+
if (customName !== void 0 && (typeof customName !== "string" || customName.length === 0 || customName.length > 64 || customName.trim() !== customName || /[\u0000-\u001f\u007f-\u009f]/u.test(customName))) reject("device.customName");
|
|
3922
4729
|
return {
|
|
3923
4730
|
deviceId: str(s, "deviceId", "device.deviceId"),
|
|
3924
4731
|
deviceName: str(s, "deviceName", "device.deviceName"),
|
|
4732
|
+
...typeof customName === "string" ? { customName } : {},
|
|
3925
4733
|
appVersion: str(s, "appVersion", "device.appVersion"),
|
|
3926
4734
|
firstSeenTs: int(s, "firstSeenTs", "device.firstSeenTs"),
|
|
3927
4735
|
lastSeenTs: int(s, "lastSeenTs", "device.lastSeenTs"),
|
|
@@ -4091,6 +4899,13 @@ const deviceIdSchema = { parse(value) {
|
|
|
4091
4899
|
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value)) reject("deviceId");
|
|
4092
4900
|
return value;
|
|
4093
4901
|
} };
|
|
4902
|
+
const customNameSchema = { parse(value) {
|
|
4903
|
+
if (value === null) return null;
|
|
4904
|
+
if (typeof value !== "string") reject("customName");
|
|
4905
|
+
const trimmed = value.trim();
|
|
4906
|
+
if (trimmed.length === 0 || trimmed.length > 64 || /[\u0000-\u001f\u007f-\u009f]/u.test(value)) reject("customName");
|
|
4907
|
+
return trimmed;
|
|
4908
|
+
} };
|
|
4094
4909
|
const scopesSchema = { parse(value) {
|
|
4095
4910
|
if (!Array.isArray(value) || value.some((scope) => typeof scope !== "string" || !DEVICE_SCOPES.includes(scope))) reject("scopes");
|
|
4096
4911
|
return normalizeDeviceScopes(value);
|
|
@@ -4099,7 +4914,7 @@ const booleanSchema = { parse(value) {
|
|
|
4099
4914
|
if (typeof value !== "boolean") reject("boolean");
|
|
4100
4915
|
return value;
|
|
4101
4916
|
} };
|
|
4102
|
-
/** The rc.
|
|
4917
|
+
/** The rc.2 Gateway materializes each strict codec through `create()`. */
|
|
4103
4918
|
function strictCodec(typeSymbol, schema) {
|
|
4104
4919
|
return {
|
|
4105
4920
|
mode: "strict",
|
|
@@ -4149,6 +4964,25 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4149
4964
|
}],
|
|
4150
4965
|
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#Boolean`, booleanSchema)
|
|
4151
4966
|
},
|
|
4967
|
+
{
|
|
4968
|
+
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_NAME_ENDPOINT}`,
|
|
4969
|
+
service: "deeppilotReport",
|
|
4970
|
+
namespace: "deeppilot",
|
|
4971
|
+
method: "setDeviceName",
|
|
4972
|
+
invocation: { kind: "direct" },
|
|
4973
|
+
parameters: [{
|
|
4974
|
+
name: "deviceId",
|
|
4975
|
+
wire: "deviceId",
|
|
4976
|
+
source: "json",
|
|
4977
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
4978
|
+
}, {
|
|
4979
|
+
name: "customName",
|
|
4980
|
+
wire: "customName",
|
|
4981
|
+
source: "json",
|
|
4982
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#CustomDeviceName`, customNameSchema)
|
|
4983
|
+
}],
|
|
4984
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#CustomDeviceName`, customNameSchema)
|
|
4985
|
+
},
|
|
4152
4986
|
{
|
|
4153
4987
|
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_SCOPES_ENDPOINT}`,
|
|
4154
4988
|
service: "deeppilotReport",
|
|
@@ -4194,9 +5028,9 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4194
5028
|
* Provide the report service and register its Remote descriptor. Rides an
|
|
4195
5029
|
* optional `typert` inject: profiles without the web stack never activate it.
|
|
4196
5030
|
*/
|
|
4197
|
-
function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester) {
|
|
5031
|
+
function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester) {
|
|
4198
5032
|
ctx.inject(["typert"], (remoteCtx) => {
|
|
4199
|
-
new DeepPilotReportService(remoteCtx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester);
|
|
5033
|
+
new DeepPilotReportService(remoteCtx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester);
|
|
4200
5034
|
const unregister = remoteCtx.typert.register(REPORT_HOST_CONTRIBUTION);
|
|
4201
5035
|
remoteCtx.effect(() => () => void unregister(), "dsh-deeppilot: report remote");
|
|
4202
5036
|
});
|
|
@@ -6131,7 +6965,7 @@ function apply(ctx, options) {
|
|
|
6131
6965
|
notification
|
|
6132
6966
|
});
|
|
6133
6967
|
return {
|
|
6134
|
-
name: device
|
|
6968
|
+
name: deviceDisplayName(device),
|
|
6135
6969
|
environment: registration.environment,
|
|
6136
6970
|
outcome,
|
|
6137
6971
|
tokenFingerprint: registration.token.slice(0, 10),
|
|
@@ -6439,11 +7273,11 @@ function apply(ctx, options) {
|
|
|
6439
7273
|
if (!registration) return false;
|
|
6440
7274
|
if (connectedIds.has(device.deviceId)) return false;
|
|
6441
7275
|
if (!mayReceivePush(device, notification)) {
|
|
6442
|
-
if (currentConfig().debug === true) log(`push skip "${device
|
|
7276
|
+
if (currentConfig().debug === true) log(`push skip "${deviceDisplayName(device)}": notification permission not granted`);
|
|
6443
7277
|
return false;
|
|
6444
7278
|
}
|
|
6445
7279
|
if (registration.categories?.[notification.category] === false) {
|
|
6446
|
-
if (currentConfig().debug === true) log(`push skip "${device
|
|
7280
|
+
if (currentConfig().debug === true) log(`push skip "${deviceDisplayName(device)}": category ${notification.category} muted`);
|
|
6447
7281
|
return false;
|
|
6448
7282
|
}
|
|
6449
7283
|
return true;
|
|
@@ -6464,10 +7298,10 @@ function apply(ctx, options) {
|
|
|
6464
7298
|
environment: registration.environment,
|
|
6465
7299
|
notification
|
|
6466
7300
|
}).then(({ outcome, reason }) => {
|
|
6467
|
-
log(`push(${transport}) ${notification.category} → "${device
|
|
7301
|
+
log(`push(${transport}) ${notification.category} → "${deviceDisplayName(device)}" [${registration.environment}] = ${outcome}${reason ? " (" + reason + ")" : ""}`);
|
|
6468
7302
|
if (shouldPrunePushToken(outcome, reason)) {
|
|
6469
7303
|
devices.clearPushToken(device.deviceId);
|
|
6470
|
-
log(`push: pruned stale token of "${device
|
|
7304
|
+
log(`push: pruned stale token of "${deviceDisplayName(device)}" (${reason ?? "unknown"}) — app re-registers on next launch`);
|
|
6471
7305
|
return;
|
|
6472
7306
|
}
|
|
6473
7307
|
if (relayUrl !== void 0 && shouldReEnrollRelayToken(transport, outcome, reason, {
|
|
@@ -6535,9 +7369,13 @@ function apply(ctx, options) {
|
|
|
6535
7369
|
try {
|
|
6536
7370
|
await ready;
|
|
6537
7371
|
pairingReady = auth.audience !== null && auth.devices !== null;
|
|
6538
|
-
devices = (auth.devices?.list() ?? []).filter((device) => device.publicKey !== void 0 && device.fingerprint !== void 0).map(({ deviceId, deviceName, appVersion, firstSeenTs, lastSeenTs, fingerprint, scopes, revokedAt, apns }) => ({
|
|
7372
|
+
devices = (auth.devices?.list() ?? []).filter((device) => device.publicKey !== void 0 && device.fingerprint !== void 0).map(({ deviceId, deviceName, customName, appVersion, firstSeenTs, lastSeenTs, fingerprint, scopes, revokedAt, apns }) => ({
|
|
6539
7373
|
deviceId,
|
|
6540
|
-
deviceName
|
|
7374
|
+
deviceName: deviceDisplayName({
|
|
7375
|
+
deviceName,
|
|
7376
|
+
customName
|
|
7377
|
+
}),
|
|
7378
|
+
...customName ? { customName } : {},
|
|
6541
7379
|
appVersion,
|
|
6542
7380
|
firstSeenTs,
|
|
6543
7381
|
lastSeenTs,
|
|
@@ -6571,6 +7409,13 @@ function apply(ctx, options) {
|
|
|
6571
7409
|
};
|
|
6572
7410
|
}, beginPairing, async (deviceId) => {
|
|
6573
7411
|
return await revokeDevice(deviceId);
|
|
7412
|
+
}, async (deviceId, customName) => {
|
|
7413
|
+
const { devices } = await ready;
|
|
7414
|
+
if (!devices) throw new Error("device registry unavailable");
|
|
7415
|
+
if (await devices.setCustomName(deviceId, customName) === null) throw new Error("active device not found");
|
|
7416
|
+
const normalized = customName === null ? "" : sanitizeDeviceField(customName, 64);
|
|
7417
|
+
log(`device custom name updated id=${auditLabel(deviceId)} custom=${String(normalized.length > 0)}`);
|
|
7418
|
+
return normalized === "" ? null : normalized;
|
|
6574
7419
|
}, async (deviceId, scopes) => {
|
|
6575
7420
|
const { devices } = await ready;
|
|
6576
7421
|
if (!devices) throw new Error("device registry unavailable");
|
|
@@ -7015,7 +7860,7 @@ function apply(ctx, options) {
|
|
|
7015
7860
|
log("DSH session bridge unavailable: " + String(error));
|
|
7016
7861
|
return;
|
|
7017
7862
|
}
|
|
7018
|
-
const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"));
|
|
7863
|
+
const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"), join(dataDir, "schedule-mutations-v1.json"), join(dataDir, "fork-mutations-v1.json"));
|
|
7019
7864
|
bridge.setPushOutlet(makePushOutlet());
|
|
7020
7865
|
state.bridge = bridge;
|
|
7021
7866
|
bridge.start();
|