dsh-deeppilot 0.8.1 → 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 +26 -145
- package/README.md +13 -30
- package/README.zh-CN.md +10 -26
- package/SKILL.md +87 -0
- package/docs/DSH_RELEASE_MEMORY.md +98 -0
- package/lib/client.js +444 -182
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +191 -29
- package/lib/index.js +996 -246
- package/lib/index.js.map +1 -1
- package/package.json +22 -20
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 : "";
|
|
@@ -1881,6 +2024,213 @@ function summarizeResult(content) {
|
|
|
1881
2024
|
return truncate(contentText(content).replace(/\s+/g, " ").trim(), 90);
|
|
1882
2025
|
}
|
|
1883
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);
|
|
2222
|
+
}
|
|
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;
|
|
2232
|
+
}
|
|
2233
|
+
//#endregion
|
|
1884
2234
|
//#region src/host-bridge.ts
|
|
1885
2235
|
const MAX_RING_DEFAULT = 2e3;
|
|
1886
2236
|
/**
|
|
@@ -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));
|
|
@@ -2479,26 +2838,26 @@ var HostBridge = class {
|
|
|
2479
2838
|
hasMore: Boolean(result.hasMore) || page.dropped > 0
|
|
2480
2839
|
});
|
|
2481
2840
|
this.deriveTitleFallback(sessionId, messages);
|
|
2482
|
-
|
|
2841
|
+
this.refreshProjections(sessionId);
|
|
2483
2842
|
return true;
|
|
2484
2843
|
} catch {
|
|
2485
2844
|
return false;
|
|
2486
2845
|
}
|
|
2487
2846
|
}
|
|
2488
2847
|
/**
|
|
2489
|
-
* Pull one session's projection baseline
|
|
2848
|
+
* Pull one session's projection baseline and fold every key
|
|
2490
2849
|
* through applyProjection — unknown keys are ignored there, `null` means the
|
|
2491
2850
|
* session no longer exists, and any failure degrades to a diagnostic: opening
|
|
2492
2851
|
* a session must never fail because a baseline could not be read.
|
|
2493
2852
|
*/
|
|
2494
2853
|
async refreshProjections(sessionId) {
|
|
2495
2854
|
try {
|
|
2496
|
-
const response = await this.apiProxy.sessions.projections
|
|
2855
|
+
const response = await this.apiProxy.sessions.projections({
|
|
2497
2856
|
rpcId: randomUUID(),
|
|
2498
2857
|
payload: { sessionId }
|
|
2499
2858
|
});
|
|
2500
2859
|
if (this.disposed) return;
|
|
2501
|
-
const result = response
|
|
2860
|
+
const result = response.result;
|
|
2502
2861
|
if (!result) return;
|
|
2503
2862
|
if (!result.ok) {
|
|
2504
2863
|
this.diagnostic("sessions.projections failed: " + result.error.code);
|
|
@@ -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) {
|
|
@@ -3303,9 +3989,9 @@ function projectSessionModels(value) {
|
|
|
3303
3989
|
}
|
|
3304
3990
|
/** Project a history page (raw events) into MessageProjection rows. */
|
|
3305
3991
|
//#endregion
|
|
3306
|
-
//#region src/
|
|
3992
|
+
//#region src/dsh-remote-interactions.ts
|
|
3307
3993
|
/**
|
|
3308
|
-
* Resident DSH
|
|
3994
|
+
* Resident DSH Remote Events client for phone interactions.
|
|
3309
3995
|
*
|
|
3310
3996
|
* The Host-side `approval/request` and `user-questions/request` events are
|
|
3311
3997
|
* owned once by DSH API Remotes. API Gateway then fans each pending waterfall
|
|
@@ -3313,6 +3999,10 @@ 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
|
*/
|
|
4002
|
+
/** The rc.2 Client sends relative RPC paths to its in-process carrier. */
|
|
4003
|
+
function inProcessRemoteRequest(input, init) {
|
|
4004
|
+
return new Request(new URL(input, "http://localhost/"), init);
|
|
4005
|
+
}
|
|
3316
4006
|
let clientFaces;
|
|
3317
4007
|
/**
|
|
3318
4008
|
* Load DSH's browser-distributed Client faces into a tiny Host-side module
|
|
@@ -3369,32 +4059,9 @@ function createAgentScope(ctx, identity) {
|
|
|
3369
4059
|
}
|
|
3370
4060
|
});
|
|
3371
4061
|
}
|
|
3372
|
-
/**
|
|
3373
|
-
* Arity of the Host's published `wireStream.open`. 0.1.7-alpha.1 declares
|
|
3374
|
-
* `(endpoint, payload, uplink, peer, signal)`; every earlier generation
|
|
3375
|
-
* declares `(endpoint, payload, signal)`. Both are plain arrow functions with
|
|
3376
|
-
* no default or rest parameters, so `length` is a reliable contract selector —
|
|
3377
|
-
* unlike a capability probe, it opens no trial stream. A future signature that
|
|
3378
|
-
* rest-parameterizes (reporting `0`) falls back to the 3-argument call, which
|
|
3379
|
-
* every generation through `0.1.6-*` accepts.
|
|
3380
|
-
*/
|
|
3381
|
-
function hostWireStreamArity(open) {
|
|
3382
|
-
return open.length;
|
|
3383
|
-
}
|
|
3384
|
-
/**
|
|
3385
|
-
* Open one Host logical stream on whichever `wireStream.open` contract the
|
|
3386
|
-
* running Host publishes. DeepPilot's carrier owns the Host in process and has
|
|
3387
|
-
* no Client-to-Host uplink, so the 5-argument form sends `undefined` for both
|
|
3388
|
-
* `uplink` and `peer` — the documented "operator's in-process carrier" case.
|
|
3389
|
-
*/
|
|
3390
|
-
function openHostStream(gateway, endpoint, payload, signal) {
|
|
3391
|
-
const open = gateway.wireStream.open;
|
|
3392
|
-
if (hostWireStreamArity(open) >= 5) return open(endpoint, payload, void 0, void 0, signal);
|
|
3393
|
-
return open(endpoint, payload, signal);
|
|
3394
|
-
}
|
|
3395
4062
|
function inProcessStream(gateway, endpoint, payload, signal) {
|
|
3396
4063
|
return (async function* () {
|
|
3397
|
-
yield* await
|
|
4064
|
+
yield* await gateway.wireStream.open(endpoint, payload, void 0, void 0, signal);
|
|
3398
4065
|
})();
|
|
3399
4066
|
}
|
|
3400
4067
|
/**
|
|
@@ -3404,16 +4071,16 @@ function inProcessStream(gateway, endpoint, payload, signal) {
|
|
|
3404
4071
|
* The returned disposer tears down the Remote Events generation, all pending
|
|
3405
4072
|
* listeners, and every lazily minted Agent scope.
|
|
3406
4073
|
*/
|
|
3407
|
-
async function
|
|
4074
|
+
async function startDshRemoteInteractions(hostCtx, handlers) {
|
|
3408
4075
|
const connection = hostCtx.get("connection");
|
|
3409
4076
|
const gateway = hostCtx.get("typertGateway");
|
|
3410
|
-
if (connection === void 0) throw new Error("
|
|
3411
|
-
if (gateway === void 0) throw new Error("
|
|
4077
|
+
if (connection === void 0) throw new Error("DSH Host connection is unavailable");
|
|
4078
|
+
if (gateway === void 0) throw new Error("DSH typertGateway is unavailable");
|
|
3412
4079
|
const faces = await loadClientFaces();
|
|
3413
4080
|
const client = new Context();
|
|
3414
4081
|
const fetchHandler = connection.createSharedFetchHandler("/api");
|
|
3415
4082
|
const transport = {
|
|
3416
|
-
fetch: (input, init) => fetchHandler.fetch(
|
|
4083
|
+
fetch: (input, init) => fetchHandler.fetch(inProcessRemoteRequest(input, init)),
|
|
3417
4084
|
openStream: (endpoint, payload, signal) => inProcessStream(gateway, endpoint, payload, signal),
|
|
3418
4085
|
ownsHost: true
|
|
3419
4086
|
};
|
|
@@ -3460,42 +4127,89 @@ async function startDsh012RemoteInteractions(hostCtx, handlers) {
|
|
|
3460
4127
|
}
|
|
3461
4128
|
}
|
|
3462
4129
|
//#endregion
|
|
3463
|
-
//#region src/
|
|
4130
|
+
//#region src/dsh-api-proxy.ts
|
|
3464
4131
|
/**
|
|
3465
|
-
*
|
|
4132
|
+
* Adapter for the DSH rc.2 controller API.
|
|
3466
4133
|
*
|
|
3467
4134
|
* DeepPilot's phone protocol deliberately speaks one stable in-process
|
|
3468
|
-
* `apiProxy` vocabulary.
|
|
3469
|
-
*
|
|
3470
|
-
*
|
|
3471
|
-
* controllers and joins the official Client interaction plane in-process,
|
|
3472
|
-
* keeping the phone protocol isolated from the Host API migration.
|
|
4135
|
+
* `apiProxy` vocabulary. This adapter maps it to the current public
|
|
4136
|
+
* Session/Workspace controllers and Gateway-backed Remote Events, keeping
|
|
4137
|
+
* the phone protocol isolated from the Host API.
|
|
3473
4138
|
*/
|
|
3474
|
-
/** Direct-controller
|
|
3475
|
-
var
|
|
4139
|
+
/** Direct-controller adapter with the shape HostBridge consumes. */
|
|
4140
|
+
var DshApiProxy = class {
|
|
3476
4141
|
ctx;
|
|
3477
4142
|
session;
|
|
3478
4143
|
workspaceController;
|
|
4144
|
+
scheduleController;
|
|
3479
4145
|
directoryPicker;
|
|
3480
4146
|
interactions = /* @__PURE__ */ new Map();
|
|
3481
4147
|
shouldSurfaceInteraction;
|
|
4148
|
+
scheduleApi;
|
|
3482
4149
|
constructor(ctx, options = {}) {
|
|
3483
4150
|
this.ctx = ctx;
|
|
3484
4151
|
const session = ctx.get("sessionController");
|
|
3485
|
-
if (session === void 0) throw new Error("
|
|
4152
|
+
if (session === void 0) throw new Error("DSH sessionController is unavailable");
|
|
3486
4153
|
this.session = session;
|
|
3487
4154
|
this.workspaceController = ctx.get("workspaceController");
|
|
3488
|
-
|
|
4155
|
+
this.scheduleController = ctx.get("schedule");
|
|
4156
|
+
if (this.workspaceController === void 0) delete this.workspace?.unarchiveSession;
|
|
3489
4157
|
this.directoryPicker = ctx.get("directoryPickerController");
|
|
3490
4158
|
this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
|
|
4159
|
+
this.scheduleApi = this.scheduleController === void 0 ? void 0 : this.createScheduleApi();
|
|
3491
4160
|
}
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
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
|
+
};
|
|
3499
4213
|
}
|
|
3500
4214
|
/**
|
|
3501
4215
|
* Decide whether this Gateway Client can surface a phone card. The decision
|
|
@@ -3547,16 +4261,13 @@ var Dsh012ApiProxy = class {
|
|
|
3547
4261
|
requestId: request.rpcId ?? randomUUID()
|
|
3548
4262
|
}, new AbortController().signal)),
|
|
3549
4263
|
create: async (request) => this.call(() => this.session.create(request.payload ?? {})),
|
|
4264
|
+
fork: async (request) => this.call(() => this.session.fork(request.payload)),
|
|
3550
4265
|
models: async (request) => this.call(async () => projectModels(await this.session.modelCatalog(), String(request.payload?.sessionId ?? ""), await this.session.list({}, new AbortController().signal))),
|
|
3551
4266
|
selectModel: async (request) => this.call(() => this.session.selectModel(request.payload ?? {})),
|
|
3552
4267
|
rename: async (request) => this.call(() => this.session.rename(request.payload)),
|
|
3553
4268
|
cancel: async (request) => this.call(() => this.session.cancel(request.payload)),
|
|
3554
4269
|
attachment: async (request) => this.call(() => this.session.attachment(request.payload)),
|
|
3555
|
-
projections: async (request) => this.call(() =>
|
|
3556
|
-
const read = this.session.projections;
|
|
3557
|
-
if (typeof read !== "function") throw unavailable("session projections unavailable");
|
|
3558
|
-
return read.call(this.session, request.payload);
|
|
3559
|
-
})
|
|
4270
|
+
projections: async (request) => this.call(() => this.session.projections(request.payload))
|
|
3560
4271
|
};
|
|
3561
4272
|
workspace = {
|
|
3562
4273
|
list: async () => this.call(async () => {
|
|
@@ -3581,9 +4292,7 @@ var Dsh012ApiProxy = class {
|
|
|
3581
4292
|
}),
|
|
3582
4293
|
unarchiveSession: async (request) => this.call(async () => {
|
|
3583
4294
|
if (this.workspaceController === void 0) throw unavailable("workspace controller unavailable");
|
|
3584
|
-
|
|
3585
|
-
if (typeof unarchive !== "function") throw unavailable("session restore unavailable on this host version");
|
|
3586
|
-
return { archivedSessionIds: [...(await unarchive.call(this.workspaceController, request.payload)).archivedSessionIds] };
|
|
4295
|
+
return { archivedSessionIds: [...(await this.workspaceController.unarchiveSession(request.payload)).archivedSessionIds] };
|
|
3587
4296
|
})
|
|
3588
4297
|
};
|
|
3589
4298
|
host = {
|
|
@@ -3634,7 +4343,7 @@ var Dsh012ApiProxy = class {
|
|
|
3634
4343
|
});
|
|
3635
4344
|
let disposeInteractions;
|
|
3636
4345
|
try {
|
|
3637
|
-
disposeInteractions = await
|
|
4346
|
+
disposeInteractions = await startDshRemoteInteractions(this.ctx, {
|
|
3638
4347
|
approval: (sessionId, request, next) => this.answerApproval(queue, sessionId, request, next),
|
|
3639
4348
|
question: (sessionId, request, next) => this.answerQuestion(queue, sessionId, request, next)
|
|
3640
4349
|
});
|
|
@@ -3719,7 +4428,8 @@ var Dsh012ApiProxy = class {
|
|
|
3719
4428
|
sessionId: String(sessionId),
|
|
3720
4429
|
running: running === true
|
|
3721
4430
|
})),
|
|
3722
|
-
listen("api-session/activity", "host/session-added")
|
|
4431
|
+
listen("api-session/activity", "host/session-added"),
|
|
4432
|
+
listen("schedule/changed", "host/schedule-changed")
|
|
3723
4433
|
];
|
|
3724
4434
|
const workspaceAbort = new AbortController();
|
|
3725
4435
|
const stop = () => workspaceAbort.abort();
|
|
@@ -3833,6 +4543,60 @@ function toWorkspaceView(value) {
|
|
|
3833
4543
|
sessionIds: Array.isArray(row.sessionIds) ? row.sessionIds.map(String) : []
|
|
3834
4544
|
};
|
|
3835
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
|
+
}
|
|
3836
4600
|
async function readWorkspaceBaseline(controller) {
|
|
3837
4601
|
const abort = new AbortController();
|
|
3838
4602
|
const iterator = controller.follow(abort.signal)[Symbol.asyncIterator]();
|
|
@@ -3873,14 +4637,16 @@ var DeepPilotReportService = class extends TypertRemoteService {
|
|
|
3873
4637
|
snapshot;
|
|
3874
4638
|
pairingStarter;
|
|
3875
4639
|
deviceRevoker;
|
|
4640
|
+
deviceRenamer;
|
|
3876
4641
|
deviceScopeUpdater;
|
|
3877
4642
|
relayTester;
|
|
3878
4643
|
pushTester;
|
|
3879
|
-
constructor(ctx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester) {
|
|
4644
|
+
constructor(ctx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester) {
|
|
3880
4645
|
super(ctx, "deeppilotReport", { namespace: "deeppilot" });
|
|
3881
4646
|
this.snapshot = snapshot;
|
|
3882
4647
|
this.pairingStarter = pairingStarter;
|
|
3883
4648
|
this.deviceRevoker = deviceRevoker;
|
|
4649
|
+
this.deviceRenamer = deviceRenamer;
|
|
3884
4650
|
this.deviceScopeUpdater = deviceScopeUpdater;
|
|
3885
4651
|
this.relayTester = relayTester;
|
|
3886
4652
|
this.pushTester = pushTester;
|
|
@@ -3894,6 +4660,9 @@ var DeepPilotReportService = class extends TypertRemoteService {
|
|
|
3894
4660
|
async revokeDevice(deviceId) {
|
|
3895
4661
|
return this.deviceRevoker(deviceId);
|
|
3896
4662
|
}
|
|
4663
|
+
async setDeviceName(deviceId, customName) {
|
|
4664
|
+
return this.deviceRenamer(deviceId, customName);
|
|
4665
|
+
}
|
|
3897
4666
|
async setDeviceScopes(deviceId, scopes) {
|
|
3898
4667
|
return this.deviceScopeUpdater(deviceId, scopes);
|
|
3899
4668
|
}
|
|
@@ -3912,6 +4681,7 @@ const REPORT_REMOTE_PACKAGE = "dsh-deeppilot";
|
|
|
3912
4681
|
const REPORT_ENDPOINT = "deeppilot/report";
|
|
3913
4682
|
const BEGIN_PAIRING_ENDPOINT = "deeppilot/beginPairing";
|
|
3914
4683
|
const REVOKE_DEVICE_ENDPOINT = "deeppilot/revokeDevice";
|
|
4684
|
+
const SET_DEVICE_NAME_ENDPOINT = "deeppilot/setDeviceName";
|
|
3915
4685
|
const SET_DEVICE_SCOPES_ENDPOINT = "deeppilot/setDeviceScopes";
|
|
3916
4686
|
function reject(field) {
|
|
3917
4687
|
throw new TypeError(`deeppilot/report result: invalid ${field}`);
|
|
@@ -3954,9 +4724,12 @@ function parseDevice(value) {
|
|
|
3954
4724
|
updatedAt: int(a, "updatedAt", "device.apns.updatedAt")
|
|
3955
4725
|
};
|
|
3956
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");
|
|
3957
4729
|
return {
|
|
3958
4730
|
deviceId: str(s, "deviceId", "device.deviceId"),
|
|
3959
4731
|
deviceName: str(s, "deviceName", "device.deviceName"),
|
|
4732
|
+
...typeof customName === "string" ? { customName } : {},
|
|
3960
4733
|
appVersion: str(s, "appVersion", "device.appVersion"),
|
|
3961
4734
|
firstSeenTs: int(s, "firstSeenTs", "device.firstSeenTs"),
|
|
3962
4735
|
lastSeenTs: int(s, "lastSeenTs", "device.lastSeenTs"),
|
|
@@ -4126,6 +4899,13 @@ const deviceIdSchema = { parse(value) {
|
|
|
4126
4899
|
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(value)) reject("deviceId");
|
|
4127
4900
|
return value;
|
|
4128
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
|
+
} };
|
|
4129
4909
|
const scopesSchema = { parse(value) {
|
|
4130
4910
|
if (!Array.isArray(value) || value.some((scope) => typeof scope !== "string" || !DEVICE_SCOPES.includes(scope))) reject("scopes");
|
|
4131
4911
|
return normalizeDeviceScopes(value);
|
|
@@ -4134,28 +4914,11 @@ const booleanSchema = { parse(value) {
|
|
|
4134
4914
|
if (typeof value !== "boolean") reject("boolean");
|
|
4135
4915
|
return value;
|
|
4136
4916
|
} };
|
|
4137
|
-
/**
|
|
4138
|
-
* One strict boundary codec carrying both generation shapes of the supported
|
|
4139
|
-
* `0.1.x` host family.
|
|
4140
|
-
*
|
|
4141
|
-
* Through `0.1.6-alpha.1` a descriptor published the schema value itself and
|
|
4142
|
-
* every consumer parsed through `codec.schema.parse(value)`. `0.1.6-alpha.2`
|
|
4143
|
-
* switched to a lazily materialized process-realm factory: `TypertRegistry`
|
|
4144
|
-
* now rejects a strict codec without `create()`, and the Gateway parses
|
|
4145
|
-
* through `codec.create().parse(value)`.
|
|
4146
|
-
*
|
|
4147
|
-
* Publishing both keys — `create()` returning the same hand-written,
|
|
4148
|
-
* dependency-free codec — keeps one package valid across the entire declared
|
|
4149
|
-
* peer range instead of forking the wire contract per host generation. The
|
|
4150
|
-
* cast is required because each generation's `TypertCodec` declares only its
|
|
4151
|
-
* own key, so a fresh object literal carrying both fails excess-property
|
|
4152
|
-
* checking against either one.
|
|
4153
|
-
*/
|
|
4917
|
+
/** The rc.2 Gateway materializes each strict codec through `create()`. */
|
|
4154
4918
|
function strictCodec(typeSymbol, schema) {
|
|
4155
4919
|
return {
|
|
4156
4920
|
mode: "strict",
|
|
4157
4921
|
typeSymbol,
|
|
4158
|
-
schema,
|
|
4159
4922
|
create: () => schema
|
|
4160
4923
|
};
|
|
4161
4924
|
}
|
|
@@ -4201,6 +4964,25 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4201
4964
|
}],
|
|
4202
4965
|
result: strictCodec(`${REPORT_REMOTE_PACKAGE}#Boolean`, booleanSchema)
|
|
4203
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
|
+
},
|
|
4204
4986
|
{
|
|
4205
4987
|
id: `${REPORT_REMOTE_PACKAGE}#${SET_DEVICE_SCOPES_ENDPOINT}`,
|
|
4206
4988
|
service: "deeppilotReport",
|
|
@@ -4246,9 +5028,9 @@ const REPORT_HOST_CONTRIBUTION = {
|
|
|
4246
5028
|
* Provide the report service and register its Remote descriptor. Rides an
|
|
4247
5029
|
* optional `typert` inject: profiles without the web stack never activate it.
|
|
4248
5030
|
*/
|
|
4249
|
-
function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester) {
|
|
5031
|
+
function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester) {
|
|
4250
5032
|
ctx.inject(["typert"], (remoteCtx) => {
|
|
4251
|
-
new DeepPilotReportService(remoteCtx, snapshot, pairingStarter, deviceRevoker, deviceScopeUpdater, relayTester, pushTester);
|
|
5033
|
+
new DeepPilotReportService(remoteCtx, snapshot, pairingStarter, deviceRevoker, deviceRenamer, deviceScopeUpdater, relayTester, pushTester);
|
|
4252
5034
|
const unregister = remoteCtx.typert.register(REPORT_HOST_CONTRIBUTION);
|
|
4253
5035
|
remoteCtx.effect(() => () => void unregister(), "dsh-deeppilot: report remote");
|
|
4254
5036
|
});
|
|
@@ -5391,18 +6173,18 @@ function localListenError(error, port) {
|
|
|
5391
6173
|
/** Operator-run relay used by distributed builds; overridable via config. */
|
|
5392
6174
|
const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
|
|
5393
6175
|
const Config = z.object({
|
|
5394
|
-
enabled:
|
|
6176
|
+
enabled: z.boolean().default(true).volatile(),
|
|
5395
6177
|
devicesPath: z.string().default(join(bridgeDataDir(), "devices-v2.json")),
|
|
5396
6178
|
historyBufferMax: z.natural().min(100).default(2e3),
|
|
5397
|
-
debug:
|
|
5398
|
-
local:
|
|
6179
|
+
debug: z.boolean().default(false).volatile(),
|
|
6180
|
+
local: z.object({
|
|
5399
6181
|
enabled: z.boolean().default(true),
|
|
5400
6182
|
port: z.natural().min(MIN_LOCAL_PORT).max(MAX_LOCAL_PORT).default(DEFAULT_LOCAL_PORT).description("DeepPilot 局域网独立端口(仅 TLS;默认 3098,修改后本地连接会短暂重连)")
|
|
5401
6183
|
}).default({
|
|
5402
6184
|
enabled: true,
|
|
5403
6185
|
port: DEFAULT_LOCAL_PORT
|
|
5404
|
-
})),
|
|
5405
|
-
remote:
|
|
6186
|
+
}).volatile(),
|
|
6187
|
+
remote: z.object({
|
|
5406
6188
|
enabled: z.boolean().default(false),
|
|
5407
6189
|
provider: z.union(["tailscale-funnel"]).default("tailscale-funnel"),
|
|
5408
6190
|
hostname: z.string().default(DEFAULT_REMOTE_HOSTNAME),
|
|
@@ -5422,7 +6204,7 @@ const Config = z.object({
|
|
|
5422
6204
|
helperPath: "",
|
|
5423
6205
|
funnelPort: 443,
|
|
5424
6206
|
maxConnectionsPerSource: 8
|
|
5425
|
-
})),
|
|
6207
|
+
}).volatile(),
|
|
5426
6208
|
push: z.object({
|
|
5427
6209
|
provider: z.union([
|
|
5428
6210
|
"none",
|
|
@@ -5448,21 +6230,6 @@ const Config = z.object({
|
|
|
5448
6230
|
})
|
|
5449
6231
|
});
|
|
5450
6232
|
/**
|
|
5451
|
-
* Declare one live-update (volatile) config node when the host supports it.
|
|
5452
|
-
*
|
|
5453
|
-
* DSH 0.1.7's schemastery adds `Schema#volatile()`; the schemas this package
|
|
5454
|
-
* typechecks against (3.18.x line) do not have the method, so the call goes
|
|
5455
|
-
* through a structural check instead of a direct chain — a direct
|
|
5456
|
-
* `.volatile()` would both fail typecheck today and throw at import on an
|
|
5457
|
-
* older host, killing plugin activation.
|
|
5458
|
-
*/
|
|
5459
|
-
function live(node) {
|
|
5460
|
-
const schema = node;
|
|
5461
|
-
if (typeof schema.volatile === "function") return schema.volatile();
|
|
5462
|
-
if (typeof schema.extra === "function") return schema.extra("volatile", true);
|
|
5463
|
-
return node;
|
|
5464
|
-
}
|
|
5465
|
-
/**
|
|
5466
6233
|
* Deep-unwrap live-update (volatile) references to plain values.
|
|
5467
6234
|
*
|
|
5468
6235
|
* On a 0.1.7 host, `apply()`'s options — and even a direct `Config(raw)` call
|
|
@@ -5470,8 +6237,7 @@ function live(node) {
|
|
|
5470
6237
|
* `currentConfig()` compares plain values (`=== true`, numeric bounds, object
|
|
5471
6238
|
* fields), and schemastery refuses to re-parse a reference, so normalization
|
|
5472
6239
|
* unwraps on the way in and on the way out. A JSON-shaped config value cannot
|
|
5473
|
-
* carry a function, so the `get` check cannot false-positive
|
|
5474
|
-
* passes through unchanged, keeping older-host behavior identical.
|
|
6240
|
+
* carry a function, so the `get` check cannot false-positive.
|
|
5475
6241
|
*/
|
|
5476
6242
|
function plainConfig(value) {
|
|
5477
6243
|
if (value === null || typeof value !== "object") return value;
|
|
@@ -5485,8 +6251,7 @@ function plainConfig(value) {
|
|
|
5485
6251
|
* Cordis hands the second argument in different shapes depending on host
|
|
5486
6252
|
* composition: a reactive options getter, the resolved config value, or
|
|
5487
6253
|
* nothing when the patch row omits `config`. Normalize all of them — and
|
|
5488
|
-
* unwrap volatile references so the returned Config is plain data
|
|
5489
|
-
* host generation.
|
|
6254
|
+
* unwrap volatile references so the returned Config is plain data.
|
|
5490
6255
|
*/
|
|
5491
6256
|
function normalizeOptions(options) {
|
|
5492
6257
|
if (typeof options === "function") return plainConfig(options());
|
|
@@ -6024,8 +6789,8 @@ async function writePrivate(path, contents) {
|
|
|
6024
6789
|
* devices) and loopback Funnel-origin listeners. The web UI and the rest of
|
|
6025
6790
|
* DSH's API are never exposed by these listeners.
|
|
6026
6791
|
*
|
|
6027
|
-
* Data plane: an in-process HostBridge consumes a local
|
|
6028
|
-
*
|
|
6792
|
+
* Data plane: an in-process HostBridge consumes a local adapter over DSH
|
|
6793
|
+
* Session/Workspace controllers, mirrors session summaries,
|
|
6029
6794
|
* tracks pending approvals/questions, and fans projected protocol-v2 pushes
|
|
6030
6795
|
* out to every connected device.
|
|
6031
6796
|
*
|
|
@@ -6066,36 +6831,10 @@ function apply(ctx, options) {
|
|
|
6066
6831
|
};
|
|
6067
6832
|
const auditSalt = randomBytes(32);
|
|
6068
6833
|
const auditLabel = (value) => createHash("sha256").update(auditSalt).update(value).digest("hex").slice(0, 12);
|
|
6069
|
-
/**
|
|
6070
|
-
* Settings-section source: while a settings service is attached this holds
|
|
6071
|
-
* the user-edited section value; otherwise the composition defaults. Read
|
|
6072
|
-
* through currentConfig() everywhere (normalizeOptions prefers it).
|
|
6073
|
-
*/
|
|
6074
|
-
let liveSource;
|
|
6075
6834
|
let scheduleRemoteReconcile;
|
|
6076
6835
|
let scheduleLocalReconcile;
|
|
6077
|
-
const currentConfig = () =>
|
|
6078
|
-
if (liveSource !== void 0) return normalizeOptions(liveSource());
|
|
6079
|
-
return normalizeOptions(options);
|
|
6080
|
-
};
|
|
6836
|
+
const currentConfig = () => normalizeOptions(options);
|
|
6081
6837
|
const enabledNow = () => currentConfig().enabled === true;
|
|
6082
|
-
const settingsHooks = {
|
|
6083
|
-
setSource: (source) => {
|
|
6084
|
-
liveSource = source;
|
|
6085
|
-
queueMicrotask(() => {
|
|
6086
|
-
scheduleLocalReconcile?.();
|
|
6087
|
-
scheduleRemoteReconcile?.();
|
|
6088
|
-
});
|
|
6089
|
-
},
|
|
6090
|
-
onChange: () => queueMicrotask(() => {
|
|
6091
|
-
scheduleLocalReconcile?.();
|
|
6092
|
-
scheduleRemoteReconcile?.();
|
|
6093
|
-
})
|
|
6094
|
-
};
|
|
6095
|
-
ctx.inject(["settings"], (settingsCtx) => {
|
|
6096
|
-
if (typeof settingsCtx.settings?.installSection !== "function") return;
|
|
6097
|
-
settingsCtx.settings.installSection(ctx, "deeppilot", Config, normalizeOptions(void 0), settingsHooks);
|
|
6098
|
-
});
|
|
6099
6838
|
ctx.on("loader/volatile-update", () => queueMicrotask(() => {
|
|
6100
6839
|
scheduleLocalReconcile?.();
|
|
6101
6840
|
scheduleRemoteReconcile?.();
|
|
@@ -6226,7 +6965,7 @@ function apply(ctx, options) {
|
|
|
6226
6965
|
notification
|
|
6227
6966
|
});
|
|
6228
6967
|
return {
|
|
6229
|
-
name: device
|
|
6968
|
+
name: deviceDisplayName(device),
|
|
6230
6969
|
environment: registration.environment,
|
|
6231
6970
|
outcome,
|
|
6232
6971
|
tokenFingerprint: registration.token.slice(0, 10),
|
|
@@ -6534,11 +7273,11 @@ function apply(ctx, options) {
|
|
|
6534
7273
|
if (!registration) return false;
|
|
6535
7274
|
if (connectedIds.has(device.deviceId)) return false;
|
|
6536
7275
|
if (!mayReceivePush(device, notification)) {
|
|
6537
|
-
if (currentConfig().debug === true) log(`push skip "${device
|
|
7276
|
+
if (currentConfig().debug === true) log(`push skip "${deviceDisplayName(device)}": notification permission not granted`);
|
|
6538
7277
|
return false;
|
|
6539
7278
|
}
|
|
6540
7279
|
if (registration.categories?.[notification.category] === false) {
|
|
6541
|
-
if (currentConfig().debug === true) log(`push skip "${device
|
|
7280
|
+
if (currentConfig().debug === true) log(`push skip "${deviceDisplayName(device)}": category ${notification.category} muted`);
|
|
6542
7281
|
return false;
|
|
6543
7282
|
}
|
|
6544
7283
|
return true;
|
|
@@ -6559,10 +7298,10 @@ function apply(ctx, options) {
|
|
|
6559
7298
|
environment: registration.environment,
|
|
6560
7299
|
notification
|
|
6561
7300
|
}).then(({ outcome, reason }) => {
|
|
6562
|
-
log(`push(${transport}) ${notification.category} → "${device
|
|
7301
|
+
log(`push(${transport}) ${notification.category} → "${deviceDisplayName(device)}" [${registration.environment}] = ${outcome}${reason ? " (" + reason + ")" : ""}`);
|
|
6563
7302
|
if (shouldPrunePushToken(outcome, reason)) {
|
|
6564
7303
|
devices.clearPushToken(device.deviceId);
|
|
6565
|
-
log(`push: pruned stale token of "${device
|
|
7304
|
+
log(`push: pruned stale token of "${deviceDisplayName(device)}" (${reason ?? "unknown"}) — app re-registers on next launch`);
|
|
6566
7305
|
return;
|
|
6567
7306
|
}
|
|
6568
7307
|
if (relayUrl !== void 0 && shouldReEnrollRelayToken(transport, outcome, reason, {
|
|
@@ -6630,9 +7369,13 @@ function apply(ctx, options) {
|
|
|
6630
7369
|
try {
|
|
6631
7370
|
await ready;
|
|
6632
7371
|
pairingReady = auth.audience !== null && auth.devices !== null;
|
|
6633
|
-
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 }) => ({
|
|
6634
7373
|
deviceId,
|
|
6635
|
-
deviceName
|
|
7374
|
+
deviceName: deviceDisplayName({
|
|
7375
|
+
deviceName,
|
|
7376
|
+
customName
|
|
7377
|
+
}),
|
|
7378
|
+
...customName ? { customName } : {},
|
|
6636
7379
|
appVersion,
|
|
6637
7380
|
firstSeenTs,
|
|
6638
7381
|
lastSeenTs,
|
|
@@ -6666,6 +7409,13 @@ function apply(ctx, options) {
|
|
|
6666
7409
|
};
|
|
6667
7410
|
}, beginPairing, async (deviceId) => {
|
|
6668
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;
|
|
6669
7419
|
}, async (deviceId, scopes) => {
|
|
6670
7420
|
const { devices } = await ready;
|
|
6671
7421
|
if (!devices) throw new Error("device registry unavailable");
|
|
@@ -7105,12 +7855,12 @@ function apply(ctx, options) {
|
|
|
7105
7855
|
const apiCtx = sub;
|
|
7106
7856
|
let proxy;
|
|
7107
7857
|
try {
|
|
7108
|
-
proxy = new
|
|
7858
|
+
proxy = new DshApiProxy(apiCtx, { shouldSurfaceInteraction: hasPairedPhoneSurface });
|
|
7109
7859
|
} catch (error) {
|
|
7110
|
-
log("
|
|
7860
|
+
log("DSH session bridge unavailable: " + String(error));
|
|
7111
7861
|
return;
|
|
7112
7862
|
}
|
|
7113
|
-
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"));
|
|
7114
7864
|
bridge.setPushOutlet(makePushOutlet());
|
|
7115
7865
|
state.bridge = bridge;
|
|
7116
7866
|
bridge.start();
|