dsh-deeppilot 0.9.13 → 0.9.15

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
@@ -16,7 +16,7 @@ import { Context } from "@deepseek-ai/cordis";
16
16
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
17
17
  import { isIP } from "node:net";
18
18
  import { generate } from "selfsigned";
19
- //#region src/wire-errors.ts
19
+ // src/wire-errors.ts
20
20
  /**
21
21
  * 错误词表(error vocabulary)的唯一属主。
22
22
  *
@@ -42,7 +42,8 @@ const ERROR_CODES = {
42
42
  E_BUSY: "session is busy",
43
43
  E_UNSUPPORTED: "protocol version or capability unsupported",
44
44
  E_INTERNAL: "internal error",
45
- E_STEER_UNAVAILABLE: "queued item can no longer steer the current turn"
45
+ E_STEER_UNAVAILABLE: "queued item can no longer steer the current turn",
46
+ E_TOO_LARGE: "response exceeds the client frame limit"
46
47
  };
47
48
  /** Bridge 未携带 code 的结果(内部守卫失败等)落到这一档。 */
48
49
  const WIRE_ERROR_FALLBACK = "E_INTERNAL";
@@ -90,6 +91,9 @@ const DOMAIN_TABLES = {
90
91
  "schedule_not_found": "E_NOT_FOUND",
91
92
  "delivery_cursor_not_found": "E_NOT_FOUND",
92
93
  "schedule_conflict": "E_BUSY",
94
+ "schedule_execution_pending": "E_BUSY",
95
+ "schedule/target-archived": "E_NOT_FOUND",
96
+ "schedule/input-invalid": "E_PROTOCOL",
93
97
  "invalid_prompt": "E_PROTOCOL",
94
98
  "invalid_selector": "E_PROTOCOL",
95
99
  "invalid_rule": "E_PROTOCOL",
@@ -209,9 +213,10 @@ function pendingResponseMessage(kind, reason) {
209
213
  case "transport": return "host connection failed while answering " + kind;
210
214
  }
211
215
  }
212
- //#endregion
213
- //#region src/connection-policy.ts
216
+ // src/connection-policy.ts
214
217
  const AUTH_TIMEOUT_MS = 35e3;
218
+ /** 附件 ack 在 `{mediaType,data}` 之外的信封字段(v/type/id/ts)余量。 */
219
+ const ATTACHMENT_ENVELOPE_HEADROOM_BYTES = 1024;
215
220
  const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
216
221
  "image/png",
217
222
  "image/jpeg",
@@ -287,8 +292,20 @@ function sanitizeImageName(value) {
287
292
  function sanitizeDocumentField(value, maxChars) {
288
293
  return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
289
294
  }
290
- //#endregion
291
- //#region src/auth-rate-limit.ts
295
+ /** 二进制模式下导出与附件的单块字节数。连接在发送缓冲降到 256 KiB 以下才发
296
+ * 下一块,缓冲峰值约 512 KiB,远低于 4 MiB 的慢客户端断开线。 */
297
+ const BINARY_CHUNK_BYTES = 262144;
298
+ /** 把 envelope 头部与原始字节拼成一帧。 */
299
+ function encodeBinaryFrame(header, body) {
300
+ const headerBytes = Buffer.from(JSON.stringify(header), "utf8");
301
+ const frame = Buffer.allocUnsafe(5 + headerBytes.length + body.length);
302
+ frame.writeUInt8(1, 0);
303
+ frame.writeUInt32BE(headerBytes.length, 1);
304
+ headerBytes.copy(frame, 5);
305
+ frame.set(body, 5 + headerBytes.length);
306
+ return frame;
307
+ }
308
+ // src/auth-rate-limit.ts
292
309
  const DEFAULT_AUTH_RATE_POLICY = {
293
310
  windowMs: 6e4,
294
311
  attemptsPerSource: 12,
@@ -405,8 +422,7 @@ var AuthRateLimiter = class {
405
422
  }
406
423
  }
407
424
  };
408
- //#endregion
409
- //#region src/diagnostics.ts
425
+ // src/diagnostics.ts
410
426
  /**
411
427
  * 连接诊断事件模型(Diagnostics)——插件唯一的「失败可观测」面。
412
428
  *
@@ -469,7 +485,7 @@ function defaultLevel(kind) {
469
485
  function sanitizeText(value, maxChars) {
470
486
  return value.replace(/[\u0000-\u001f\u007f-\u009f]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, maxChars);
471
487
  }
472
- function normalizeReason(reason) {
488
+ function normalizeReason$1(reason) {
473
489
  return sanitizeText(reason, MAX_REASON_CHARS).replace(/\s+/g, "-");
474
490
  }
475
491
  /**
@@ -550,7 +566,7 @@ var DiagnosticsStore = class {
550
566
  const event = {
551
567
  ts,
552
568
  kind,
553
- reason: normalizeReason(reason),
569
+ reason: normalizeReason$1(reason),
554
570
  level: extra.level ?? defaultLevel(kind),
555
571
  ...attemptId !== void 0 ? { attemptId } : {},
556
572
  ...source !== void 0 ? { source } : {},
@@ -732,8 +748,7 @@ function buildHealthPayload(input) {
732
748
  }
733
749
  };
734
750
  }
735
- //#endregion
736
- //#region src/device-auth.ts
751
+ // src/device-auth.ts
737
752
  function getPairingCodeTtlMs() {
738
753
  return Number(process.env.DEEPPILOT_PAIRING_TTL_MS) || 3e5;
739
754
  }
@@ -822,6 +837,30 @@ function normalizeDeviceScopes(value) {
822
837
  const allowed = new Set(DEVICE_SCOPES);
823
838
  return [...new Set(value.filter((scope) => typeof scope === "string" && allowed.has(scope)))];
824
839
  }
840
+ /**
841
+ * 0.8.3 之前的「全部权限」:当时还没有 `schedule.manage`。App 配对时一直申请
842
+ * DeviceScope 的全部取值,设置页也没有收窄权限的入口,所以存量记录恰好等于这五项,
843
+ * 只说明「配对时这项权限还不存在」,不是有意关闭。
844
+ */
845
+ const LEGACY_FULL_SCOPES = [
846
+ "sessions.read",
847
+ "prompt.send",
848
+ "sessions.manage",
849
+ "interactions.respond",
850
+ "notifications.register"
851
+ ];
852
+ /**
853
+ * 存量设备权限升级:恰好等于旧版全部权限时补上之后新增的默认权限,返回新列表;
854
+ * 其余情况(含任何非完整集合)返回 null,原样保留。只放宽「旧版全量」这一种形态,
855
+ * 避免把将来可能出现的、有意收窄过的记录一并放开。
856
+ */
857
+ function upgradeLegacyFullScopes(value) {
858
+ if (!Array.isArray(value)) return null;
859
+ const current = new Set(value);
860
+ if (current.size !== LEGACY_FULL_SCOPES.length) return null;
861
+ if (!LEGACY_FULL_SCOPES.every((scope) => current.has(scope))) return null;
862
+ return [...DEFAULT_DEVICE_SCOPES];
863
+ }
825
864
  /** One active, single-use pairing grant per plugin runtime. */
826
865
  var PairingCodeManager = class {
827
866
  active = null;
@@ -857,8 +896,7 @@ function createAuthChallenge(audience, now = Date.now()) {
857
896
  expiresAt: now + AUTH_CHALLENGE_TTL_MS
858
897
  };
859
898
  }
860
- //#endregion
861
- //#region src/wire-interaction.ts
899
+ // src/wire-interaction.ts
862
900
  /**
863
901
  * 交互特性的 wire 行:回答待决的 approval 与 question。
864
902
  *
@@ -918,8 +956,7 @@ const interactionRows = [{
918
956
  ctx.send("s2c.ack", {});
919
957
  }
920
958
  }];
921
- //#endregion
922
- //#region src/wire-device.ts
959
+ // src/wire-device.ts
923
960
  /**
924
961
  * 设备生命周期特性的 wire 行:自撤销(解绑)。
925
962
  *
@@ -957,8 +994,7 @@ const deviceRows = [{
957
994
  ctx.close(4401, "device revoked");
958
995
  }
959
996
  }];
960
- //#endregion
961
- //#region src/token.ts
997
+ // src/token.ts
962
998
  /** Expand a leading ~ using the process home directory. */
963
999
  function expandHome(p) {
964
1000
  if (p === "~") return homedir();
@@ -1035,7 +1071,20 @@ var DeviceStore = class DeviceStore {
1035
1071
  const store = new DeviceStore(filePath);
1036
1072
  try {
1037
1073
  const raw = JSON.parse(await readFile(expandHome(filePath), "utf8"));
1038
- for (const rec of raw.devices ?? []) if (typeof rec.deviceId === "string") store.devices.set(rec.deviceId, rec);
1074
+ let upgraded = 0;
1075
+ for (const rec of raw.devices ?? []) {
1076
+ if (typeof rec.deviceId !== "string") continue;
1077
+ const next = rec.revokedAt === void 0 ? upgradeLegacyFullScopes(rec.scopes) : null;
1078
+ if (next !== null) {
1079
+ rec.scopes = next;
1080
+ upgraded += 1;
1081
+ }
1082
+ store.devices.set(rec.deviceId, rec);
1083
+ }
1084
+ if (upgraded > 0) {
1085
+ console.log(formatLogLine("info", `device scopes upgraded for ${upgraded} legacy device(s): schedule.manage added`));
1086
+ store.flush();
1087
+ }
1039
1088
  } catch (error) {
1040
1089
  if (error?.code === "ENOENT") {} else console.log(formatLogLine("warn", "device registry unreadable, starting empty: " + String(error)));
1041
1090
  }
@@ -1260,8 +1309,7 @@ var DeviceStore = class DeviceStore {
1260
1309
  console.log(formatLogLine("warn", "device registry write failed" + (typeof code === "string" ? " code=" + code : "") + ": " + message.slice(0, 160)));
1261
1310
  }
1262
1311
  };
1263
- //#endregion
1264
- //#region src/wire-push.ts
1312
+ // src/wire-push.ts
1265
1313
  /** Live Activity 需要的三个 scope:通知注册 + 会话读取 + 交互应答。 */
1266
1314
  const LIVE_ACTIVITY_SCOPES = [
1267
1315
  "notifications.register",
@@ -1408,21 +1456,83 @@ const pushRows = [
1408
1456
  }
1409
1457
  }
1410
1458
  ];
1411
- //#endregion
1412
- //#region src/wire-schedule.ts
1459
+ // src/wire-schedule.ts
1413
1460
  /**
1414
- * 定时任务特性的 wire 行:列表、历史、创建、修改、删除。
1461
+ * 定时任务特性的 wire 行:会话级(列表、历史、创建、修改、删除)与宿主级
1462
+ * (list/view/create/edit/run/limits)两套。
1415
1463
  *
1416
1464
  * scope 取 `schedule.manage` + `sessions.read`:PROTOCOL.md scope 映射表写明
1417
1465
  * 「`schedule.manage` 定时任务/提醒的列表、历史、创建、修改和删除;同时需要
1418
1466
  * `sessions.read` 才能读取任务内容」。迁移前这条组合一半住在 requiredScope、
1419
1467
  * 一半住在 connection.ts 调用点的一行补充规则里(334-336),现在是一行声明。
1420
1468
  *
1421
- * 五个帧共享一个能力位 `schedules`;该位现在只要求 Schedule 服务存在
1422
- * (见 host-capabilities.ts 的 G4 修正)。
1469
+ * 五个会话级帧共享能力位 `schedules`;该位现在只要求 Schedule 服务存在
1470
+ * (见 host-capabilities.ts 的 G4 修正)。六个宿主级帧共享 `schedulesHost`:
1471
+ * 该位由 adapter 判别位推导,false/缺失时新帧一律 E_UNSUPPORTED,客户端只能
1472
+ * 走会话级帧(PROTOCOL.md「Schedule 宿主级任务扩展」)。
1423
1473
  */
1424
1474
  /** PROTOCOL.md scope 映射表:schedule.manage 需搭配 sessions.read。 */
1425
1475
  const SCHEDULE_SCOPES = ["schedule.manage", "sessions.read"];
1476
+ /** 普通 JSON 对象(宿主级 timing/target/patch 都是嵌套对象)。 */
1477
+ function isObjectValue(value) {
1478
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1479
+ }
1480
+ /** `{time, time_zone}` 本地时间输入:取值合法性由 Host 完整校验。 */
1481
+ function isLocalTimeInput(value) {
1482
+ return isObjectValue(value) && isText(value.time) && isText(value.time_zone);
1483
+ }
1484
+ function isWeekdayList(value) {
1485
+ return Array.isArray(value) && value.length > 0 && value.every((day) => isInteger(day, 1, 7));
1486
+ }
1487
+ /**
1488
+ * 宿主级 timing 判别联合的形状检查。
1489
+ *
1490
+ * 只判结构与数量级(PROTOCOL.md 章节限额);日期格式、时区与频率是否可达由
1491
+ * Host 的 Schedule 服务负责——重复一遍只会制造两套口径。
1492
+ */
1493
+ function isHostTiming(value) {
1494
+ if (!isObjectValue(value)) return false;
1495
+ switch (value.kind) {
1496
+ case "after": return isInteger(value.after_seconds, 1, Number.MAX_SAFE_INTEGER);
1497
+ case "at": return isText(value.at) || isObjectValue(value.at) && isText(value.at.date) && isText(value.at.time) && isText(value.at.time_zone);
1498
+ case "every": return isInteger(value.every_seconds, 60, Number.MAX_SAFE_INTEGER);
1499
+ case "daily": return isLocalTimeInput(value.daily);
1500
+ case "weekly": return isObjectValue(value.weekly) && isLocalTimeInput(value.weekly) && isWeekdayList(value.weekly.weekdays);
1501
+ case "cron": return isObjectValue(value.cron) && isText(value.cron.expression) && isText(value.cron.time_zone);
1502
+ case "rrule": return isText(value.rrule);
1503
+ default: return false;
1504
+ }
1505
+ }
1506
+ /** `{cwd?, agentPreset?}`:省略即 Host 默认。 */
1507
+ function isHostCreateSpec(value) {
1508
+ return isObjectValue(value) && isOptionalField(value, "cwd", (v) => isText(v)) && isOptionalField(value, "agentPreset", (v) => isText(v));
1509
+ }
1510
+ /**
1511
+ * 创建/编辑请求里的 target。`session` 必须给 sessionId;`new_once`/`new` 的
1512
+ * 会话由 Host 预留或新建,`create` 省略即默认。客户端若把查看结果里的
1513
+ * `new_once.sessionId` 原样回传,也接受(未知/冗余字段不该让编辑失败)。
1514
+ */
1515
+ function isHostTarget(value) {
1516
+ if (!isObjectValue(value)) return false;
1517
+ switch (value.kind) {
1518
+ case "session": return isText(value.sessionId);
1519
+ case "new_once": return isOptionalField(value, "sessionId", (v) => isText(v)) && isOptionalField(value, "create", isHostCreateSpec);
1520
+ case "new": return isOptionalField(value, "create", isHostCreateSpec);
1521
+ default: return false;
1522
+ }
1523
+ }
1524
+ /** 编辑补丁:至少一个字段,且各字段形状合法。 */
1525
+ function isHostPatch(value) {
1526
+ if (!isObjectValue(value)) return false;
1527
+ if ([
1528
+ "title",
1529
+ "prompt",
1530
+ "timing",
1531
+ "target",
1532
+ "active"
1533
+ ].filter((key) => value[key] !== void 0).length === 0) return false;
1534
+ return isOptionalField(value, "title", (v) => isText(v, 120)) && isOptionalField(value, "prompt", (v) => isText(v, 262144)) && isOptionalField(value, "timing", isHostTiming) && isOptionalField(value, "target", isHostTarget) && isOptionalField(value, "active", (v) => typeof v === "boolean");
1535
+ }
1426
1536
  const scheduleRows = [
1427
1537
  {
1428
1538
  type: "c2s.schedule.list",
@@ -1454,14 +1564,16 @@ const scheduleRows = [
1454
1564
  validate: (payload) => {
1455
1565
  const checked = payloadObject(payload);
1456
1566
  if (!checked.ok) return checked;
1457
- if (!isText(checked.value.sessionId) || !isText(checked.value.id) || !isInteger(checked.value.limit, 1, 100) || !isOptionalField(checked.value, "before", (v) => isText(v))) return reject$1("E_PROTOCOL", "invalid schedule history");
1567
+ if (!isOptionalField(checked.value, "sessionId", (v) => isText(v)) || !isText(checked.value.id) || !isInteger(checked.value.limit, 1, 100) || !isOptionalField(checked.value, "before", (v) => isText(v))) return reject$1("E_PROTOCOL", "invalid schedule history");
1458
1568
  return checked;
1459
1569
  },
1460
1570
  handle: async (ctx, payload) => {
1461
- const result = await ctx.bridge.scheduleHistory(payload.sessionId, payload.id, payload.limit, payload.before);
1571
+ const sessionId = payload.sessionId;
1572
+ if (sessionId === void 0 && ctx.bridge.capabilities.schedulesHost !== true) return ctx.fail("E_PROTOCOL", "invalid schedule history");
1573
+ const result = await ctx.bridge.scheduleHistory(sessionId ?? "", payload.id, payload.limit, payload.before);
1462
1574
  if (!result.ok) return ctx.fail(result.code, result.message);
1463
1575
  ctx.send("s2c.schedule.history", {
1464
- sessionId: payload.sessionId,
1576
+ ...sessionId !== void 0 ? { sessionId } : {},
1465
1577
  history: result.value
1466
1578
  });
1467
1579
  }
@@ -1553,10 +1665,159 @@ const scheduleRows = [
1553
1665
  ...result.replayed ? { replayed: true } : {}
1554
1666
  });
1555
1667
  }
1668
+ },
1669
+ {
1670
+ type: "c2s.schedule.host.list",
1671
+ stage: "authenticated",
1672
+ scopes: SCHEDULE_SCOPES,
1673
+ capability: "schedulesHost",
1674
+ doc: "PROTOCOL.md c2s.schedule.host.list",
1675
+ validate: (payload) => {
1676
+ const checked = payloadObject(payload);
1677
+ if (!checked.ok) return checked;
1678
+ const p = checked.value;
1679
+ if (!isOptionalField(p, "active", (v) => typeof v === "boolean") || !isOptionalField(p, "limit", (v) => isInteger(v, 1, 500)) || !isOptionalField(p, "offset", (v) => isInteger(v, 0, Number.MAX_SAFE_INTEGER))) return reject$1("E_PROTOCOL", "invalid schedule host list");
1680
+ return checked;
1681
+ },
1682
+ handle: async (ctx, payload) => {
1683
+ const result = await ctx.bridge.listHostSchedules({
1684
+ ...payload.active !== void 0 ? { active: payload.active } : {},
1685
+ ...payload.limit !== void 0 ? { limit: payload.limit } : {},
1686
+ ...payload.offset !== void 0 ? { offset: payload.offset } : {}
1687
+ });
1688
+ if (!result.ok) return ctx.fail(result.code, result.message);
1689
+ ctx.send("s2c.schedule.host.snapshot", result.value);
1690
+ }
1691
+ },
1692
+ {
1693
+ type: "c2s.schedule.host.view",
1694
+ stage: "authenticated",
1695
+ scopes: SCHEDULE_SCOPES,
1696
+ capability: "schedulesHost",
1697
+ doc: "PROTOCOL.md c2s.schedule.host.view",
1698
+ validate: (payload) => {
1699
+ const checked = payloadObject(payload);
1700
+ if (!checked.ok) return checked;
1701
+ if (!isText(checked.value.id)) return reject$1("E_PROTOCOL", "invalid schedule id");
1702
+ return checked;
1703
+ },
1704
+ handle: async (ctx, payload) => {
1705
+ const result = await ctx.bridge.viewHostSchedule(payload.id);
1706
+ if (!result.ok) return ctx.fail(result.code, result.message);
1707
+ ctx.send("s2c.schedule.host.task", { task: result.value });
1708
+ }
1709
+ },
1710
+ {
1711
+ type: "c2s.schedule.host.create",
1712
+ stage: "authenticated",
1713
+ scopes: SCHEDULE_SCOPES,
1714
+ capability: "schedulesHost",
1715
+ doc: "PROTOCOL.md c2s.schedule.host.create",
1716
+ validate: (payload) => {
1717
+ const checked = payloadObject(payload);
1718
+ if (!checked.ok) return checked;
1719
+ const p = checked.value;
1720
+ if (!validSendId(p.clientRequestId) || !isText(p.title, 120) || !isText(p.prompt, 262144) || !isHostTiming(p.timing) || !isOptionalField(p, "target", isHostTarget)) return reject$1("E_PROTOCOL", "invalid schedule host create fields");
1721
+ return checked;
1722
+ },
1723
+ handle: async (ctx, payload) => {
1724
+ const result = await ctx.bridge.createHostSchedule(ctx.deviceId, payload);
1725
+ if (!result.ok) return ctx.fail(result.code, result.message);
1726
+ ctx.send("s2c.schedule.updated", {
1727
+ clientRequestId: payload.clientRequestId,
1728
+ task: result.value,
1729
+ ...result.replayed ? { replayed: true } : {}
1730
+ });
1731
+ }
1732
+ },
1733
+ {
1734
+ type: "c2s.schedule.host.edit",
1735
+ stage: "authenticated",
1736
+ scopes: SCHEDULE_SCOPES,
1737
+ capability: "schedulesHost",
1738
+ doc: "PROTOCOL.md c2s.schedule.host.edit",
1739
+ validate: (payload) => {
1740
+ const checked = payloadObject(payload);
1741
+ if (!checked.ok) return checked;
1742
+ const p = checked.value;
1743
+ if (!validSendId(p.clientRequestId) || !isText(p.id) || !isOptionalField(p, "expectedUpdatedAt", (v) => v === null || isText(v))) return reject$1("E_PROTOCOL", "invalid schedule host edit fields");
1744
+ if (p.action !== "update" && p.action !== "delete") return reject$1("E_PROTOCOL", "invalid schedule host edit action");
1745
+ if (p.action === "update" && !isHostPatch(p.patch)) return reject$1("E_PROTOCOL", "invalid schedule host edit patch");
1746
+ return checked;
1747
+ },
1748
+ handle: async (ctx, payload) => {
1749
+ const result = await ctx.bridge.editHostSchedule(ctx.deviceId, payload);
1750
+ if (!result.ok) return ctx.fail(result.code, result.message);
1751
+ const value = result.value;
1752
+ ctx.send("s2c.schedule.updated", {
1753
+ clientRequestId: payload.clientRequestId,
1754
+ ..."deleted" in value ? { deleted: true } : { task: value.task },
1755
+ ...result.replayed ? { replayed: true } : {}
1756
+ });
1757
+ }
1758
+ },
1759
+ {
1760
+ type: "c2s.schedule.host.run",
1761
+ stage: "authenticated",
1762
+ scopes: SCHEDULE_SCOPES,
1763
+ capability: "schedulesHost",
1764
+ doc: "PROTOCOL.md c2s.schedule.host.run",
1765
+ validate: (payload) => {
1766
+ const checked = payloadObject(payload);
1767
+ if (!checked.ok) return checked;
1768
+ if (!validSendId(checked.value.clientRequestId) || !isText(checked.value.id)) return reject$1("E_PROTOCOL", "invalid schedule host run fields");
1769
+ return checked;
1770
+ },
1771
+ handle: async (ctx, payload) => {
1772
+ const result = await ctx.bridge.runHostSchedule(ctx.deviceId, {
1773
+ clientRequestId: payload.clientRequestId,
1774
+ id: payload.id
1775
+ });
1776
+ if (!result.ok) return ctx.fail(result.code, result.message);
1777
+ ctx.send("s2c.schedule.updated", {
1778
+ clientRequestId: payload.clientRequestId,
1779
+ task: result.value,
1780
+ ...result.replayed ? { replayed: true } : {}
1781
+ });
1782
+ }
1783
+ },
1784
+ {
1785
+ type: "c2s.schedule.host.deleteEnded",
1786
+ stage: "authenticated",
1787
+ scopes: SCHEDULE_SCOPES,
1788
+ capability: "schedulesHost",
1789
+ doc: "PROTOCOL.md c2s.schedule.host.deleteEnded",
1790
+ validate: (payload) => {
1791
+ const checked = payloadObject(payload);
1792
+ if (!checked.ok) return checked;
1793
+ if (!validSendId(checked.value.clientRequestId)) return reject$1("E_PROTOCOL", "invalid schedule host delete ended fields");
1794
+ return checked;
1795
+ },
1796
+ handle: async (ctx, payload) => {
1797
+ const result = await ctx.bridge.deleteEndedHostSchedules(ctx.deviceId, { clientRequestId: payload.clientRequestId });
1798
+ if (!result.ok) return ctx.fail(result.code, result.message);
1799
+ ctx.send("s2c.schedule.updated", {
1800
+ clientRequestId: payload.clientRequestId,
1801
+ ...result.value.deleted !== void 0 ? { deletedEnded: result.value.deleted } : {},
1802
+ ...result.replayed ? { replayed: true } : {}
1803
+ });
1804
+ }
1805
+ },
1806
+ {
1807
+ type: "c2s.schedule.host.limits",
1808
+ stage: "authenticated",
1809
+ scopes: SCHEDULE_SCOPES,
1810
+ capability: "schedulesHost",
1811
+ doc: "PROTOCOL.md c2s.schedule.host.limits",
1812
+ validate: (payload) => payloadObject(payload),
1813
+ handle: async (ctx) => {
1814
+ const result = await ctx.bridge.scheduleLimits();
1815
+ if (!result.ok) return ctx.fail(result.code, result.message);
1816
+ ctx.send("s2c.schedule.host.limits", result.value);
1817
+ }
1556
1818
  }
1557
1819
  ];
1558
- //#endregion
1559
- //#region src/document-payload.ts
1820
+ // src/document-payload.ts
1560
1821
  const PREFIX = "[DeepPilot document:";
1561
1822
  const SUFFIX = "]";
1562
1823
  /**
@@ -1593,8 +1854,7 @@ function projectedDocument(text) {
1593
1854
  return;
1594
1855
  }
1595
1856
  }
1596
- //#endregion
1597
- //#region src/host-event-projection.ts
1857
+ // src/host-event-projection.ts
1598
1858
  const MAX_MESSAGE_PROJECTION_BYTES = 262144;
1599
1859
  /** One durable host event sequence becomes exactly one phone message row.
1600
1860
  * Keep the last projection when a host history response repeats an event. */
@@ -1682,7 +1942,8 @@ function projectEvent(sessionId, event) {
1682
1942
  name: String(data?.name ?? "tool"),
1683
1943
  state: "running",
1684
1944
  summary: summarizeArgs(data?.arguments),
1685
- ...data?.callId ? { callId: String(data.callId) } : {}
1945
+ ...data?.callId ? { callId: String(data.callId) } : {},
1946
+ ...editField(data?.name, data?.arguments)
1686
1947
  },
1687
1948
  ts: tsOf(event)
1688
1949
  }
@@ -1730,6 +1991,12 @@ function userMessageSource(data) {
1730
1991
  if (obj.source && typeof obj.source === "object") return obj.source;
1731
1992
  if (obj.message && typeof obj.message === "object" && obj.message.source && typeof obj.message.source === "object") return obj.message.source;
1732
1993
  }
1994
+ /** Source kind of one user/message payload (`user`, `schedule`, injected
1995
+ * context kinds, …); undefined on legacy hosts that record no source. */
1996
+ function userMessageSourceKind(data) {
1997
+ const kind = userMessageSource(data)?.kind;
1998
+ return typeof kind === "string" ? kind : void 0;
1999
+ }
1733
2000
  /** Wire role for one user/message payload. A payload without any readable
1734
2001
  * source degrades to 'user' so history written by older hosts stays visible;
1735
2002
  * a present source follows the host's own trajectory rule — anything whose
@@ -1882,6 +2149,10 @@ function chunkText(data) {
1882
2149
  const direct = data;
1883
2150
  return typeof direct.text === "string" ? direct.text : "";
1884
2151
  }
2152
+ function editField(name, raw) {
2153
+ const edit = toolEditOf(name, raw);
2154
+ return edit ? { edit } : {};
2155
+ }
1885
2156
  function summarizeArgs(raw) {
1886
2157
  if (typeof raw !== "string" || raw.length === 0) return "";
1887
2158
  try {
@@ -1893,6 +2164,68 @@ function summarizeArgs(raw) {
1893
2164
  return truncate(raw, 90);
1894
2165
  }
1895
2166
  }
2167
+ /** 宿主的文件编辑工具名(DSH `dsh-tool-str-replace-editor`)。 */
2168
+ const EDITOR_TOOL_NAME = "str_replace_editor";
2169
+ /**
2170
+ * 把编辑类工具调用的原始参数归一化成 `ToolEditProjection`。
2171
+ *
2172
+ * 只认 `str_replace_editor` 的 `str_replace` / `create` / `insert`(`view` 不改文件),
2173
+ * 其他工具或无法解析的参数返回 undefined,工具行保持原样。与 iOS `ToolEditDiff.parse`
2174
+ * 的解析口径一致:`new_str` 缺失视为删除(空串)。
2175
+ */
2176
+ function toolEditOf(name, raw) {
2177
+ if (name !== EDITOR_TOOL_NAME || typeof raw !== "string" || raw.length === 0) return void 0;
2178
+ let args;
2179
+ try {
2180
+ const parsed = JSON.parse(raw);
2181
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
2182
+ args = parsed;
2183
+ } catch {
2184
+ return;
2185
+ }
2186
+ const path = args.path;
2187
+ if (typeof path !== "string" || path.length === 0) return void 0;
2188
+ let edit;
2189
+ switch (args.command) {
2190
+ case "str_replace":
2191
+ if (typeof args.old_str !== "string") return void 0;
2192
+ edit = {
2193
+ path,
2194
+ operation: "replace",
2195
+ oldText: args.old_str,
2196
+ newText: typeof args.new_str === "string" ? args.new_str : ""
2197
+ };
2198
+ break;
2199
+ case "create":
2200
+ if (typeof args.file_text !== "string") return void 0;
2201
+ edit = {
2202
+ path,
2203
+ operation: "create",
2204
+ newText: args.file_text
2205
+ };
2206
+ break;
2207
+ case "insert":
2208
+ if (typeof args.new_str !== "string") return void 0;
2209
+ edit = {
2210
+ path,
2211
+ operation: "insert",
2212
+ newText: args.new_str,
2213
+ ...Number.isSafeInteger(args.insert_line) ? { insertLine: args.insert_line } : {}
2214
+ };
2215
+ break;
2216
+ default: return;
2217
+ }
2218
+ return [edit.oldText, edit.newText].some((text) => text !== void 0 && Buffer.byteLength(text, "utf8") > 16384) ? withoutEditBodies(edit) : edit;
2219
+ }
2220
+ /** 去掉改动正文,只留路径、操作与 truncated 标记(超限或整条消息超限时用)。 */
2221
+ function withoutEditBodies(edit) {
2222
+ return {
2223
+ path: edit.path,
2224
+ operation: edit.operation,
2225
+ ...edit.insertLine !== void 0 ? { insertLine: edit.insertLine } : {},
2226
+ truncated: true
2227
+ };
2228
+ }
1896
2229
  function truncate(text, max) {
1897
2230
  return text.length <= max ? text : text.slice(0, max - 1) + "…";
1898
2231
  }
@@ -1936,7 +2269,8 @@ function projectHistory(events) {
1936
2269
  tool: {
1937
2270
  name: String(data?.name ?? "tool"),
1938
2271
  state: "running",
1939
- summary: summarizeArgs(data?.arguments)
2272
+ summary: summarizeArgs(data?.arguments),
2273
+ ...editField(data?.name, data?.arguments)
1940
2274
  }
1941
2275
  };
1942
2276
  messages.push(row);
@@ -1985,7 +2319,8 @@ function limitMessageProjection(message) {
1985
2319
  ...message.tool ? { tool: {
1986
2320
  ...message.tool,
1987
2321
  name: truncateUtf8(message.tool.name, 4096),
1988
- summary: truncateUtf8(message.tool.summary, 65536)
2322
+ summary: truncateUtf8(message.tool.summary, 65536),
2323
+ ...message.tool.edit ? { edit: withoutEditBodies(message.tool.edit) } : {}
1989
2324
  } } : {},
1990
2325
  ...message.attachments ? { attachments: message.attachments.slice(0, 16).map((attachment) => ({
1991
2326
  ...attachment,
@@ -2129,8 +2464,7 @@ function historyWindow(source, options) {
2129
2464
  hasMore: older.length > 0
2130
2465
  };
2131
2466
  }
2132
- //#endregion
2133
- //#region src/wire-session.ts
2467
+ // src/wire-session.ts
2134
2468
  /**
2135
2469
  * 会话特性的 wire 行:列表/归档、打开与关闭、创建/分支/重命名/取消、历史与
2136
2470
  * 附件、模型目录与切换、prompt 投递。
@@ -2464,6 +2798,9 @@ const sessionRows = [
2464
2798
  const invalid = requireSessionId(checked.value);
2465
2799
  if (invalid) return invalid;
2466
2800
  if (!isText(checked.value.attachmentId)) return reject$1("E_PROTOCOL", "invalid attachmentId");
2801
+ const maxFrameBytes = checked.value.maxFrameBytes;
2802
+ if (maxFrameBytes !== void 0 && (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes <= 0)) return reject$1("E_PROTOCOL", "invalid maxFrameBytes");
2803
+ if (checked.value.binary !== void 0 && typeof checked.value.binary !== "boolean") return reject$1("E_PROTOCOL", "invalid binary");
2467
2804
  return checked;
2468
2805
  },
2469
2806
  handle: async (ctx, payload) => {
@@ -2471,6 +2808,34 @@ const sessionRows = [
2471
2808
  if (attachmentId.length === 0) return ctx.fail("E_PROTOCOL", "sessionId and attachmentId required");
2472
2809
  const image = await ctx.bridge.attachmentData(payload.sessionId, attachmentId);
2473
2810
  if (!image) return ctx.fail("E_NOT_FOUND", "attachment unavailable");
2811
+ if (payload.binary === true && ctx.sendBinary) {
2812
+ const bytes = Buffer.from(image.data, "base64");
2813
+ if (bytes.length === 0) return ctx.fail("E_NOT_FOUND", "attachment unavailable");
2814
+ if (bytes.length > 12582912) return ctx.fail("E_TOO_LARGE", `attachment ${bytes.length} bytes exceeds limit`);
2815
+ let index = 0;
2816
+ try {
2817
+ for (let offset = 0; offset < bytes.length; offset += BINARY_CHUNK_BYTES) {
2818
+ await ctx.waitForWritable?.();
2819
+ ctx.sendBinary("s2c.session.attachment.chunk", { index: index++ }, bytes.subarray(offset, offset + BINARY_CHUNK_BYTES));
2820
+ }
2821
+ } catch {
2822
+ return;
2823
+ }
2824
+ const ack = {
2825
+ ...image.mediaType !== void 0 ? { mediaType: image.mediaType } : {},
2826
+ chunks: index,
2827
+ bytes: bytes.length
2828
+ };
2829
+ return ctx.send("s2c.ack", ack);
2830
+ }
2831
+ const clientLimit = payload.maxFrameBytes ?? 1048576;
2832
+ const frameBytes = image.data.length + (image.mediaType?.length ?? 0) + ATTACHMENT_ENVELOPE_HEADROOM_BYTES;
2833
+ if (frameBytes > Math.min(clientLimit, 2097152)) return ctx.fail("E_TOO_LARGE", `attachment frame ${frameBytes} bytes exceeds limit`);
2834
+ try {
2835
+ await ctx.waitForWritable?.();
2836
+ } catch {
2837
+ return;
2838
+ }
2474
2839
  ctx.send("s2c.ack", image);
2475
2840
  }
2476
2841
  },
@@ -2682,8 +3047,7 @@ const sessionRows = [
2682
3047
  }
2683
3048
  }
2684
3049
  ];
2685
- //#endregion
2686
- //#region src/wire-command.ts
3050
+ // src/wire-command.ts
2687
3051
  function sessionPayload(value) {
2688
3052
  const result = payloadObject(value);
2689
3053
  return result.ok && !isText(result.value.sessionId) ? reject$1("E_PROTOCOL", "会话标识无效") : result;
@@ -2755,6 +3119,7 @@ const commandRows = [...[
2755
3119
  const result = sessionPayload(value);
2756
3120
  if (!result.ok) return result;
2757
3121
  if (result.value.format !== void 0 && result.value.format !== "zip" && result.value.format !== "markdown") return reject$1("E_PROTOCOL", "导出格式无效");
3122
+ if (result.value.binary !== void 0 && typeof result.value.binary !== "boolean") return reject$1("E_PROTOCOL", "传输方式无效");
2758
3123
  return result;
2759
3124
  },
2760
3125
  handle: async (ctx, payload) => {
@@ -2768,22 +3133,34 @@ const commandRows = [...[
2768
3133
  try {
2769
3134
  reader = (format === void 0 ? await ctx.bridge.exportSession(payload.sessionId, signal) : await ctx.bridge.exportSession(payload.sessionId, signal, format)).body.getReader();
2770
3135
  signal.addEventListener("abort", cancel, { once: true });
3136
+ const binary = payload.binary === true && ctx.sendBinary !== void 0;
2771
3137
  let index = 0, bytes = 0;
3138
+ const sendChunk = async (chunk) => {
3139
+ bytes += chunk.length;
3140
+ await ctx.waitForWritable?.();
3141
+ if (binary) ctx.sendBinary("s2c.session.export.chunk", { index: index++ }, chunk);
3142
+ else ctx.send("s2c.session.export.chunk", {
3143
+ index: index++,
3144
+ data: Buffer.from(chunk).toString("base64")
3145
+ });
3146
+ };
3147
+ let carry;
2772
3148
  while (true) {
2773
3149
  signal.throwIfAborted();
2774
3150
  const next = await reader.read();
2775
3151
  if (next.done) break;
2776
- for (let offset = 0; offset < next.value.length; offset += 49152) {
2777
- const chunk = next.value.subarray(offset, offset + 49152);
2778
- bytes += chunk.length;
2779
- await ctx.waitForWritable?.();
2780
- ctx.send("s2c.session.export.chunk", {
2781
- index: index++,
2782
- data: Buffer.from(chunk).toString("base64")
2783
- });
3152
+ if (!binary) {
3153
+ for (let offset = 0; offset < next.value.length; offset += 49152) await sendChunk(next.value.subarray(offset, offset + 49152));
3154
+ continue;
3155
+ }
3156
+ carry = carry ? Buffer.concat([carry, next.value]) : Buffer.from(next.value);
3157
+ while (carry.length >= BINARY_CHUNK_BYTES) {
3158
+ await sendChunk(carry.subarray(0, BINARY_CHUNK_BYTES));
3159
+ carry = carry.subarray(BINARY_CHUNK_BYTES);
2784
3160
  }
2785
3161
  }
2786
3162
  signal.throwIfAborted();
3163
+ if (carry !== void 0 && carry.length > 0) await sendChunk(carry);
2787
3164
  ctx.send("s2c.ack", {
2788
3165
  sessionId: payload.sessionId,
2789
3166
  fileName: format === "markdown" ? "dsh-session.md" : "dsh-session-log.zip",
@@ -2806,8 +3183,7 @@ const commandRows = [...[
2806
3183
  }
2807
3184
  }
2808
3185
  }];
2809
- //#endregion
2810
- //#region src/plugin-update.ts
3186
+ // src/plugin-update.ts
2811
3187
  const PACKAGE = "dsh-deeppilot";
2812
3188
  const PHASES = [
2813
3189
  "preparing",
@@ -3254,8 +3630,7 @@ function supportsPluginUpdate(value) {
3254
3630
  "registries"
3255
3631
  ].every((key) => typeof value[key] === "function");
3256
3632
  }
3257
- //#endregion
3258
- //#region src/wire-plugin-update.ts
3633
+ // src/wire-plugin-update.ts
3259
3634
  function payloadObject$1(value) {
3260
3635
  if (value === void 0 || value === null) return {
3261
3636
  ok: true,
@@ -3459,8 +3834,7 @@ const ROWS_BY_TYPE = new Map(WIRE_FRAME_ROWS.map((row) => [row.type, row]));
3459
3834
  function registryRowFor(type) {
3460
3835
  return ROWS_BY_TYPE.get(type);
3461
3836
  }
3462
- //#endregion
3463
- //#region src/connection-gate.ts
3837
+ // src/connection-gate.ts
3464
3838
  /**
3465
3839
  * 连接门(ConnectionGate)——「一个匿名 socket 被允许做什么」的唯一属主。
3466
3840
  *
@@ -3752,6 +4126,7 @@ var ConnectionGate = class {
3752
4126
  debug: host.debug,
3753
4127
  log: (message) => host.log(message),
3754
4128
  send: (type, payload) => host.send(type, payload, env.id),
4129
+ ...host.sendBinary ? { sendBinary: (type, payload, body) => host.sendBinary(type, payload, body, env.id) } : {},
3755
4130
  fail: (code, message) => host.fail(env.id, code, message),
3756
4131
  close: (code, reason) => host.close(code, reason),
3757
4132
  canReceive: (scope) => host.canReceive(scope),
@@ -3779,8 +4154,7 @@ var ConnectionGate = class {
3779
4154
  };
3780
4155
  }
3781
4156
  };
3782
- //#endregion
3783
- //#region src/connection.ts
4157
+ // src/connection.ts
3784
4158
  /**
3785
4159
  * One connected phone. Implements BridgeSink so the HostBridge can push
3786
4160
  * projected frames and replays.
@@ -3852,6 +4226,7 @@ var BridgeConnection = class {
3852
4226
  },
3853
4227
  log: (message) => deps.log(message),
3854
4228
  send: (type, payload, id) => connection.send(type, payload, id),
4229
+ sendBinary: (type, payload, body, id) => connection.send(type, payload, id, void 0, body),
3855
4230
  fail: (id, code, message) => connection.fail(id, code, message),
3856
4231
  close: (code, reason) => connection.close(code, reason),
3857
4232
  canReceive: (scope) => connection.canReceive(scope),
@@ -4021,7 +4396,8 @@ var BridgeConnection = class {
4021
4396
  this.ws.terminate();
4022
4397
  }
4023
4398
  }
4024
- send(type, payload, id, seq) {
4399
+ /** `body` 存在时按二进制帧发送(见 binary-frame.ts),头部仍是同一个 envelope。 */
4400
+ send(type, payload, id, seq, body) {
4025
4401
  const envelope = {
4026
4402
  v: 2,
4027
4403
  type,
@@ -4035,7 +4411,7 @@ var BridgeConnection = class {
4035
4411
  this.close(1013, "client too slow");
4036
4412
  return;
4037
4413
  }
4038
- this.ws.send(JSON.stringify(envelope));
4414
+ this.ws.send(body === void 0 ? JSON.stringify(envelope) : encodeBinaryFrame(envelope, body));
4039
4415
  }
4040
4416
  fail(id, code, message) {
4041
4417
  this.send("s2c.error", {
@@ -4069,8 +4445,7 @@ var BridgeConnection = class {
4069
4445
  function normalizeFunnelConnectionLimit(value) {
4070
4446
  return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 16 ? value : 8;
4071
4447
  }
4072
- //#endregion
4073
- //#region src/tailscale-version.ts
4448
+ // src/tailscale-version.ts
4074
4449
  /**
4075
4450
  * 内嵌 helper 上报的 tailscale.com 模块版本。
4076
4451
  *
@@ -4093,8 +4468,7 @@ const MAX_TAILSCALE_VERSION_LENGTH = 80;
4093
4468
  function isTailscaleVersion(value) {
4094
4469
  return typeof value === "string" && value.length <= MAX_TAILSCALE_VERSION_LENGTH && TAILSCALE_VERSION_PATTERN.test(value);
4095
4470
  }
4096
- //#endregion
4097
- //#region src/remote-supervisor.ts
4471
+ // src/remote-supervisor.ts
4098
4472
  const RESTART_DELAYS_MS = [
4099
4473
  1e3,
4100
4474
  2e3,
@@ -4523,8 +4897,7 @@ var RemoteSupervisor = class {
4523
4897
  else if (next.phase === "error" || next.phase === "unavailable") this.options.log(`remote Funnel ${next.phase}: ${next.message ?? "unknown error"}`);
4524
4898
  }
4525
4899
  };
4526
- //#endregion
4527
- //#region src/local-policy.ts
4900
+ // src/local-policy.ts
4528
4901
  const DEFAULT_LOCAL_PORT = 3098;
4529
4902
  const MIN_LOCAL_PORT = 1024;
4530
4903
  const MAX_LOCAL_PORT = 65535;
@@ -4542,8 +4915,7 @@ function localListenError(error, port) {
4542
4915
  if (code === "EACCES") return `permission denied while opening local port ${port}`;
4543
4916
  return error instanceof Error ? error.message : String(error);
4544
4917
  }
4545
- //#endregion
4546
- //#region src/config.ts
4918
+ // src/config.ts
4547
4919
  /** Operator-run relay used by distributed builds; overridable via config. */
4548
4920
  const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
4549
4921
  const Config = z.object({
@@ -4632,8 +5004,7 @@ function normalizeOptions(options) {
4632
5004
  if (options && typeof options === "object") return plainConfig(options);
4633
5005
  return plainConfig(Config(void 0) ?? {});
4634
5006
  }
4635
- //#endregion
4636
- //#region src/apns.ts
5007
+ // src/apns.ts
4637
5008
  /**
4638
5009
  * Minimal APNs provider client (HTTP/2) with zero npm dependencies.
4639
5010
  *
@@ -4870,8 +5241,7 @@ var ApnsClient = class {
4870
5241
  return token.length <= 10 ? "…" : token.slice(0, 6) + "…" + token.slice(-4);
4871
5242
  }
4872
5243
  };
4873
- //#endregion
4874
- //#region src/relay-url.ts
5244
+ // src/relay-url.ts
4875
5245
  /**
4876
5246
  * Normalize an operator-configured relay URL without a backtracking regular
4877
5247
  * expression. Scanning from the end keeps even pathological inputs linear.
@@ -4882,8 +5252,7 @@ function normalizeRelayBaseUrl(value) {
4882
5252
  while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end -= 1;
4883
5253
  return end === trimmed.length ? trimmed : trimmed.slice(0, end);
4884
5254
  }
4885
- //#endregion
4886
- //#region src/relay-client.ts
5255
+ // src/relay-client.ts
4887
5256
  var RelayClient = class {
4888
5257
  base;
4889
5258
  token;
@@ -4976,8 +5345,7 @@ var RelayClient = class {
4976
5345
  }
4977
5346
  }
4978
5347
  };
4979
- //#endregion
4980
- //#region src/widget-push.ts
5348
+ // src/widget-push.ts
4981
5349
  /** Coalesce bursts and bound continuous updates, without starving the trailing
4982
5350
  * state. One scheduler per host, not per token or per streaming text frame. */
4983
5351
  var WidgetPushScheduler = class {
@@ -5014,8 +5382,7 @@ var WidgetPushScheduler = class {
5014
5382
  this.timer = void 0;
5015
5383
  }
5016
5384
  };
5017
- //#endregion
5018
- //#region src/live-activity.ts
5385
+ // src/live-activity.ts
5019
5386
  function liveActivityState(session) {
5020
5387
  const total = Math.max(0, session?.todos?.total ?? session?.todoItems?.length ?? 0);
5021
5388
  const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
@@ -5087,8 +5454,7 @@ var LiveActivityPushManager = class {
5087
5454
  this.scheduler.dispose();
5088
5455
  }
5089
5456
  };
5090
- //#endregion
5091
- //#region src/push-policy.ts
5457
+ // src/push-policy.ts
5092
5458
  /** Prune only when the provider supplies an authoritative token-lifecycle verdict. */
5093
5459
  function shouldPrunePushToken(outcome, reason) {
5094
5460
  return outcome === "invalid-token" && (reason === "Unregistered" || reason === "ExpiredToken");
@@ -5124,8 +5490,7 @@ function pushContent(notification, mode) {
5124
5490
  body: bodies[notification.category] ?? "Open DeepPilot for an update."
5125
5491
  };
5126
5492
  }
5127
- //#endregion
5128
- //#region src/relay-test.ts
5493
+ // src/relay-test.ts
5129
5494
  /**
5130
5495
  * Relay connectivity self-test used by the settings page "测试中继" button.
5131
5496
  *
@@ -5251,8 +5616,7 @@ async function runRelayProbe(options) {
5251
5616
  steps
5252
5617
  };
5253
5618
  }
5254
- //#endregion
5255
- //#region src/push-gateway.ts
5619
+ // src/push-gateway.ts
5256
5620
  /**
5257
5621
  * 推送网关(PushGateway)——离线推送的单一属主。
5258
5622
  *
@@ -5742,8 +6106,7 @@ var PushGateway = class {
5742
6106
  await this.ensureRelayEnrolled(url);
5743
6107
  }
5744
6108
  };
5745
- //#endregion
5746
- //#region src/phone-server.ts
6109
+ // src/phone-server.ts
5747
6110
  /**
5748
6111
  * A deliberately narrow transport listener. Both the LAN endpoint and the
5749
6112
  * loopback-only Funnel origin use this factory, so neither can accidentally
@@ -5822,8 +6185,7 @@ function closeServer(server) {
5822
6185
  if (server === void 0 || !server.listening) return Promise.resolve();
5823
6186
  return new Promise((resolve) => server.close(() => resolve()));
5824
6187
  }
5825
- //#endregion
5826
- //#region src/transport-reconciler.ts
6188
+ // src/transport-reconciler.ts
5827
6189
  var TransportReconciler = class {
5828
6190
  spec;
5829
6191
  config;
@@ -6051,8 +6413,7 @@ function createRemoteTransport(deps) {
6051
6413
  statusOf: (supervisor) => supervisor?.status() ?? fallbackStatus()
6052
6414
  }, deps.config, deps.log, fallbackStatus());
6053
6415
  }
6054
- //#endregion
6055
- //#region src/dispatch-journal.ts
6416
+ // src/dispatch-journal.ts
6056
6417
  /**
6057
6418
  * at-most-once dispatch journal:prompt 投递与非 prompt 变更的**同一个** module。
6058
6419
  *
@@ -6484,8 +6845,7 @@ function scheduleMutationCodec(persistValues) {
6484
6845
  }))
6485
6846
  };
6486
6847
  }
6487
- //#endregion
6488
- //#region src/notification-text.ts
6848
+ // src/notification-text.ts
6489
6849
  /**
6490
6850
  * 通知正文的纯文本投影。
6491
6851
  *
@@ -6528,8 +6888,7 @@ function decodePercentRuns(text) {
6528
6888
  }
6529
6889
  });
6530
6890
  }
6531
- //#endregion
6532
- //#region src/host-api.ts
6891
+ // src/host-api.ts
6533
6892
  /**
6534
6893
  * Subagent sessions are host-internal workers of a parent conversation.
6535
6894
  * They must never surface on the phone: not in the project/session list,
@@ -6552,8 +6911,231 @@ function unwrapStreamItem(item) {
6552
6911
  } : nested;
6553
6912
  return item;
6554
6913
  }
6555
- //#endregion
6556
- //#region src/host-capabilities.ts
6914
+ // src/quiet-completion.ts
6915
+ /**
6916
+ * 计划任务按需完成通知(#35)。
6917
+ *
6918
+ * heartbeat 式计划任务大多数轮次"一切正常",每轮都推「任务完成」是噪音。插件
6919
+ * 给计划任务会话提供一个 Agent 工具:Agent 按任务提示词里的规则判断本轮不需要
6920
+ * 提醒时调用它,本轮成功结束就不发完成通知(不写 s2c.notify、不推 APNs)。
6921
+ * 失败、审批、提问照常通知;模型忘了调用时照常提醒,宁可多一次打扰也不漏报。
6922
+ *
6923
+ * 职责划分:
6924
+ * - scheduledTurnOf:同步读宿主会话日志,判定当前这一轮是否由计划任务投递驱动。
6925
+ * 工具的 `when` 与 `execute` 都在宿主进程内同步调用它,不依赖桥接层异步收到的
6926
+ * 事件(实测 `when` 求值时本轮 user/message 尚未落盘,桥接层也还没收到)。
6927
+ * - QuietCompletionTracker:计划任务会话的粘性集合,以及每个会话"本轮已请求静默"
6928
+ * 的记录;HostBridge 在 user/message 与 turn.end 处调用它。
6929
+ * - quietCompletionTool:面向模型的工具定义,由 index.ts 注册到宿主 `ctx.tools`。
6930
+ *
6931
+ * 只在宿主级计划任务(`schedulesHost`,DSH 0.2.2 起)上注册:0.2.1 的
6932
+ * `tools.register` 不支持 `when`,多余参数会被静默忽略,工具会变成全局可见。
6933
+ */
6934
+ const QUIET_COMPLETION_TOOL_NAME = "deeppilot_skip_completion_notice";
6935
+ /** 往回扫描会话日志的上限:一轮内的事件远少于此,防止异常长轮次拖慢步骤准备。 */
6936
+ const MAX_TURN_SCAN_EVENTS = 2e3;
6937
+ function sessionLogOf(agent) {
6938
+ const session = agent?.session;
6939
+ if (!session || typeof session.id !== "string" || typeof session.seq !== "number" || typeof session.eventAt !== "function") return;
6940
+ return session;
6941
+ }
6942
+ function sourceKindOf(value) {
6943
+ const kind = value?.source?.kind;
6944
+ return typeof kind === "string" ? kind : void 0;
6945
+ }
6946
+ /** 一次 inbox 拼接里插入的条目(按插入顺序)及其目标队列。 */
6947
+ function insertedOf(event) {
6948
+ if (event.type !== "agent/inbox/spliced") return void 0;
6949
+ const data = event.data;
6950
+ const items = Array.isArray(data?.inserted) ? data.inserted : [];
6951
+ return items.length > 0 ? {
6952
+ target: data?.target,
6953
+ items
6954
+ } : void 0;
6955
+ }
6956
+ /**
6957
+ * 判定当前这一轮是否由计划任务投递驱动(同步读宿主会话日志,DSH 0.2.2-rc.1 实测形状)。
6958
+ *
6959
+ * 宿主把待处理消息先拼进 inbox(`agent/inbox/spliced`,带条目本体与目标队列),
6960
+ * `turn/start` 之后的拼接只是取走(`removedCount`,不带内容),条目随后才落成
6961
+ * user/message。因此按 inbox 插入判定:
6962
+ * - 本轮开始之后插入 `next-step` 的用户条目 = 用户中途插话,不算;
6963
+ * - 本轮开始时取走的是上一轮开始之后插入 `next-turn` 的条目;其中最后插入的一条
6964
+ * 来源为 `schedule` 才算(计划任务投递);来源为 `user` 不算;
6965
+ * - 同为 `schedule` 来源的任务变更提示插入的是 `next-step`(宿主注明"不是投递"),不参与判定;
6966
+ * - 扫不到本轮 `turn/start` 时按普通轮次处理,宁可照常提醒。
6967
+ */
6968
+ function scheduledTurnOf(session) {
6969
+ const floor = Math.max(0, session.seq - MAX_TURN_SCAN_EVENTS);
6970
+ let turn;
6971
+ let seenTurnStart = false;
6972
+ for (let seq = session.seq - 1; seq >= floor; seq--) {
6973
+ const event = session.eventAt(seq);
6974
+ if (!event) continue;
6975
+ if (event.type === "turn/start") {
6976
+ if (seenTurnStart) break;
6977
+ seenTurnStart = true;
6978
+ const value = event.data?.turn;
6979
+ if (typeof value === "number") turn = value;
6980
+ continue;
6981
+ }
6982
+ const splice = insertedOf(event);
6983
+ if (splice === void 0) continue;
6984
+ if (!seenTurnStart) {
6985
+ if (splice.target === "next-step" && splice.items.some((item) => sourceKindOf(item) === "user")) return {
6986
+ scheduled: false,
6987
+ ...turn !== void 0 ? { turn } : {}
6988
+ };
6989
+ continue;
6990
+ }
6991
+ if (splice.target !== "next-turn") continue;
6992
+ for (let index = splice.items.length - 1; index >= 0; index--) {
6993
+ const kind = sourceKindOf(splice.items[index]);
6994
+ if (kind === "user") return {
6995
+ scheduled: false,
6996
+ ...turn !== void 0 ? { turn } : {}
6997
+ };
6998
+ if (kind === "schedule") return {
6999
+ scheduled: true,
7000
+ ...turn !== void 0 ? { turn } : {}
7001
+ };
7002
+ }
7003
+ }
7004
+ return {
7005
+ scheduled: false,
7006
+ ...turn !== void 0 ? { turn } : {}
7007
+ };
7008
+ }
7009
+ var QuietCompletionTracker = class {
7010
+ /**
7011
+ * 判定过计划任务轮次的会话。粘性集合:加入后不再移除,工具不按轮出现或消失。
7012
+ * 宿主每次工具列表变化都会往会话追加一条工具增减消息并让提示词缓存失效,
7013
+ * 按轮开关会让同一会话反复产生这类消息。
7014
+ */
7015
+ scheduleSessions = /* @__PURE__ */ new Set();
7016
+ quiet = /* @__PURE__ */ new Map();
7017
+ /** 启动时用宿主任务列表里的投递目标会话预热,避免重启后工具先消失再出现。 */
7018
+ seed(sessionIds) {
7019
+ for (const sessionId of sessionIds) if (sessionId) this.scheduleSessions.add(sessionId);
7020
+ }
7021
+ isScheduleSession(sessionId) {
7022
+ return this.scheduleSessions.has(sessionId);
7023
+ }
7024
+ /** 加入粘性集合;返回是否为新加入(调用方据此记一次日志)。 */
7025
+ markScheduleSession(sessionId) {
7026
+ if (this.scheduleSessions.has(sessionId)) return false;
7027
+ this.scheduleSessions.add(sessionId);
7028
+ return true;
7029
+ }
7030
+ /** 工具执行:只在计划任务轮次内生效。 */
7031
+ requestQuiet(sessionId, verdict, seq) {
7032
+ if (sessionId === void 0 || !verdict.scheduled) return {
7033
+ suppressed: false,
7034
+ note: "This run was not started by a scheduled task; the completion notification will be sent as usual."
7035
+ };
7036
+ this.quiet.set(sessionId, {
7037
+ ...verdict.turn !== void 0 ? { turn: verdict.turn } : {},
7038
+ seq
7039
+ });
7040
+ return { suppressed: true };
7041
+ }
7042
+ /**
7043
+ * 桥接层观察到 user/message:用户在请求静默之后插话(日志位置更晚)就撤销静默,
7044
+ * 本轮照常提醒。计划任务与注入上下文不影响。
7045
+ */
7046
+ noteUserMessage(sessionId, sourceKind, seq) {
7047
+ const request = this.quiet.get(sessionId);
7048
+ if (request === void 0 || sourceKind !== "user") return;
7049
+ if (seq === void 0 || seq > request.seq) this.quiet.delete(sessionId);
7050
+ }
7051
+ /**
7052
+ * turn.end 时调用。返回 true 表示本轮完成通知应被抑制:成功结束、且本轮请求过静默。
7053
+ * 轮次号能对上时按轮次号判定;迟到的上一轮 turn.end 保留本轮的请求。
7054
+ */
7055
+ consumeTurnEnd(sessionId, ok, turn, seq) {
7056
+ const request = this.quiet.get(sessionId);
7057
+ if (request === void 0) return false;
7058
+ if (turn !== void 0 && request.turn !== void 0) {
7059
+ if (turn < request.turn) return false;
7060
+ this.quiet.delete(sessionId);
7061
+ return ok && turn === request.turn;
7062
+ }
7063
+ if (seq !== void 0 && seq < request.seq) return false;
7064
+ this.quiet.delete(sessionId);
7065
+ return ok;
7066
+ }
7067
+ };
7068
+ const DESCRIPTION = [
7069
+ "Call this during a scheduled run only when the task prompt says the user does not need to be notified",
7070
+ "for results like this one. It suppresses only this run's \"task completed\" notification; approvals,",
7071
+ "questions and errors still notify. Do not call it if anything needs the user's attention, or if the",
7072
+ "prompt gives no such rule."
7073
+ ].join(" ");
7074
+ /** reason 归一化:非字符串按空串处理,去首尾空白并截断。 */
7075
+ function normalizeReason(raw) {
7076
+ const text = typeof raw === "string" ? raw.trim() : "";
7077
+ return text.length > 200 ? text.slice(0, 200) : text;
7078
+ }
7079
+ /**
7080
+ * 面向模型的工具定义(宿主 ToolDefinition 原始形状:JSON Schema 参数、输出 schema
7081
+ * 与 render、execute)。`log` 只记会话 ID、结果与 reason 长度:reason 由模型撰写,
7082
+ * 可能引用结果内容,按插件日志约定不打正文。
7083
+ */
7084
+ function quietCompletionTool(tracker, log = () => {}) {
7085
+ return {
7086
+ name: QUIET_COMPLETION_TOOL_NAME,
7087
+ description: DESCRIPTION,
7088
+ parameters: {
7089
+ type: "object",
7090
+ additionalProperties: false,
7091
+ required: ["reason"],
7092
+ properties: { reason: {
7093
+ type: "string",
7094
+ description: `Why this run does not need the user's attention (at most 200 characters).`
7095
+ } }
7096
+ },
7097
+ output: {
7098
+ schema: {
7099
+ type: "object",
7100
+ additionalProperties: false,
7101
+ required: ["suppressed"],
7102
+ properties: {
7103
+ suppressed: { type: "boolean" },
7104
+ note: { type: "string" }
7105
+ }
7106
+ },
7107
+ render(_args, value) {
7108
+ return [{
7109
+ type: "text",
7110
+ text: value.suppressed ? "The completion notification for this run will not be sent." : value.note ?? "The completion notification will be sent as usual."
7111
+ }];
7112
+ }
7113
+ },
7114
+ async execute(args, exec) {
7115
+ const session = sessionLogOf(exec.agent);
7116
+ const reason = normalizeReason(args?.reason);
7117
+ const result = session === void 0 ? tracker.requestQuiet(void 0, { scheduled: false }, 0) : tracker.requestQuiet(session.id, scheduledTurnOf(session), session.seq);
7118
+ log(`quiet completion requested session=${session?.id ?? "?"} suppressed=${result.suppressed} reasonChars=${reason.length}`);
7119
+ return result;
7120
+ }
7121
+ };
7122
+ }
7123
+ /**
7124
+ * 注册工具。`when`:粘性集合内的会话直接放行;否则同步判定本轮是否计划任务轮次,
7125
+ * 是则加入集合(首轮即可见,`new` 目标每次新建的会话同样可见)。普通会话看不到。
7126
+ * 返回宿主给的注销函数。
7127
+ */
7128
+ function registerQuietCompletionTool(tools, tracker, log = () => {}) {
7129
+ return tools.register(quietCompletionTool(tracker, log), { when: (context) => {
7130
+ const session = sessionLogOf(context.agent);
7131
+ if (session === void 0) return false;
7132
+ if (tracker.isScheduleSession(session.id)) return true;
7133
+ if (!scheduledTurnOf(session).scheduled) return false;
7134
+ if (tracker.markScheduleSession(session.id)) log(`quiet completion: session joined scheduled set session=${session.id}`);
7135
+ return true;
7136
+ } });
7137
+ }
7138
+ // src/host-capabilities.ts
6557
7139
  const pluginUpdateProbes = /* @__PURE__ */ new WeakMap();
6558
7140
  function setPluginUpdateProbe(proxy, probe) {
6559
7141
  if (probe) pluginUpdateProbes.set(proxy, probe);
@@ -6569,6 +7151,7 @@ const HOST_CAPABILITY_PROBES = {
6569
7151
  sessionRestore: (proxy) => typeof proxy.workspace?.unarchiveSession === "function",
6570
7152
  projectSelection: (proxy) => typeof proxy.workspace?.list === "function" && typeof proxy.workspace?.create === "function",
6571
7153
  schedules: (proxy) => proxy.schedule !== void 0,
7154
+ schedulesHost: (proxy) => proxy.schedule?.mode === "host",
6572
7155
  promptQueue: (proxy) => typeof proxy.sessions.updateQueue === "function",
6573
7156
  sessionMarkdown: (proxy) => proxy.commands?.markdownAvailable === true && typeof proxy.commands?.export === "function",
6574
7157
  assistantStream: (proxy) => proxy.assistantStreamAvailable === true,
@@ -6582,6 +7165,7 @@ function capabilityBits(proxy, pushOutlet) {
6582
7165
  return {
6583
7166
  historyPaging: true,
6584
7167
  replay: true,
7168
+ binaryFrames: true,
6585
7169
  approvals: true,
6586
7170
  questions: true,
6587
7171
  pendingSnapshot: true,
@@ -6595,6 +7179,7 @@ function capabilityBits(proxy, pushOutlet) {
6595
7179
  sessionRestore: HOST_CAPABILITY_PROBES.sessionRestore(proxy),
6596
7180
  projectSelection: HOST_CAPABILITY_PROBES.projectSelection(proxy),
6597
7181
  schedules: HOST_CAPABILITY_PROBES.schedules(proxy),
7182
+ schedulesHost: HOST_CAPABILITY_PROBES.schedulesHost(proxy),
6598
7183
  promptQueue: HOST_CAPABILITY_PROBES.promptQueue(proxy),
6599
7184
  sessionMarkdown: HOST_CAPABILITY_PROBES.sessionMarkdown(proxy),
6600
7185
  assistantStream: HOST_CAPABILITY_PROBES.assistantStream(proxy),
@@ -6605,8 +7190,7 @@ function capabilityBits(proxy, pushOutlet) {
6605
7190
  deviceRevoke: true
6606
7191
  };
6607
7192
  }
6608
- //#endregion
6609
- //#region src/prompt-queue.ts
7193
+ // src/prompt-queue.ts
6610
7194
  /**
6611
7195
  * 运行中排队与转向(PROTOCOL.md「运行中排队与转向扩展」)的纯函数部分。
6612
7196
  *
@@ -6857,8 +7441,7 @@ var AssistantStream = class {
6857
7441
  this.attempts.clear();
6858
7442
  }
6859
7443
  };
6860
- //#endregion
6861
- //#region src/host-bridge.ts
7444
+ // src/host-bridge.ts
6862
7445
  const MAX_RING_DEFAULT = 2e3;
6863
7446
  /**
6864
7447
  * Process-wide bridge state: session mirror, pending approvals/questions,
@@ -6880,6 +7463,8 @@ var HostBridge = class {
6880
7463
  * and restore them. Excluded from `summaries` and from the live broadcast. */
6881
7464
  archivedSummaries = /* @__PURE__ */ new Map();
6882
7465
  subagentSessionIds = /* @__PURE__ */ new Set();
7466
+ /** 计划任务按需完成通知(quiet-completion.ts):index.ts 用它注册 Agent 工具。 */
7467
+ quietCompletion = new QuietCompletionTracker();
6883
7468
  /**
6884
7469
  * 每个会话最近一次已知的队列(inbox 投影的折叠结果)及其指纹。来源是实时
6885
7470
  * 投影通知或打开会话时读到的基线;指纹用来丢掉内容没变的重复通知。
@@ -7127,6 +7712,35 @@ var HostBridge = class {
7127
7712
  this.runMuxStream();
7128
7713
  this.refreshSummaries();
7129
7714
  }
7715
+ /**
7716
+ * 用宿主任务列表里的投递目标会话预热计划任务会话集合,避免插件重启后工具
7717
+ * 先消失、等下一次投递再出现(每次变化都会给会话追加工具增减消息)。
7718
+ * `new` 目标没有固定会话,只能从最近一次投递回执里取。失败只记日志:集合仍会
7719
+ * 在下一次投递时补上。由 index.ts 在 schedule 服务就绪、注册工具时调用。
7720
+ */
7721
+ async seedQuietCompletionSessions() {
7722
+ if (this.hostSchedule === void 0) return;
7723
+ const sessionIds = [];
7724
+ let offset = 0;
7725
+ for (;;) {
7726
+ const result = await this.listHostSchedules({
7727
+ limit: HOST_SCHEDULE_PAGE_SIZE,
7728
+ offset
7729
+ });
7730
+ if (!result.ok) {
7731
+ this.log?.(`quiet completion seed skipped: ${result.code}`);
7732
+ return;
7733
+ }
7734
+ for (const task of result.value.tasks) {
7735
+ if (task.target.kind !== "new") sessionIds.push(task.target.sessionId);
7736
+ if (task.lastDelivery) sessionIds.push(task.lastDelivery.targetSessionId);
7737
+ }
7738
+ offset += result.value.tasks.length;
7739
+ if (result.value.tasks.length === 0 || offset >= result.value.total) break;
7740
+ }
7741
+ if (this.disposed) return;
7742
+ this.quietCompletion.seed(sessionIds);
7743
+ }
7130
7744
  dispose() {
7131
7745
  if (this.disposed) return;
7132
7746
  this.disposed = true;
@@ -7217,6 +7831,7 @@ var HostBridge = class {
7217
7831
  if (event.type === "turn/end") this.assistantStream.abandon(sessionId);
7218
7832
  if (event.type === "agent-preset/selected") this.record("s2c.skills.changed", { sessionId });
7219
7833
  this.noteActivity(sessionId, event);
7834
+ if (event.type === "user/message") this.quietCompletion.noteUserMessage(sessionId, userMessageSourceKind(event.data), event.seq);
7220
7835
  this.captureAssistantText(sessionId, event);
7221
7836
  const projection = projectEvent(sessionId, event);
7222
7837
  this.captureActivity(sessionId, event, projection?.data.tool);
@@ -7229,7 +7844,12 @@ var HostBridge = class {
7229
7844
  ...streamId && projection.kind === "message.final" ? { streamId } : {}
7230
7845
  }
7231
7846
  });
7232
- if (projection?.kind === "turn.end") this.emitTurnCompletedNotify(sessionId, projection.data.ok === true);
7847
+ if (projection?.kind === "turn.end") {
7848
+ const ok = projection.data.ok === true;
7849
+ const turn = event.data?.turn;
7850
+ if (this.quietCompletion.consumeTurnEnd(sessionId, ok, typeof turn === "number" ? turn : void 0, event.seq)) this.log?.(`completion notice skipped by scheduled run session=${sessionId}`);
7851
+ else this.emitTurnCompletedNotify(sessionId, ok);
7852
+ }
7233
7853
  break;
7234
7854
  }
7235
7855
  case "session/projection": {
@@ -8182,6 +8802,17 @@ var HostBridge = class {
8182
8802
  replayed: result.replayed
8183
8803
  };
8184
8804
  }
8805
+ /**
8806
+ * 当前 adapter 是否宿主级计划任务模型。
8807
+ *
8808
+ * 能力位 `schedulesHost`(host-capabilities.ts)与新帧、旧帧映射全部读这一个
8809
+ * 判别位:探测只发生一次(DshApiProxy.resolveSchedule),这里不再看控制器
8810
+ * 方法,也不看版本号字符串。
8811
+ */
8812
+ get hostSchedule() {
8813
+ const schedule = this.apiProxy.schedule;
8814
+ return schedule?.mode === "host" ? schedule : void 0;
8815
+ }
8185
8816
  async listSchedules(sessionId) {
8186
8817
  const schedule = this.apiProxy.schedule;
8187
8818
  if (schedule === void 0) return {
@@ -8189,6 +8820,7 @@ var HostBridge = class {
8189
8820
  code: "E_UNSUPPORTED",
8190
8821
  message: "schedules unavailable on this host version"
8191
8822
  };
8823
+ if (schedule.mode === "host") return this.listHostSchedulesAsLegacy(schedule, sessionId);
8192
8824
  try {
8193
8825
  const response = await schedule.list({
8194
8826
  rpcId: randomUUID(),
@@ -8212,15 +8844,376 @@ var HostBridge = class {
8212
8844
  };
8213
8845
  }
8214
8846
  }
8215
- async scheduleHistory(sessionId, id, limit, before) {
8216
- const schedule = this.apiProxy.schedule;
8847
+ /** 宿主级列表 -> 旧会话级视图:过滤 session 目标、丢弃 ended、按 scheduledAt 升序。 */
8848
+ async listHostSchedulesAsLegacy(schedule, sessionId) {
8849
+ try {
8850
+ const tasks = [];
8851
+ let offset = 0;
8852
+ for (;;) {
8853
+ const response = await schedule.list({
8854
+ rpcId: randomUUID(),
8855
+ payload: {
8856
+ limit: HOST_SCHEDULE_PAGE_SIZE,
8857
+ offset
8858
+ }
8859
+ });
8860
+ if (!response.result) return {
8861
+ ok: false,
8862
+ code: "E_INTERNAL",
8863
+ message: "schedule list returned no result"
8864
+ };
8865
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8866
+ const page = response.result.value;
8867
+ tasks.push(...page.tasks);
8868
+ offset += page.tasks.length;
8869
+ if (page.tasks.length === 0 || offset >= page.total) break;
8870
+ }
8871
+ const views = [];
8872
+ for (const task of tasks) {
8873
+ if (task.target.kind !== "session" || task.target.sessionId !== sessionId) continue;
8874
+ if (task.status === "ended") continue;
8875
+ const view = toLegacyScheduleView(task);
8876
+ if (view !== void 0) views.push(view);
8877
+ }
8878
+ views.sort((left, right) => left.scheduledAt < right.scheduledAt ? -1 : left.scheduledAt > right.scheduledAt ? 1 : 0);
8879
+ return {
8880
+ ok: true,
8881
+ value: views
8882
+ };
8883
+ } catch (error) {
8884
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
8885
+ }
8886
+ }
8887
+ async listHostSchedules(request = {}) {
8888
+ const schedule = this.hostSchedule;
8217
8889
  if (schedule === void 0) return {
8218
8890
  ok: false,
8219
8891
  code: "E_UNSUPPORTED",
8220
- message: "schedules unavailable on this host version"
8892
+ message: "host-level schedules unavailable on this host version"
8221
8893
  };
8222
8894
  try {
8223
- const response = await schedule.history({
8895
+ const response = await schedule.list({
8896
+ rpcId: randomUUID(),
8897
+ payload: request
8898
+ });
8899
+ if (!response.result) return {
8900
+ ok: false,
8901
+ code: "E_INTERNAL",
8902
+ message: "schedule host list returned no result"
8903
+ };
8904
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8905
+ return {
8906
+ ok: true,
8907
+ value: response.result.value
8908
+ };
8909
+ } catch (error) {
8910
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
8911
+ }
8912
+ }
8913
+ async viewHostSchedule(id) {
8914
+ const schedule = this.hostSchedule;
8915
+ if (schedule === void 0) return {
8916
+ ok: false,
8917
+ code: "E_UNSUPPORTED",
8918
+ message: "host-level schedules unavailable on this host version"
8919
+ };
8920
+ try {
8921
+ const response = await schedule.view({
8922
+ rpcId: randomUUID(),
8923
+ payload: { id }
8924
+ });
8925
+ if (!response.result) return {
8926
+ ok: false,
8927
+ code: "E_INTERNAL",
8928
+ message: "schedule host view returned no result"
8929
+ };
8930
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8931
+ const value = response.result.value;
8932
+ if (value.kind === "not-found") return {
8933
+ ok: false,
8934
+ code: "E_NOT_FOUND",
8935
+ message: "schedule not found"
8936
+ };
8937
+ return {
8938
+ ok: true,
8939
+ value: value.task
8940
+ };
8941
+ } catch (error) {
8942
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
8943
+ }
8944
+ }
8945
+ async createHostSchedule(deviceId, payload) {
8946
+ const schedule = this.hostSchedule;
8947
+ if (schedule === void 0) return {
8948
+ ok: false,
8949
+ code: "E_UNSUPPORTED",
8950
+ message: "host-level schedules unavailable on this host version"
8951
+ };
8952
+ const { clientRequestId, ...request } = payload;
8953
+ const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
8954
+ const response = await schedule.create({
8955
+ rpcId: randomUUID(),
8956
+ payload: {
8957
+ title: request.title,
8958
+ prompt: request.prompt,
8959
+ timing: request.timing,
8960
+ ...request.target !== void 0 ? { target: request.target } : {}
8961
+ }
8962
+ });
8963
+ if (!response.result) return {
8964
+ ok: false,
8965
+ code: "E_INTERNAL",
8966
+ message: "schedule host create returned no result"
8967
+ };
8968
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8969
+ return {
8970
+ ok: true,
8971
+ value: response.result.value
8972
+ };
8973
+ });
8974
+ if (result.ok) {
8975
+ this.record("s2c.schedule.changed", {});
8976
+ return {
8977
+ ok: true,
8978
+ value: result.value,
8979
+ replayed: result.replayed
8980
+ };
8981
+ }
8982
+ return {
8983
+ ok: false,
8984
+ code: result.code,
8985
+ message: result.message ?? result.code,
8986
+ replayed: result.replayed
8987
+ };
8988
+ }
8989
+ async editHostSchedule(deviceId, payload) {
8990
+ const schedule = this.hostSchedule;
8991
+ if (schedule === void 0) return {
8992
+ ok: false,
8993
+ code: "E_UNSUPPORTED",
8994
+ message: "host-level schedules unavailable on this host version"
8995
+ };
8996
+ const { clientRequestId, ...request } = payload;
8997
+ const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
8998
+ const expected = request.expectedUpdatedAt !== void 0 ? { expectedUpdatedAt: request.expectedUpdatedAt } : {};
8999
+ const response = await schedule.edit({
9000
+ rpcId: randomUUID(),
9001
+ payload: request.action === "update" ? {
9002
+ id: request.id,
9003
+ action: "update",
9004
+ patch: request.patch,
9005
+ ...expected
9006
+ } : {
9007
+ id: request.id,
9008
+ action: "delete",
9009
+ ...expected
9010
+ }
9011
+ });
9012
+ if (!response.result) return {
9013
+ ok: false,
9014
+ code: "E_INTERNAL",
9015
+ message: "schedule host edit returned no result"
9016
+ };
9017
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9018
+ const value = response.result.value;
9019
+ if (value.kind === "conflict") return {
9020
+ ok: false,
9021
+ code: "E_BUSY",
9022
+ message: "schedule changed since it was read"
9023
+ };
9024
+ if (value.kind === "not-found") return {
9025
+ ok: false,
9026
+ code: "E_NOT_FOUND",
9027
+ message: "schedule not found"
9028
+ };
9029
+ if (request.action === "delete") {
9030
+ if (value.kind !== "deleted") return {
9031
+ ok: false,
9032
+ code: "E_INTERNAL",
9033
+ message: "schedule delete returned an updated task"
9034
+ };
9035
+ return {
9036
+ ok: true,
9037
+ value: {
9038
+ deleted: true,
9039
+ id: value.id
9040
+ }
9041
+ };
9042
+ }
9043
+ if (value.kind !== "task") return {
9044
+ ok: false,
9045
+ code: "E_INTERNAL",
9046
+ message: "schedule update returned a deletion"
9047
+ };
9048
+ return {
9049
+ ok: true,
9050
+ value: { task: value.task }
9051
+ };
9052
+ });
9053
+ if (result.ok) {
9054
+ this.record("s2c.schedule.changed", {});
9055
+ return {
9056
+ ok: true,
9057
+ value: result.value,
9058
+ replayed: result.replayed
9059
+ };
9060
+ }
9061
+ return {
9062
+ ok: false,
9063
+ code: result.code,
9064
+ message: result.message ?? result.code,
9065
+ replayed: result.replayed
9066
+ };
9067
+ }
9068
+ async runHostSchedule(deviceId, payload) {
9069
+ const schedule = this.hostSchedule;
9070
+ if (schedule === void 0) return {
9071
+ ok: false,
9072
+ code: "E_UNSUPPORTED",
9073
+ message: "host-level schedules unavailable on this host version"
9074
+ };
9075
+ const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, { id: payload.id }, async () => {
9076
+ const response = await schedule.run({
9077
+ rpcId: randomUUID(),
9078
+ payload: { id: payload.id }
9079
+ });
9080
+ if (!response.result) return {
9081
+ ok: false,
9082
+ code: "E_INTERNAL",
9083
+ message: "schedule host run returned no result"
9084
+ };
9085
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9086
+ const value = response.result.value;
9087
+ if (value.kind === "pending") return {
9088
+ ok: false,
9089
+ code: "E_BUSY",
9090
+ message: "schedule execution already pending"
9091
+ };
9092
+ if (value.kind === "not-found") return {
9093
+ ok: false,
9094
+ code: "E_NOT_FOUND",
9095
+ message: "schedule not found"
9096
+ };
9097
+ return {
9098
+ ok: true,
9099
+ value: value.task
9100
+ };
9101
+ });
9102
+ if (result.ok) {
9103
+ this.record("s2c.schedule.changed", {});
9104
+ return {
9105
+ ok: true,
9106
+ value: result.value,
9107
+ replayed: result.replayed
9108
+ };
9109
+ }
9110
+ return {
9111
+ ok: false,
9112
+ code: result.code,
9113
+ message: result.message ?? result.code,
9114
+ replayed: result.replayed
9115
+ };
9116
+ }
9117
+ /**
9118
+ * 删除宿主上全部已结束(`status="ended"`)的任务,返回删除条数。
9119
+ *
9120
+ * 与其他变更帧共用 scheduleMutations journal:同一 clientRequestId 重放时直接回放
9121
+ * 首次结果,不会再删一次。成功后广播 `s2c.schedule.changed`;失败时同样广播——上游
9122
+ * 文档说明失败的批次无法确定哪些行已删除,客户端应重新读取列表。
9123
+ */
9124
+ async deleteEndedHostSchedules(deviceId, payload) {
9125
+ const schedule = this.hostSchedule;
9126
+ if (schedule === void 0) return {
9127
+ ok: false,
9128
+ code: "E_UNSUPPORTED",
9129
+ message: "host-level schedules unavailable on this host version"
9130
+ };
9131
+ const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, { operation: "deleteEnded" }, async () => {
9132
+ const response = await schedule.deleteEnded({
9133
+ rpcId: randomUUID(),
9134
+ payload: {}
9135
+ });
9136
+ if (!response.result) return {
9137
+ ok: false,
9138
+ code: "E_INTERNAL",
9139
+ message: "schedule delete ended returned no result"
9140
+ };
9141
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9142
+ return {
9143
+ ok: true,
9144
+ value: response.result.value
9145
+ };
9146
+ });
9147
+ this.record("s2c.schedule.changed", {});
9148
+ if (result.ok) return {
9149
+ ok: true,
9150
+ value: result.value ?? {},
9151
+ replayed: result.replayed
9152
+ };
9153
+ return {
9154
+ ok: false,
9155
+ code: result.code,
9156
+ message: result.message ?? result.code,
9157
+ replayed: result.replayed
9158
+ };
9159
+ }
9160
+ async scheduleLimits() {
9161
+ const schedule = this.hostSchedule;
9162
+ if (schedule === void 0) return {
9163
+ ok: false,
9164
+ code: "E_UNSUPPORTED",
9165
+ message: "host-level schedules unavailable on this host version"
9166
+ };
9167
+ try {
9168
+ const response = await schedule.limits({
9169
+ rpcId: randomUUID(),
9170
+ payload: {}
9171
+ });
9172
+ if (!response.result) return {
9173
+ ok: false,
9174
+ code: "E_INTERNAL",
9175
+ message: "schedule limits returned no result"
9176
+ };
9177
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9178
+ return {
9179
+ ok: true,
9180
+ value: response.result.value
9181
+ };
9182
+ } catch (error) {
9183
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
9184
+ }
9185
+ }
9186
+ async scheduleHistory(sessionId, id, limit, before) {
9187
+ const schedule = this.apiProxy.schedule;
9188
+ if (schedule === void 0) return {
9189
+ ok: false,
9190
+ code: "E_UNSUPPORTED",
9191
+ message: "schedules unavailable on this host version"
9192
+ };
9193
+ if (schedule.mode === "host") try {
9194
+ const response = await schedule.history({
9195
+ rpcId: randomUUID(),
9196
+ payload: {
9197
+ id,
9198
+ limit,
9199
+ ...before !== void 0 ? { before } : {}
9200
+ }
9201
+ });
9202
+ if (!response.result) return {
9203
+ ok: false,
9204
+ code: "E_INTERNAL",
9205
+ message: "schedule history returned no result"
9206
+ };
9207
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9208
+ return {
9209
+ ok: true,
9210
+ value: response.result.value
9211
+ };
9212
+ } catch (error) {
9213
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
9214
+ }
9215
+ try {
9216
+ const response = await schedule.history({
8224
9217
  rpcId: randomUUID(),
8225
9218
  payload: {
8226
9219
  sessionId,
@@ -8254,6 +9247,68 @@ var HostBridge = class {
8254
9247
  code: "E_UNSUPPORTED",
8255
9248
  message: "schedules unavailable on this host version"
8256
9249
  };
9250
+ if (schedule.mode === "host") {
9251
+ const { clientRequestId, sessionId, title, prompt, ...selectors } = payload;
9252
+ const timing = hostTimingFromLegacySelector(selectors);
9253
+ if (timing === void 0) return {
9254
+ ok: false,
9255
+ code: "E_PROTOCOL",
9256
+ message: "unsupported schedule selector"
9257
+ };
9258
+ const request = {
9259
+ title,
9260
+ prompt,
9261
+ timing,
9262
+ target: {
9263
+ kind: "session",
9264
+ sessionId
9265
+ }
9266
+ };
9267
+ const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
9268
+ const response = await schedule.create({
9269
+ rpcId: randomUUID(),
9270
+ payload: {
9271
+ title,
9272
+ prompt,
9273
+ timing,
9274
+ target: {
9275
+ kind: "session",
9276
+ sessionId
9277
+ }
9278
+ }
9279
+ });
9280
+ if (!response.result) return {
9281
+ ok: false,
9282
+ code: "E_INTERNAL",
9283
+ message: "schedule create returned no result"
9284
+ };
9285
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9286
+ const legacy = toLegacyScheduleView(response.result.value);
9287
+ if (legacy === void 0) return {
9288
+ ok: false,
9289
+ code: "E_PROTOCOL",
9290
+ message: "schedule cannot be represented in the session-scoped view"
9291
+ };
9292
+ return {
9293
+ ok: true,
9294
+ value: legacy
9295
+ };
9296
+ });
9297
+ if (result.ok) {
9298
+ this.record("s2c.schedule.changed", {});
9299
+ return {
9300
+ ok: true,
9301
+ value: result.value,
9302
+ replayed: result.replayed
9303
+ };
9304
+ }
9305
+ return {
9306
+ ok: false,
9307
+ code: result.code,
9308
+ message: result.message ?? result.code,
9309
+ replayed: result.replayed
9310
+ };
9311
+ }
8257
9312
  const { clientRequestId, sessionId, ...request } = payload;
8258
9313
  const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, {
8259
9314
  sessionId,
@@ -8299,6 +9354,106 @@ var HostBridge = class {
8299
9354
  code: "E_UNSUPPORTED",
8300
9355
  message: "schedules unavailable on this host version"
8301
9356
  };
9357
+ if (schedule.mode === "host") {
9358
+ const request = {
9359
+ sessionId: payload.sessionId,
9360
+ id: payload.id,
9361
+ expected: payload.expected,
9362
+ title: payload.title,
9363
+ prompt: payload.prompt,
9364
+ change: payload.change
9365
+ };
9366
+ const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, request, async () => {
9367
+ const current = await schedule.view({
9368
+ rpcId: randomUUID(),
9369
+ payload: { id: payload.id }
9370
+ });
9371
+ if (!current.result) return {
9372
+ ok: false,
9373
+ code: "E_INTERNAL",
9374
+ message: "schedule view returned no result"
9375
+ };
9376
+ if (!current.result.ok) return wireErrorOf("schedule", current.result.error);
9377
+ if (current.result.value.kind === "not-found") return {
9378
+ ok: false,
9379
+ code: "E_NOT_FOUND",
9380
+ message: "schedule not found"
9381
+ };
9382
+ const currentTask = current.result.value.task;
9383
+ if (!legacyScheduleMatchesExpected(currentTask, payload.expected)) return {
9384
+ ok: false,
9385
+ code: "E_BUSY",
9386
+ message: "schedule changed since it was read"
9387
+ };
9388
+ const patch = hostPatchFromLegacyUpdate(payload);
9389
+ if (Object.keys(patch).length === 0) {
9390
+ const legacy = toLegacyScheduleView(currentTask);
9391
+ if (legacy === void 0) return {
9392
+ ok: false,
9393
+ code: "E_PROTOCOL",
9394
+ message: "schedule cannot be represented in the session-scoped view"
9395
+ };
9396
+ return {
9397
+ ok: true,
9398
+ value: legacy
9399
+ };
9400
+ }
9401
+ const response = await schedule.edit({
9402
+ rpcId: randomUUID(),
9403
+ payload: {
9404
+ id: payload.id,
9405
+ action: "update",
9406
+ patch
9407
+ }
9408
+ });
9409
+ if (!response.result) return {
9410
+ ok: false,
9411
+ code: "E_INTERNAL",
9412
+ message: "schedule edit returned no result"
9413
+ };
9414
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9415
+ const value = response.result.value;
9416
+ if (value.kind === "conflict") return {
9417
+ ok: false,
9418
+ code: "E_BUSY",
9419
+ message: "schedule changed since it was read"
9420
+ };
9421
+ if (value.kind === "not-found") return {
9422
+ ok: false,
9423
+ code: "E_NOT_FOUND",
9424
+ message: "schedule not found"
9425
+ };
9426
+ if (value.kind !== "task") return {
9427
+ ok: false,
9428
+ code: "E_INTERNAL",
9429
+ message: "schedule edit returned an unexpected deletion"
9430
+ };
9431
+ const legacy = toLegacyScheduleView(value.task);
9432
+ if (legacy === void 0) return {
9433
+ ok: false,
9434
+ code: "E_PROTOCOL",
9435
+ message: "schedule cannot be represented in the session-scoped view"
9436
+ };
9437
+ return {
9438
+ ok: true,
9439
+ value: legacy
9440
+ };
9441
+ });
9442
+ if (result.ok) {
9443
+ this.record("s2c.schedule.changed", {});
9444
+ return {
9445
+ ok: true,
9446
+ value: result.value,
9447
+ replayed: result.replayed
9448
+ };
9449
+ }
9450
+ return {
9451
+ ok: false,
9452
+ code: result.code,
9453
+ message: result.message ?? result.code,
9454
+ replayed: result.replayed
9455
+ };
9456
+ }
8302
9457
  const { clientRequestId, ...request } = payload;
8303
9458
  const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
8304
9459
  const response = await schedule.update({
@@ -8344,6 +9499,63 @@ var HostBridge = class {
8344
9499
  code: "E_UNSUPPORTED",
8345
9500
  message: "schedules unavailable on this host version"
8346
9501
  };
9502
+ if (schedule.mode === "host") {
9503
+ const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
9504
+ sessionId: payload.sessionId,
9505
+ id: payload.id
9506
+ }, async () => {
9507
+ const response = await schedule.edit({
9508
+ rpcId: randomUUID(),
9509
+ payload: {
9510
+ id: payload.id,
9511
+ action: "delete"
9512
+ }
9513
+ });
9514
+ if (!response.result) return {
9515
+ ok: false,
9516
+ code: "E_INTERNAL",
9517
+ message: "schedule delete returned no result"
9518
+ };
9519
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9520
+ const value = response.result.value;
9521
+ if (value.kind === "deleted") return {
9522
+ ok: true,
9523
+ value: {
9524
+ id: value.id,
9525
+ deleted: true
9526
+ }
9527
+ };
9528
+ if (value.kind === "not-found") return {
9529
+ ok: false,
9530
+ code: "E_NOT_FOUND",
9531
+ message: "schedule not found"
9532
+ };
9533
+ if (value.kind === "conflict") return {
9534
+ ok: false,
9535
+ code: "E_BUSY",
9536
+ message: "schedule changed since it was read"
9537
+ };
9538
+ return {
9539
+ ok: false,
9540
+ code: "E_INTERNAL",
9541
+ message: "schedule delete returned an updated task"
9542
+ };
9543
+ });
9544
+ if (result.ok) {
9545
+ this.record("s2c.schedule.changed", {});
9546
+ return {
9547
+ ok: true,
9548
+ value: result.value,
9549
+ replayed: result.replayed
9550
+ };
9551
+ }
9552
+ return {
9553
+ ok: false,
9554
+ code: result.code,
9555
+ message: result.message ?? result.code,
9556
+ replayed: result.replayed
9557
+ };
9558
+ }
8347
9559
  const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
8348
9560
  sessionId: payload.sessionId,
8349
9561
  id: payload.id
@@ -8728,8 +9940,236 @@ function projectSessionModels(value) {
8728
9940
  };
8729
9941
  }
8730
9942
  /** Project a history page (raw events) into MessageProjection rows. */
8731
- //#endregion
8732
- //#region src/dsh-remote-interactions.ts
9943
+ /** 宿主级列表全量拉取时的页大小(Host 允许的最大值)。 */
9944
+ const HOST_SCHEDULE_PAGE_SIZE = 500;
9945
+ /** 参与旧帧冲突比较的字段:`state`/`deliveryMode` 是派生字段,不参与。 */
9946
+ const LEGACY_PROJECTION_FIELDS = [
9947
+ "id",
9948
+ "kind",
9949
+ "title",
9950
+ "prompt",
9951
+ "scheduledAt",
9952
+ "afterSeconds",
9953
+ "everySeconds",
9954
+ "time",
9955
+ "timeZone",
9956
+ "weekdays",
9957
+ "expression"
9958
+ ];
9959
+ function isPlainObject$1(value) {
9960
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9961
+ }
9962
+ function legacyNumber(...values) {
9963
+ for (const value of values) if (typeof value === "number" && Number.isFinite(value)) return value;
9964
+ }
9965
+ function legacyString(...values) {
9966
+ for (const value of values) if (typeof value === "string" && value.length > 0) return value;
9967
+ }
9968
+ function normalizeAtInput(value) {
9969
+ if (typeof value === "string" && value.length > 0) return value;
9970
+ if (!isPlainObject$1(value)) return void 0;
9971
+ const date = legacyString(value.date);
9972
+ const time = legacyString(value.time);
9973
+ const zone = legacyString(value.time_zone, value.timeZone);
9974
+ return date !== void 0 && time !== void 0 && zone !== void 0 ? {
9975
+ date,
9976
+ time,
9977
+ time_zone: zone
9978
+ } : void 0;
9979
+ }
9980
+ function normalizeLocalTime(value) {
9981
+ if (!isPlainObject$1(value)) return void 0;
9982
+ const time = legacyString(value.time);
9983
+ const zone = legacyString(value.time_zone, value.timeZone);
9984
+ return time !== void 0 && zone !== void 0 ? {
9985
+ time,
9986
+ time_zone: zone
9987
+ } : void 0;
9988
+ }
9989
+ function normalizeWeekdays(value) {
9990
+ if (!Array.isArray(value) || value.length === 0) return void 0;
9991
+ const days = value.filter((day) => typeof day === "number" && Number.isSafeInteger(day) && day >= 1 && day <= 7);
9992
+ return days.length === value.length ? days : void 0;
9993
+ }
9994
+ /** 旧创建帧的选择器 -> 宿主级 timing。旧 wire 校验保证恰好一个选择器。 */
9995
+ function hostTimingFromLegacySelector(selectors) {
9996
+ const after = legacyNumber(selectors.after_seconds);
9997
+ if (after !== void 0) return {
9998
+ kind: "after",
9999
+ after_seconds: after
10000
+ };
10001
+ if (selectors.at !== void 0) {
10002
+ const at = normalizeAtInput(selectors.at);
10003
+ return at === void 0 ? void 0 : {
10004
+ kind: "at",
10005
+ at
10006
+ };
10007
+ }
10008
+ const every = legacyNumber(selectors.every_seconds);
10009
+ if (every !== void 0) return {
10010
+ kind: "every",
10011
+ every_seconds: every
10012
+ };
10013
+ if (selectors.daily !== void 0) {
10014
+ const daily = normalizeLocalTime(selectors.daily);
10015
+ return daily === void 0 ? void 0 : {
10016
+ kind: "daily",
10017
+ daily
10018
+ };
10019
+ }
10020
+ if (selectors.weekly !== void 0) {
10021
+ if (!isPlainObject$1(selectors.weekly)) return void 0;
10022
+ const time = normalizeLocalTime(selectors.weekly);
10023
+ const weekdays = normalizeWeekdays(selectors.weekly.weekdays);
10024
+ return time === void 0 || weekdays === void 0 ? void 0 : {
10025
+ kind: "weekly",
10026
+ weekly: {
10027
+ ...time,
10028
+ weekdays
10029
+ }
10030
+ };
10031
+ }
10032
+ if (selectors.cron !== void 0) {
10033
+ if (!isPlainObject$1(selectors.cron)) return void 0;
10034
+ const expression = legacyString(selectors.cron.expression);
10035
+ const zone = legacyString(selectors.cron.time_zone, selectors.cron.timeZone);
10036
+ return expression === void 0 || zone === void 0 ? void 0 : {
10037
+ kind: "cron",
10038
+ cron: {
10039
+ expression,
10040
+ time_zone: zone
10041
+ }
10042
+ };
10043
+ }
10044
+ }
10045
+ /**
10046
+ * 旧更新帧的 `change` -> 宿主级 timing。
10047
+ *
10048
+ * 两种历史写法都认:选择器式(`{kind:'daily', daily:{...}}`,与创建帧同形)与
10049
+ * 扁平式(`{kind:'daily', time, timeZone}` / `{kind:'cron', expression}`)。
10050
+ */
10051
+ function hostTimingFromLegacyChange(change) {
10052
+ if (!isPlainObject$1(change)) return void 0;
10053
+ const selector = hostTimingFromLegacySelector(change);
10054
+ if (selector !== void 0) return selector;
10055
+ switch (change.kind) {
10056
+ case "daily": {
10057
+ const time = legacyString(change.time);
10058
+ const zone = legacyString(change.time_zone, change.timeZone);
10059
+ return time === void 0 || zone === void 0 ? void 0 : {
10060
+ kind: "daily",
10061
+ daily: {
10062
+ time,
10063
+ time_zone: zone
10064
+ }
10065
+ };
10066
+ }
10067
+ case "weekly": {
10068
+ const time = legacyString(change.time);
10069
+ const zone = legacyString(change.time_zone, change.timeZone);
10070
+ const weekdays = normalizeWeekdays(change.weekdays);
10071
+ return time === void 0 || zone === void 0 || weekdays === void 0 ? void 0 : {
10072
+ kind: "weekly",
10073
+ weekly: {
10074
+ time,
10075
+ time_zone: zone,
10076
+ weekdays
10077
+ }
10078
+ };
10079
+ }
10080
+ case "cron": {
10081
+ const expression = legacyString(change.expression);
10082
+ const zone = legacyString(change.time_zone, change.timeZone);
10083
+ return expression === void 0 || zone === void 0 ? void 0 : {
10084
+ kind: "cron",
10085
+ cron: {
10086
+ expression,
10087
+ time_zone: zone
10088
+ }
10089
+ };
10090
+ }
10091
+ case "rrule": {
10092
+ const rrule = legacyString(change.rrule);
10093
+ return rrule === void 0 ? void 0 : {
10094
+ kind: "rrule",
10095
+ rrule
10096
+ };
10097
+ }
10098
+ default: return;
10099
+ }
10100
+ }
10101
+ /** 旧更新帧的 title/prompt/change -> 宿主级 patch。 */
10102
+ function hostPatchFromLegacyUpdate(payload) {
10103
+ const patch = {};
10104
+ if (typeof payload.title === "string") patch.title = payload.title;
10105
+ if (typeof payload.prompt === "string") patch.prompt = payload.prompt;
10106
+ const timing = hostTimingFromLegacyChange(payload.change);
10107
+ if (timing !== void 0) patch.timing = timing;
10108
+ return patch;
10109
+ }
10110
+ /**
10111
+ * 宿主级任务视图 -> 旧会话级视图。
10112
+ *
10113
+ * `state` 只由 `scheduledAt` 是否已过推导(PROTOCOL.md 映射表);`rrule` 在旧枚举
10114
+ * 里没有对应 kind,返回 undefined 让调用方跳过——旧客户端按枚举解码列表,塞进
10115
+ * 未知 kind 会让整页解码失败,静默隐藏比让旧 App 打不开列表更安全。
10116
+ */
10117
+ function toLegacyScheduleView(task) {
10118
+ const instant = Date.parse(task.scheduledAt);
10119
+ const base = {
10120
+ id: task.id,
10121
+ title: task.title,
10122
+ prompt: task.prompt,
10123
+ scheduledAt: task.scheduledAt,
10124
+ state: Number.isFinite(instant) && instant < Date.now() ? "overdue" : "scheduled",
10125
+ deliveryMode: "host"
10126
+ };
10127
+ switch (task.timing.kind) {
10128
+ case "after": return {
10129
+ ...base,
10130
+ kind: "after",
10131
+ afterSeconds: task.timing.after_seconds
10132
+ };
10133
+ case "at": return {
10134
+ ...base,
10135
+ kind: "at"
10136
+ };
10137
+ case "every": return {
10138
+ ...base,
10139
+ kind: "every",
10140
+ everySeconds: task.timing.every_seconds
10141
+ };
10142
+ case "daily": return {
10143
+ ...base,
10144
+ kind: "daily",
10145
+ time: task.timing.daily.time,
10146
+ timeZone: task.timing.daily.time_zone
10147
+ };
10148
+ case "weekly": return {
10149
+ ...base,
10150
+ kind: "weekly",
10151
+ time: task.timing.weekly.time,
10152
+ timeZone: task.timing.weekly.time_zone,
10153
+ weekdays: [...task.timing.weekly.weekdays]
10154
+ };
10155
+ case "cron": return {
10156
+ ...base,
10157
+ kind: "cron",
10158
+ expression: task.timing.cron.expression,
10159
+ timeZone: task.timing.cron.time_zone
10160
+ };
10161
+ default: return;
10162
+ }
10163
+ }
10164
+ /** 旧帧的冲突保护:当前任务的旧投影与客户端回传的 `expected` 逐字段相等。 */
10165
+ function legacyScheduleMatchesExpected(task, expected) {
10166
+ const view = toLegacyScheduleView(task);
10167
+ if (view === void 0) return false;
10168
+ if (!isPlainObject$1(expected)) return false;
10169
+ const actual = view;
10170
+ return LEGACY_PROJECTION_FIELDS.every((key) => JSON.stringify(actual[key]) === JSON.stringify(expected[key]));
10171
+ }
10172
+ // src/dsh-remote-interactions.ts
8733
10173
  /**
8734
10174
  * Resident DSH Remote Events client for phone interactions.
8735
10175
  *
@@ -8917,8 +10357,7 @@ async function startDshRemoteInteractions(hostCtx, handlers) {
8917
10357
  throw error;
8918
10358
  }
8919
10359
  }
8920
- //#endregion
8921
- //#region src/command-api.ts
10360
+ // src/command-api.ts
8922
10361
  /**
8923
10362
  * 给本域自造的失败带上 Host 风格的码。
8924
10363
  *
@@ -9065,8 +10504,7 @@ var DshCommandApi = class {
9065
10504
  return response;
9066
10505
  }
9067
10506
  };
9068
- //#endregion
9069
- //#region src/dsh-api-proxy.ts
10507
+ // src/dsh-api-proxy.ts
9070
10508
  /**
9071
10509
  * Adapter for the DSH rc.2 controller API.
9072
10510
  *
@@ -9075,6 +10513,14 @@ var DshCommandApi = class {
9075
10513
  * Session/Workspace controllers and Gateway-backed Remote Events, keeping
9076
10514
  * the phone protocol isolated from the Host API.
9077
10515
  */
10516
+ /**
10517
+ * 形状探测:宿主级模型只有 `edit`(没有 `update`)。不按版本号字符串判断——
10518
+ * 版本号无法说明 Schedule bundle 是否挂载、也无法覆盖回退组合。
10519
+ */
10520
+ function isHostScheduleController(controller) {
10521
+ const value = controller;
10522
+ return typeof value.edit === "function" && typeof value.update !== "function";
10523
+ }
9078
10524
  /** Direct-controller adapter with the shape HostBridge consumes. */
9079
10525
  var DshApiProxy = class {
9080
10526
  ctx;
@@ -9145,14 +10591,16 @@ var DshApiProxy = class {
9145
10591
  const controller = this.ctx.get("schedule");
9146
10592
  if (controller === void 0) return;
9147
10593
  this.scheduleController = controller;
9148
- this.scheduleApi = this.createScheduleApi(controller);
10594
+ this.scheduleApi = isHostScheduleController(controller) ? this.createHostScheduleApi(controller) : this.createLegacyScheduleApi(controller);
9149
10595
  }
9150
10596
  get schedule() {
9151
10597
  this.resolveSchedule();
9152
10598
  return this.scheduleApi;
9153
10599
  }
9154
- createScheduleApi(controller) {
10600
+ /** 会话级适配器:行为与宿主级扩展加入前逐字节一致,只多一个判别位。 */
10601
+ createLegacyScheduleApi(controller) {
9155
10602
  return {
10603
+ mode: "legacy",
9156
10604
  list: async (request) => this.call(async () => ({
9157
10605
  sessionId: request.payload.sessionId,
9158
10606
  tasks: (await controller.list({ sessionId: request.payload.sessionId })).map(toScheduleTask)
@@ -9201,6 +10649,83 @@ var DshApiProxy = class {
9201
10649
  };
9202
10650
  }
9203
10651
  /**
10652
+ * 宿主级适配器:把 DSH `ScheduleService` 的分页列表/结果对象归一化成 Bridge
10653
+ * 消费的稳定视图。
10654
+ *
10655
+ * 失败分两类:`view/edit/run/history` 的「找不到」是**结果对象**里的 code
10656
+ * (非变更 miss,不抛错),这里翻译成 outcome 或带 code 的 Error;
10657
+ * `create/edit/run` 的输入非法与目标已归档是 RemoteError 抛出,由
10658
+ * `this.call` 收成 `{code}`,再由 Bridge 按错误词表映射。
10659
+ */
10660
+ createHostScheduleApi(controller) {
10661
+ return {
10662
+ mode: "host",
10663
+ list: async (request) => this.call(async () => {
10664
+ const payload = request.payload ?? {};
10665
+ const value = await controller.list({
10666
+ ...payload.active !== void 0 ? { active: payload.active } : {},
10667
+ ...payload.limit !== void 0 ? { limit: payload.limit } : {},
10668
+ ...payload.offset !== void 0 ? { offset: payload.offset } : {}
10669
+ });
10670
+ const items = Array.isArray(value.items) ? value.items : [];
10671
+ return {
10672
+ tasks: items.map(toHostScheduleTask),
10673
+ shown: finiteCount(value.shown, items.length),
10674
+ total: finiteCount(value.total, items.length),
10675
+ limit: finiteCount(value.limit, payload.limit ?? 100),
10676
+ offset: finiteCount(value.offset, payload.offset ?? 0)
10677
+ };
10678
+ }),
10679
+ view: async (request) => this.call(async () => {
10680
+ return toHostScheduleViewOutcome(await controller.view({ id: request.payload.id }), request.payload.id);
10681
+ }),
10682
+ create: async (request) => this.call(async () => {
10683
+ const payload = request.payload;
10684
+ return toHostScheduleTask(await controller.create({
10685
+ title: payload.title,
10686
+ prompt: payload.prompt,
10687
+ timing: payload.timing,
10688
+ ...payload.target !== void 0 ? { target: normalizeTargetRequest(payload.target) } : {}
10689
+ }));
10690
+ }),
10691
+ edit: async (request) => this.call(async () => {
10692
+ const payload = request.payload;
10693
+ const expected = payload.expectedUpdatedAt !== void 0 ? { expectedUpdatedAt: payload.expectedUpdatedAt } : {};
10694
+ const edit = payload.action === "update" ? {
10695
+ id: payload.id,
10696
+ action: "update",
10697
+ patch: normalizeHostPatch(payload.patch),
10698
+ ...expected
10699
+ } : {
10700
+ id: payload.id,
10701
+ action: "delete",
10702
+ ...expected
10703
+ };
10704
+ return toHostScheduleEditOutcome(await controller.edit(edit), payload.id);
10705
+ }),
10706
+ run: async (request) => this.call(async () => {
10707
+ return toHostScheduleRunOutcome(await controller.run({ id: request.payload.id }), request.payload.id);
10708
+ }),
10709
+ history: async (request) => this.call(async () => {
10710
+ const value = await controller.history({
10711
+ id: request.payload.id,
10712
+ limit: request.payload.limit,
10713
+ ...request.payload.before !== void 0 ? { before: request.payload.before } : {}
10714
+ });
10715
+ if (typeof value.code === "string") throw Object.assign(new Error(String(value.code)), { code: value.code });
10716
+ return toScheduleHistory(value);
10717
+ }),
10718
+ limits: async () => this.call(async () => {
10719
+ const value = controller.deliveryLimits();
10720
+ return { newSessionIntervalSeconds: Math.max(0, Math.trunc(Number(value?.newSessionIntervalSeconds) || 0)) };
10721
+ }),
10722
+ deleteEnded: async () => this.call(async () => {
10723
+ const value = await controller.deleteInactive();
10724
+ return { deleted: Math.max(0, Math.trunc(Number(value?.deleted) || 0)) };
10725
+ })
10726
+ };
10727
+ }
10728
+ /**
9204
10729
  * Decide whether this Gateway Client can surface a phone card. The decision
9205
10730
  * fails open to false so a broken device registry delegates this delivery
9206
10731
  * while the official Web Client remains able to answer.
@@ -9613,6 +11138,206 @@ function toScheduleHistory(value) {
9613
11138
  ...typeof row.nextBefore === "string" ? { nextBefore: row.nextBefore } : {}
9614
11139
  };
9615
11140
  }
11141
+ /**
11142
+ * 请求侧 target:`new_once`/`new` 的会话由 Host 预留或新建,客户端若把查看结果
11143
+ * 里的 `sessionId` 原样回传,不向上游透传(Host 的创建/编辑请求没有这个字段)。
11144
+ */
11145
+ function normalizeTargetRequest(value) {
11146
+ if (value.kind === "session") return {
11147
+ kind: "session",
11148
+ sessionId: String(value.sessionId ?? "")
11149
+ };
11150
+ const create = toHostScheduleCreateSpec(value.create);
11151
+ return {
11152
+ kind: value.kind,
11153
+ ...create !== void 0 ? { create } : {}
11154
+ };
11155
+ }
11156
+ /** 编辑补丁归一化:字段按契约逐个透传,target 走请求侧投影。 */
11157
+ function normalizeHostPatch(value) {
11158
+ return {
11159
+ ...value.title !== void 0 ? { title: value.title } : {},
11160
+ ...value.prompt !== void 0 ? { prompt: value.prompt } : {},
11161
+ ...value.timing !== void 0 ? { timing: value.timing } : {},
11162
+ ...value.target !== void 0 ? { target: normalizeTargetRequest(value.target) } : {},
11163
+ ...value.active !== void 0 ? { active: value.active } : {}
11164
+ };
11165
+ }
11166
+ function isRecord(value) {
11167
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11168
+ }
11169
+ function toRecord(value) {
11170
+ return isRecord(value) ? value : {};
11171
+ }
11172
+ /** 非负安全整数;形状不符时退回 fallback(分页字段只用于展示)。 */
11173
+ function finiteCount(value, fallback) {
11174
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
11175
+ }
11176
+ function positiveSeconds(value) {
11177
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
11178
+ }
11179
+ function hostResultCode(value) {
11180
+ return typeof value.code === "string" ? value.code : void 0;
11181
+ }
11182
+ /** 结果对象里的「非变更 miss」不是抛错,这里翻译成 outcome 或带 code 的 Error。 */
11183
+ function throwHostResultCode(code) {
11184
+ throw Object.assign(new Error(code), { code });
11185
+ }
11186
+ function toHostScheduleCreateSpec(value) {
11187
+ const row = toRecord(value);
11188
+ const spec = {
11189
+ ...typeof row.cwd === "string" ? { cwd: row.cwd } : {},
11190
+ ...typeof row.agentPreset === "string" ? { agentPreset: row.agentPreset } : {}
11191
+ };
11192
+ return Object.keys(spec).length > 0 ? spec : void 0;
11193
+ }
11194
+ /**
11195
+ * 交付目标投影。`session` 与 `new_once` 带 sessionId(后者是创建时 Host 预留
11196
+ * 的会话);`new` 每次投递新建会话。`create` 省略时按 Host 默认。
11197
+ */
11198
+ function toHostScheduleTarget(value) {
11199
+ const target = toRecord(value);
11200
+ const create = toHostScheduleCreateSpec(target.create);
11201
+ if (target.kind === "session") return {
11202
+ kind: "session",
11203
+ sessionId: String(target.sessionId ?? "")
11204
+ };
11205
+ if (target.kind === "new_once") return {
11206
+ kind: "new_once",
11207
+ sessionId: String(target.sessionId ?? ""),
11208
+ ...create !== void 0 ? { create } : {}
11209
+ };
11210
+ return {
11211
+ kind: "new",
11212
+ ...create !== void 0 ? { create } : {}
11213
+ };
11214
+ }
11215
+ function toHostScheduleReceipt(value) {
11216
+ const row = toRecord(value);
11217
+ return {
11218
+ scheduledAt: String(row.scheduledAt ?? ""),
11219
+ deliveredAt: String(row.deliveredAt ?? ""),
11220
+ messageId: String(row.messageId ?? ""),
11221
+ targetSessionId: String(row.targetSessionId ?? "")
11222
+ };
11223
+ }
11224
+ /**
11225
+ * timing 反向投影:DSH 存储记录用 camelCase(`afterSeconds`/`timeZone`),wire
11226
+ * 用 PROTOCOL.md 的 snake_case 嵌套结构。
11227
+ *
11228
+ * `at` 存储后只剩 `scheduledAt`(原始绝对时刻不是独立字段),因此用排定时刻
11229
+ * 回填——对 active 的一次性任务它就是目标时刻。
11230
+ */
11231
+ function toHostScheduleTiming(row) {
11232
+ switch (row.kind) {
11233
+ case "after": return {
11234
+ kind: "after",
11235
+ after_seconds: positiveSeconds(row.afterSeconds)
11236
+ };
11237
+ case "every": return {
11238
+ kind: "every",
11239
+ every_seconds: positiveSeconds(row.everySeconds)
11240
+ };
11241
+ case "daily": return {
11242
+ kind: "daily",
11243
+ daily: localTimeInput(row)
11244
+ };
11245
+ case "weekly": return {
11246
+ kind: "weekly",
11247
+ weekly: {
11248
+ ...localTimeInput(row),
11249
+ weekdays: Array.isArray(row.weekdays) ? row.weekdays.filter((day) => typeof day === "number") : []
11250
+ }
11251
+ };
11252
+ case "cron": return {
11253
+ kind: "cron",
11254
+ cron: {
11255
+ expression: String(row.expression ?? ""),
11256
+ time_zone: String(row.timeZone ?? "")
11257
+ }
11258
+ };
11259
+ case "rrule": return {
11260
+ kind: "rrule",
11261
+ rrule: String(row.rrule ?? "")
11262
+ };
11263
+ default: return {
11264
+ kind: "at",
11265
+ at: String(row.scheduledAt ?? "")
11266
+ };
11267
+ }
11268
+ }
11269
+ function localTimeInput(row) {
11270
+ return {
11271
+ time: String(row.time ?? ""),
11272
+ time_zone: String(row.timeZone ?? "")
11273
+ };
11274
+ }
11275
+ /** Host `ScheduleCatalogEntry` -> wire `schedule.host.task`(丢弃 involvedSessionIds)。 */
11276
+ function toHostScheduleTask(value) {
11277
+ const row = toRecord(value);
11278
+ return {
11279
+ id: String(row.id ?? ""),
11280
+ title: String(row.title ?? ""),
11281
+ prompt: String(row.prompt ?? ""),
11282
+ timing: toHostScheduleTiming(row),
11283
+ target: toHostScheduleTarget(row.target),
11284
+ status: row.status === "paused" || row.status === "ended" ? row.status : "active",
11285
+ scheduledAt: String(row.scheduledAt ?? ""),
11286
+ ...typeof row.createdAt === "string" ? { createdAt: row.createdAt } : {},
11287
+ ...typeof row.updatedAt === "string" ? { updatedAt: row.updatedAt } : {},
11288
+ ...typeof row.executionActive === "boolean" ? { executionActive: row.executionActive } : {},
11289
+ ...typeof row.deliveryFloorEndsAt === "string" ? { deliveryFloorEndsAt: row.deliveryFloorEndsAt } : {},
11290
+ ...isRecord(row.lastDelivery) ? { lastDelivery: toHostScheduleReceipt(row.lastDelivery) } : {}
11291
+ };
11292
+ }
11293
+ function toHostScheduleViewOutcome(value, id) {
11294
+ const code = hostResultCode(value);
11295
+ if (code === "schedule_not_found") return {
11296
+ kind: "not-found",
11297
+ id
11298
+ };
11299
+ if (code !== void 0) throwHostResultCode(code);
11300
+ return {
11301
+ kind: "task",
11302
+ task: toHostScheduleTask(value)
11303
+ };
11304
+ }
11305
+ function toHostScheduleEditOutcome(value, id) {
11306
+ const code = hostResultCode(value);
11307
+ if (code === "schedule_conflict") return {
11308
+ kind: "conflict",
11309
+ id
11310
+ };
11311
+ if (code === "schedule_not_found") return {
11312
+ kind: "not-found",
11313
+ id
11314
+ };
11315
+ if (code !== void 0) throwHostResultCode(code);
11316
+ if (value.deleted === true) return {
11317
+ kind: "deleted",
11318
+ id: String(value.id ?? id)
11319
+ };
11320
+ return {
11321
+ kind: "task",
11322
+ task: toHostScheduleTask(value)
11323
+ };
11324
+ }
11325
+ function toHostScheduleRunOutcome(value, id) {
11326
+ const code = hostResultCode(value);
11327
+ if (code === "schedule_execution_pending") return {
11328
+ kind: "pending",
11329
+ id
11330
+ };
11331
+ if (code === "schedule_not_found") return {
11332
+ kind: "not-found",
11333
+ id
11334
+ };
11335
+ if (code !== void 0) throwHostResultCode(code);
11336
+ return {
11337
+ kind: "task",
11338
+ task: toHostScheduleTask(value)
11339
+ };
11340
+ }
9616
11341
  async function readWorkspaceBaseline(controller) {
9617
11342
  const abort = new AbortController();
9618
11343
  const iterator = controller.follow(abort.signal)[Symbol.asyncIterator]();
@@ -9642,8 +11367,7 @@ async function projectModels(catalog, sessionId, list) {
9642
11367
  failures: Array.isArray(value.failures) ? value.failures : []
9643
11368
  };
9644
11369
  }
9645
- //#endregion
9646
- //#region src/report-service.ts
11370
+ // src/report-service.ts
9647
11371
  /**
9648
11372
  * The Typert receiver the Gateway resolves for the DeepPilot Bridge report.
9649
11373
  * Snapshot data stays non-secret; the token crosses the boundary only through
@@ -9689,8 +11413,7 @@ var DeepPilotReportService = class extends TypertRemoteService {
9689
11413
  return this.pushTester();
9690
11414
  }
9691
11415
  };
9692
- //#endregion
9693
- //#region src/report-wire.ts
11416
+ // src/report-wire.ts
9694
11417
  /** The npm package identity both contribution registrations claim. */
9695
11418
  const REPORT_REMOTE_PACKAGE = "dsh-deeppilot";
9696
11419
  /** Canonical `<namespace>/<method>` endpoint of the report Remote. */
@@ -10075,8 +11798,7 @@ const REPORT_HOST_CONTRIBUTION = {
10075
11798
  }
10076
11799
  ]
10077
11800
  };
10078
- //#endregion
10079
- //#region src/report-remote.ts
11801
+ // src/report-remote.ts
10080
11802
  /**
10081
11803
  * Provide the report service and register its Remote descriptor. Rides an
10082
11804
  * optional `typert` inject: profiles without the web stack never activate it.
@@ -10088,8 +11810,7 @@ function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceR
10088
11810
  remoteCtx.effect(() => () => void unregister(), "dsh-deeppilot: report remote");
10089
11811
  });
10090
11812
  }
10091
- //#endregion
10092
- //#region src/local-address.ts
11813
+ // src/local-address.ts
10093
11814
  function octetsOf(address) {
10094
11815
  const octets = address.split(".").map(Number);
10095
11816
  if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return null;
@@ -10140,8 +11861,7 @@ function localLANIPv4Addresses() {
10140
11861
  candidates.sort((left, right) => priority(left.name) - priority(right.name) || left.name.localeCompare(right.name));
10141
11862
  return [...new Set(candidates.map(({ address }) => address))];
10142
11863
  }
10143
- //#endregion
10144
- //#region src/update-check.ts
11864
+ // src/update-check.ts
10145
11865
  /**
10146
11866
  * Lightweight self-update check for dsh-deeppilot.
10147
11867
  *
@@ -10337,8 +12057,7 @@ var UpdateChecker = class {
10337
12057
  /** No-op kept for API symmetry with the host lifecycle wiring. */
10338
12058
  dispose() {}
10339
12059
  };
10340
- //#endregion
10341
- //#region src/phone-http.ts
12060
+ // src/phone-http.ts
10342
12061
  const CLIENT_IP_HEADER = "x-deeppilot-client-ip";
10343
12062
  function rejectUpgrade(socket, status, reason, retryAfterSeconds) {
10344
12063
  const body = JSON.stringify({ error: reason });
@@ -10372,8 +12091,7 @@ function requestClientIdentity(req) {
10372
12091
  }
10373
12092
  return normalizedAddress(req.socket.remoteAddress) ?? "unknown";
10374
12093
  }
10375
- //#endregion
10376
- //#region src/lan-tls.ts
12094
+ // src/lan-tls.ts
10377
12095
  const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
10378
12096
  const LAN_TLS_KEY_FILE = "key.pem";
10379
12097
  const LAN_TLS_CERT_FILE = "cert.pem";
@@ -10546,8 +12264,7 @@ async function writePrivate(path, contents) {
10546
12264
  throw error;
10547
12265
  }
10548
12266
  }
10549
- //#endregion
10550
- //#region src/index.ts
12267
+ // src/index.ts
10551
12268
  /**
10552
12269
  * dsh-deeppilot — data bridge between the DSH host and DeepPilot
10553
12270
  * clients. Owns independent, narrowly routed LAN (TLS-only, pinned by paired
@@ -11184,6 +12901,20 @@ function apply(ctx, options) {
11184
12901
  if (state.bridge === bridge) state.bridge = void 0;
11185
12902
  bridge.dispose();
11186
12903
  }, "deeppilot: host streams");
12904
+ apiCtx.inject(["tools", "schedule"], (sub) => {
12905
+ if (bridge.capabilities.schedulesHost !== true) {
12906
+ log("quiet completion tool not registered: host-level schedules unavailable");
12907
+ return;
12908
+ }
12909
+ const tools = sub.tools;
12910
+ if (typeof tools?.register !== "function") {
12911
+ log("quiet completion tool not registered: tools service unavailable", "warn");
12912
+ return;
12913
+ }
12914
+ sub.effect(() => registerQuietCompletionTool(tools, bridge.quietCompletion, (message) => log(message)), "deeppilot: quiet completion tool");
12915
+ log("quiet completion tool registered");
12916
+ bridge.seedQuietCompletionSessions();
12917
+ });
11187
12918
  });
11188
12919
  ctx.effect(() => async () => {
11189
12920
  closeAllConnections();
@@ -11197,7 +12928,6 @@ function apply(ctx, options) {
11197
12928
  await diagnostics.dispose();
11198
12929
  }, "deeppilot: process resources");
11199
12930
  }
11200
- //#endregion
11201
12931
  export { Config, HostBridge, apply, inject, mayReceivePush, name, shouldPrunePushToken, shouldReEnrollRelayToken };
11202
12932
 
11203
12933
  //# sourceMappingURL=index.js.map