dsh-deeppilot 0.8.2 → 0.8.3

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