dsh-deeppilot 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPATIBILITY.md +53 -3
- package/README.md +10 -7
- package/README.zh-CN.md +9 -7
- package/SKILL.md +87 -0
- package/docs/DSH_RELEASE_MEMORY.md +274 -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 +985 -119
- package/lib/index.js.map +1 -1
- package/package.json +19 -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,6 +4141,8 @@ var DshApiProxy = class {
|
|
|
3455
4141
|
ctx;
|
|
3456
4142
|
session;
|
|
3457
4143
|
workspaceController;
|
|
4144
|
+
scheduleController;
|
|
4145
|
+
scheduleApi;
|
|
3458
4146
|
directoryPicker;
|
|
3459
4147
|
interactions = /* @__PURE__ */ new Map();
|
|
3460
4148
|
shouldSurfaceInteraction;
|
|
@@ -3467,6 +4155,82 @@ var DshApiProxy = class {
|
|
|
3467
4155
|
if (this.workspaceController === void 0) delete this.workspace?.unarchiveSession;
|
|
3468
4156
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
3469
4157
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
4158
|
+
this.resolveSchedule();
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* Resolve the optional Schedule service lazily, on every read.
|
|
4162
|
+
*
|
|
4163
|
+
* The bridge is built from a `ctx.inject` on sessionController / connection
|
|
4164
|
+
* / typertGateway, and `schedule` is deliberately NOT in that list: it only
|
|
4165
|
+
* exists when the user enables the optional Automation tasks bundle. On a
|
|
4166
|
+
* DSH 0.2.0 host those three services become ready well before
|
|
4167
|
+
* ScheduleService finishes its own async init, so a value captured here in
|
|
4168
|
+
* the constructor stayed `undefined` for the bridge's whole lifetime and
|
|
4169
|
+
* welcome advertised `schedules=false` even with the bundle mounted.
|
|
4170
|
+
*
|
|
4171
|
+
* Once resolved the controller is kept: services are not torn down and
|
|
4172
|
+
* rebuilt underneath a live bridge, so re-resolving per call would be pure
|
|
4173
|
+
* overhead.
|
|
4174
|
+
*/
|
|
4175
|
+
resolveSchedule() {
|
|
4176
|
+
if (this.scheduleController !== void 0) return;
|
|
4177
|
+
const controller = this.ctx.get("schedule");
|
|
4178
|
+
if (controller === void 0) return;
|
|
4179
|
+
this.scheduleController = controller;
|
|
4180
|
+
this.scheduleApi = this.createScheduleApi(controller);
|
|
4181
|
+
}
|
|
4182
|
+
get schedule() {
|
|
4183
|
+
this.resolveSchedule();
|
|
4184
|
+
return this.scheduleApi;
|
|
4185
|
+
}
|
|
4186
|
+
createScheduleApi(controller) {
|
|
4187
|
+
return {
|
|
4188
|
+
list: async (request) => this.call(async () => ({
|
|
4189
|
+
sessionId: request.payload.sessionId,
|
|
4190
|
+
tasks: (await controller.list({ sessionId: request.payload.sessionId })).map(toScheduleTask)
|
|
4191
|
+
})),
|
|
4192
|
+
history: async (request) => this.call(async () => {
|
|
4193
|
+
const value = await controller.history({
|
|
4194
|
+
sessionId: request.payload.sessionId,
|
|
4195
|
+
id: request.payload.id,
|
|
4196
|
+
limit: request.payload.limit,
|
|
4197
|
+
...request.payload.before !== void 0 ? { before: request.payload.before } : {}
|
|
4198
|
+
});
|
|
4199
|
+
if (typeof value.code === "string") throw Object.assign(new Error(String(value.code)), { code: value.code });
|
|
4200
|
+
return {
|
|
4201
|
+
sessionId: request.payload.sessionId,
|
|
4202
|
+
history: toScheduleHistory(value)
|
|
4203
|
+
};
|
|
4204
|
+
}),
|
|
4205
|
+
create: async (request) => this.call(async () => {
|
|
4206
|
+
const { sessionId, ...createRequest } = request.payload;
|
|
4207
|
+
return toScheduleTask(await controller.create(sessionId, createRequest));
|
|
4208
|
+
}),
|
|
4209
|
+
update: async (request) => this.call(async () => {
|
|
4210
|
+
const payload = request.payload;
|
|
4211
|
+
const result = await controller.update({
|
|
4212
|
+
...payload,
|
|
4213
|
+
expected: toScheduleExpected(payload.expected)
|
|
4214
|
+
});
|
|
4215
|
+
return {
|
|
4216
|
+
id: String(result.id ?? payload.id),
|
|
4217
|
+
updated: result.updated === true,
|
|
4218
|
+
...result.record !== void 0 ? { record: toScheduleTask(result.record) } : {},
|
|
4219
|
+
...typeof result.code === "string" ? { code: result.code } : {}
|
|
4220
|
+
};
|
|
4221
|
+
}),
|
|
4222
|
+
delete: async (request) => this.call(async () => {
|
|
4223
|
+
const result = await controller.delete({
|
|
4224
|
+
sessionId: request.payload.sessionId,
|
|
4225
|
+
id: request.payload.id
|
|
4226
|
+
});
|
|
4227
|
+
return {
|
|
4228
|
+
id: String(result.id),
|
|
4229
|
+
deleted: result.deleted === true,
|
|
4230
|
+
...result.code !== void 0 ? { code: String(result.code) } : {}
|
|
4231
|
+
};
|
|
4232
|
+
})
|
|
4233
|
+
};
|
|
3470
4234
|
}
|
|
3471
4235
|
/**
|
|
3472
4236
|
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
@@ -3518,6 +4282,7 @@ var DshApiProxy = class {
|
|
|
3518
4282
|
requestId: request.rpcId ?? randomUUID()
|
|
3519
4283
|
}, new AbortController().signal)),
|
|
3520
4284
|
create: async (request) => this.call(() => this.session.create(request.payload ?? {})),
|
|
4285
|
+
fork: async (request) => this.call(() => this.session.fork(request.payload)),
|
|
3521
4286
|
models: async (request) => this.call(async () => projectModels(await this.session.modelCatalog(), String(request.payload?.sessionId ?? ""), await this.session.list({}, new AbortController().signal))),
|
|
3522
4287
|
selectModel: async (request) => this.call(() => this.session.selectModel(request.payload ?? {})),
|
|
3523
4288
|
rename: async (request) => this.call(() => this.session.rename(request.payload)),
|
|
@@ -3684,7 +4449,8 @@ var DshApiProxy = class {
|
|
|
3684
4449
|
sessionId: String(sessionId),
|
|
3685
4450
|
running: running === true
|
|
3686
4451
|
})),
|
|
3687
|
-
listen("api-session/activity", "host/session-added")
|
|
4452
|
+
listen("api-session/activity", "host/session-added"),
|
|
4453
|
+
listen("schedule/changed", "host/schedule-changed")
|
|
3688
4454
|
];
|
|
3689
4455
|
const workspaceAbort = new AbortController();
|
|
3690
4456
|
const stop = () => workspaceAbort.abort();
|
|
@@ -3798,6 +4564,60 @@ function toWorkspaceView(value) {
|
|
|
3798
4564
|
sessionIds: Array.isArray(row.sessionIds) ? row.sessionIds.map(String) : []
|
|
3799
4565
|
};
|
|
3800
4566
|
}
|
|
4567
|
+
function toScheduleExpected(value) {
|
|
4568
|
+
const { state: _state, deliveryMode: _deliveryMode, ...record } = value !== null && typeof value === "object" ? value : {};
|
|
4569
|
+
return record;
|
|
4570
|
+
}
|
|
4571
|
+
function toScheduleTask(value) {
|
|
4572
|
+
const row = value;
|
|
4573
|
+
const kind = [
|
|
4574
|
+
"after",
|
|
4575
|
+
"at",
|
|
4576
|
+
"every",
|
|
4577
|
+
"daily",
|
|
4578
|
+
"weekly",
|
|
4579
|
+
"cron"
|
|
4580
|
+
].includes(String(row.kind)) ? String(row.kind) : "at";
|
|
4581
|
+
const optionalNumber = (key) => typeof row[key] === "number" && Number.isFinite(row[key]) ? row[key] : void 0;
|
|
4582
|
+
return {
|
|
4583
|
+
id: String(row.id ?? ""),
|
|
4584
|
+
kind,
|
|
4585
|
+
title: String(row.title ?? ""),
|
|
4586
|
+
prompt: String(row.prompt ?? ""),
|
|
4587
|
+
scheduledAt: String(row.scheduledAt ?? ""),
|
|
4588
|
+
state: row.state === "overdue" ? "overdue" : "scheduled",
|
|
4589
|
+
deliveryMode: "host",
|
|
4590
|
+
...optionalNumber("afterSeconds") !== void 0 ? { afterSeconds: optionalNumber("afterSeconds") } : {},
|
|
4591
|
+
...optionalNumber("everySeconds") !== void 0 ? { everySeconds: optionalNumber("everySeconds") } : {},
|
|
4592
|
+
...typeof row.time === "string" ? { time: row.time } : {},
|
|
4593
|
+
...typeof row.timeZone === "string" ? { timeZone: row.timeZone } : {},
|
|
4594
|
+
...Array.isArray(row.weekdays) ? { weekdays: row.weekdays.filter((day) => typeof day === "number") } : {},
|
|
4595
|
+
...typeof row.expression === "string" ? { expression: row.expression } : {}
|
|
4596
|
+
};
|
|
4597
|
+
}
|
|
4598
|
+
function toScheduleHistory(value) {
|
|
4599
|
+
const row = value;
|
|
4600
|
+
const records = Array.isArray(row.records) ? row.records : [];
|
|
4601
|
+
return {
|
|
4602
|
+
id: String(row.id ?? ""),
|
|
4603
|
+
records: records.map((entry) => {
|
|
4604
|
+
const item = entry;
|
|
4605
|
+
return {
|
|
4606
|
+
scheduledAt: String(item.scheduledAt ?? ""),
|
|
4607
|
+
deliveredAt: String(item.deliveredAt ?? ""),
|
|
4608
|
+
messageId: String(item.messageId ?? ""),
|
|
4609
|
+
...typeof item.prompt === "string" ? { prompt: item.prompt } : {}
|
|
4610
|
+
};
|
|
4611
|
+
}),
|
|
4612
|
+
earlierRecordsUnavailable: row.earlierRecordsUnavailable === true,
|
|
4613
|
+
...typeof row.earlierRecordsPruned === "boolean" ? { earlierRecordsPruned: row.earlierRecordsPruned } : {},
|
|
4614
|
+
retention: {
|
|
4615
|
+
days: Number(row.retention?.days ?? 0),
|
|
4616
|
+
records: Number(row.retention?.records ?? 0)
|
|
4617
|
+
},
|
|
4618
|
+
...typeof row.nextBefore === "string" ? { nextBefore: row.nextBefore } : {}
|
|
4619
|
+
};
|
|
4620
|
+
}
|
|
3801
4621
|
async function readWorkspaceBaseline(controller) {
|
|
3802
4622
|
const abort = new AbortController();
|
|
3803
4623
|
const iterator = controller.follow(abort.signal)[Symbol.asyncIterator]();
|
|
@@ -3838,14 +4658,16 @@ var DeepPilotReportService = class extends TypertRemoteService {
|
|
|
3838
4658
|
snapshot;
|
|
3839
4659
|
pairingStarter;
|
|
3840
4660
|
deviceRevoker;
|
|
4661
|
+
deviceRenamer;
|
|
3841
4662
|
deviceScopeUpdater;
|
|
3842
4663
|
relayTester;
|
|
3843
4664
|
pushTester;
|
|
3844
|
-
constructor(ctx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester) {
|
|
4665
|
+
constructor(ctx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester) {
|
|
3845
4666
|
super(ctx, "deeppilotReport", { namespace: "deeppilot" });
|
|
3846
4667
|
this.snapshot = snapshot;
|
|
3847
4668
|
this.pairingStarter = pairingStarter;
|
|
3848
4669
|
this.deviceRevoker = deviceRevoker;
|
|
4670
|
+
this.deviceRenamer = deviceRenamer;
|
|
3849
4671
|
this.deviceScopeUpdater = deviceScopeUpdater;
|
|
3850
4672
|
this.relayTester = relayTester;
|
|
3851
4673
|
this.pushTester = pushTester;
|
|
@@ -3859,6 +4681,9 @@ var DeepPilotReportService = class extends TypertRemoteService {
|
|
|
3859
4681
|
async revokeDevice(deviceId) {
|
|
3860
4682
|
return this.deviceRevoker(deviceId);
|
|
3861
4683
|
}
|
|
4684
|
+
async setDeviceName(deviceId, customName) {
|
|
4685
|
+
return this.deviceRenamer(deviceId, customName);
|
|
4686
|
+
}
|
|
3862
4687
|
async setDeviceScopes(deviceId, scopes) {
|
|
3863
4688
|
return this.deviceScopeUpdater(deviceId, scopes);
|
|
3864
4689
|
}
|
|
@@ -3877,6 +4702,7 @@ const REPORT_REMOTE_PACKAGE = "dsh-deeppilot";
|
|
|
3877
4702
|
const REPORT_ENDPOINT = "deeppilot/report";
|
|
3878
4703
|
const BEGIN_PAIRING_ENDPOINT = "deeppilot/beginPairing";
|
|
3879
4704
|
const REVOKE_DEVICE_ENDPOINT = "deeppilot/revokeDevice";
|
|
4705
|
+
const SET_DEVICE_NAME_ENDPOINT = "deeppilot/setDeviceName";
|
|
3880
4706
|
const SET_DEVICE_SCOPES_ENDPOINT = "deeppilot/setDeviceScopes";
|
|
3881
4707
|
function reject(field) {
|
|
3882
4708
|
throw new TypeError(`deeppilot/report result: invalid ${field}`);
|
|
@@ -3919,9 +4745,12 @@ function parseDevice(value) {
|
|
|
3919
4745
|
updatedAt: int(a, "updatedAt", "device.apns.updatedAt")
|
|
3920
4746
|
};
|
|
3921
4747
|
}
|
|
4748
|
+
const customName = s.customName;
|
|
4749
|
+
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
4750
|
return {
|
|
3923
4751
|
deviceId: str(s, "deviceId", "device.deviceId"),
|
|
3924
4752
|
deviceName: str(s, "deviceName", "device.deviceName"),
|
|
4753
|
+
...typeof customName === "string" ? { customName } : {},
|
|
3925
4754
|
appVersion: str(s, "appVersion", "device.appVersion"),
|
|
3926
4755
|
firstSeenTs: int(s, "firstSeenTs", "device.firstSeenTs"),
|
|
3927
4756
|
lastSeenTs: int(s, "lastSeenTs", "device.lastSeenTs"),
|
|
@@ -4091,6 +4920,13 @@ const deviceIdSchema = { parse(value) {
|
|
|
4091
4920
|
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value)) reject("deviceId");
|
|
4092
4921
|
return value;
|
|
4093
4922
|
} };
|
|
4923
|
+
const customNameSchema = { parse(value) {
|
|
4924
|
+
if (value === null) return null;
|
|
4925
|
+
if (typeof value !== "string") reject("customName");
|
|
4926
|
+
const trimmed = value.trim();
|
|
4927
|
+
if (trimmed.length === 0 || trimmed.length > 64 || /[\u0000-\u001f\u007f-\u009f]/u.test(value)) reject("customName");
|
|
4928
|
+
return trimmed;
|
|
4929
|
+
} };
|
|
4094
4930
|
const scopesSchema = { parse(value) {
|
|
4095
4931
|
if (!Array.isArray(value) || value.some((scope) => typeof scope !== "string" || !DEVICE_SCOPES.includes(scope))) reject("scopes");
|
|
4096
4932
|
return normalizeDeviceScopes(value);
|
|
@@ -4099,7 +4935,7 @@ const booleanSchema = { parse(value) {
|
|
|
4099
4935
|
if (typeof value !== "boolean") reject("boolean");
|
|
4100
4936
|
return value;
|
|
4101
4937
|
} };
|
|
4102
|
-
/** The rc.
|
|
4938
|
+
/** The rc.2 Gateway materializes each strict codec through `create()`. */
|
|
4103
4939
|
function strictCodec(typeSymbol, schema) {
|
|
4104
4940
|
return {
|
|
4105
4941
|
mode: "strict",
|
|
@@ -4149,6 +4985,25 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4149
4985
|
}],
|
|
4150
4986
|
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#Boolean`, booleanSchema)
|
|
4151
4987
|
},
|
|
4988
|
+
{
|
|
4989
|
+
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_NAME_ENDPOINT}`,
|
|
4990
|
+
service: "deeppilotReport",
|
|
4991
|
+
namespace: "deeppilot",
|
|
4992
|
+
method: "setDeviceName",
|
|
4993
|
+
invocation: { kind: "direct" },
|
|
4994
|
+
parameters: [{
|
|
4995
|
+
name: "deviceId",
|
|
4996
|
+
wire: "deviceId",
|
|
4997
|
+
source: "json",
|
|
4998
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#DeviceId`, deviceIdSchema)
|
|
4999
|
+
}, {
|
|
5000
|
+
name: "customName",
|
|
5001
|
+
wire: "customName",
|
|
5002
|
+
source: "json",
|
|
5003
|
+
codec: strictCodec(`${REPORT_REMOTE_PACKAGE}#CustomDeviceName`, customNameSchema)
|
|
5004
|
+
}],
|
|
5005
|
+
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#CustomDeviceName`, customNameSchema)
|
|
5006
|
+
},
|
|
4152
5007
|
{
|
|
4153
5008
|
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_SCOPES_ENDPOINT}`,
|
|
4154
5009
|
service: "deeppilotReport",
|
|
@@ -4194,9 +5049,9 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4194
5049
|
* Provide the report service and register its Remote descriptor. Rides an
|
|
4195
5050
|
* optional `typert` inject: profiles without the web stack never activate it.
|
|
4196
5051
|
*/
|
|
4197
|
-
function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester) {
|
|
5052
|
+
function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester) {
|
|
4198
5053
|
ctx.inject(["typert"], (remoteCtx) => {
|
|
4199
|
-
new DeepPilotReportService(remoteCtx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester);
|
|
5054
|
+
new DeepPilotReportService(remoteCtx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester);
|
|
4200
5055
|
const unregister = remoteCtx.typert.register(REPORT_HOST_CONTRIBUTION);
|
|
4201
5056
|
remoteCtx.effect(() => () => void unregister(), "dsh-deeppilot: report remote");
|
|
4202
5057
|
});
|
|
@@ -6131,7 +6986,7 @@ function apply(ctx, options) {
|
|
|
6131
6986
|
notification
|
|
6132
6987
|
});
|
|
6133
6988
|
return {
|
|
6134
|
-
name: device
|
|
6989
|
+
name: deviceDisplayName(device),
|
|
6135
6990
|
environment: registration.environment,
|
|
6136
6991
|
outcome,
|
|
6137
6992
|
tokenFingerprint: registration.token.slice(0, 10),
|
|
@@ -6439,11 +7294,11 @@ function apply(ctx, options) {
|
|
|
6439
7294
|
if (!registration) return false;
|
|
6440
7295
|
if (connectedIds.has(device.deviceId)) return false;
|
|
6441
7296
|
if (!mayReceivePush(device, notification)) {
|
|
6442
|
-
if (currentConfig().debug === true) log(`push skip "${device
|
|
7297
|
+
if (currentConfig().debug === true) log(`push skip "${deviceDisplayName(device)}": notification permission not granted`);
|
|
6443
7298
|
return false;
|
|
6444
7299
|
}
|
|
6445
7300
|
if (registration.categories?.[notification.category] === false) {
|
|
6446
|
-
if (currentConfig().debug === true) log(`push skip "${device
|
|
7301
|
+
if (currentConfig().debug === true) log(`push skip "${deviceDisplayName(device)}": category ${notification.category} muted`);
|
|
6447
7302
|
return false;
|
|
6448
7303
|
}
|
|
6449
7304
|
return true;
|
|
@@ -6464,10 +7319,10 @@ function apply(ctx, options) {
|
|
|
6464
7319
|
environment: registration.environment,
|
|
6465
7320
|
notification
|
|
6466
7321
|
}).then(({ outcome, reason }) => {
|
|
6467
|
-
log(`push(${transport}) ${notification.category} → "${device
|
|
7322
|
+
log(`push(${transport}) ${notification.category} → "${deviceDisplayName(device)}" [${registration.environment}] = ${outcome}${reason ? " (" + reason + ")" : ""}`);
|
|
6468
7323
|
if (shouldPrunePushToken(outcome, reason)) {
|
|
6469
7324
|
devices.clearPushToken(device.deviceId);
|
|
6470
|
-
log(`push: pruned stale token of "${device
|
|
7325
|
+
log(`push: pruned stale token of "${deviceDisplayName(device)}" (${reason ?? "unknown"}) — app re-registers on next launch`);
|
|
6471
7326
|
return;
|
|
6472
7327
|
}
|
|
6473
7328
|
if (relayUrl !== void 0 && shouldReEnrollRelayToken(transport, outcome, reason, {
|
|
@@ -6535,9 +7390,13 @@ function apply(ctx, options) {
|
|
|
6535
7390
|
try {
|
|
6536
7391
|
await ready;
|
|
6537
7392
|
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 }) => ({
|
|
7393
|
+
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
7394
|
deviceId,
|
|
6540
|
-
deviceName
|
|
7395
|
+
deviceName: deviceDisplayName({
|
|
7396
|
+
deviceName,
|
|
7397
|
+
customName
|
|
7398
|
+
}),
|
|
7399
|
+
...customName ? { customName } : {},
|
|
6541
7400
|
appVersion,
|
|
6542
7401
|
firstSeenTs,
|
|
6543
7402
|
lastSeenTs,
|
|
@@ -6571,6 +7430,13 @@ function apply(ctx, options) {
|
|
|
6571
7430
|
};
|
|
6572
7431
|
}, beginPairing, async (deviceId) => {
|
|
6573
7432
|
return await revokeDevice(deviceId);
|
|
7433
|
+
}, async (deviceId, customName) => {
|
|
7434
|
+
const { devices } = await ready;
|
|
7435
|
+
if (!devices) throw new Error("device registry unavailable");
|
|
7436
|
+
if (await devices.setCustomName(deviceId, customName) === null) throw new Error("active device not found");
|
|
7437
|
+
const normalized = customName === null ? "" : sanitizeDeviceField(customName, 64);
|
|
7438
|
+
log(`device custom name updated id=${auditLabel(deviceId)} custom=${String(normalized.length > 0)}`);
|
|
7439
|
+
return normalized === "" ? null : normalized;
|
|
6574
7440
|
}, async (deviceId, scopes) => {
|
|
6575
7441
|
const { devices } = await ready;
|
|
6576
7442
|
if (!devices) throw new Error("device registry unavailable");
|
|
@@ -7015,7 +7881,7 @@ function apply(ctx, options) {
|
|
|
7015
7881
|
log("DSH session bridge unavailable: " + String(error));
|
|
7016
7882
|
return;
|
|
7017
7883
|
}
|
|
7018
|
-
const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"));
|
|
7884
|
+
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
7885
|
bridge.setPushOutlet(makePushOutlet());
|
|
7020
7886
|
state.bridge = bridge;
|
|
7021
7887
|
bridge.start();
|