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/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
- constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath) {
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
- if (this.apiProxy.supportsProjections === true) this.refreshProjections(sessionId);
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 (DSH 0.1.7+) and fold every key
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?.result;
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/dsh012-remote-interactions.ts
3992
+ //#region src/dsh-remote-interactions.ts
3307
3993
  /**
3308
- * Resident DSH 0.1.2-alpha.3 Remote Events client for phone interactions.
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 openHostStream(gateway, endpoint, payload, signal);
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 startDsh012RemoteInteractions(hostCtx, handlers) {
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("dsh 0.1.2-alpha.3 Host connection is unavailable");
3411
- if (gateway === void 0) throw new Error("dsh 0.1.2-alpha.3 typertGateway is unavailable");
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(new Request(input, init)),
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/dsh012-api-proxy.ts
4130
+ //#region src/dsh-api-proxy.ts
3464
4131
  /**
3465
- * Compatibility façade for the DSH 0.1.2 controller API.
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. Harness 0.1.2 removed that service in favor of
3469
- * direct Session/Workspace controllers plus Gateway-backed Remote Events.
3470
- * This adapter rebuilds the small subset the bridge needs from those public
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 facade with the exact legacy shape HostBridge consumes. */
3475
- var Dsh012ApiProxy = class {
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("dsh 0.1.2 sessionController is unavailable");
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
- if (typeof this.workspaceController?.unarchiveSession !== "function") delete this.workspace?.unarchiveSession;
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
- * Whether the host controller implements `session.projections` (added in
3494
- * DSH 0.1.7). HostBridge gates its open-time baseline refresh on this so
3495
- * older hosts keep their current list-row-hint behaviour with no RPC noise.
3496
- */
3497
- get supportsProjections() {
3498
- return typeof this.session.projections === "function";
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
- const unarchive = this.workspaceController.unarchiveSession;
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 startDsh012RemoteInteractions(this.ctx, {
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: live(z.boolean().default(true)),
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: live(z.boolean().default(false)),
5398
- local: live(z.object({
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: live(z.object({
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; plain input
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 on every
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 compatibility façade
6028
- * over DSH 0.1.2 Session/Workspace controllers, mirrors session summaries,
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.deviceName,
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.deviceName}": notification permission not granted`);
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.deviceName}": category ${notification.category} muted`);
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.deviceName}" [${registration.environment}] = ${outcome}${reason ? " (" + reason + ")" : ""}`);
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.deviceName}" (${reason ?? "unknown"}) — app re-registers on next launch`);
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 Dsh012ApiProxy(apiCtx, { shouldSurfaceInteraction: hasPairedPhoneSurface });
7858
+ proxy = new DshApiProxy(apiCtx, { shouldSurfaceInteraction: hasPairedPhoneSurface });
7109
7859
  } catch (error) {
7110
- log("dsh 0.1.2 session bridge unavailable: " + String(error));
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();