dsh-deeppilot 0.9.11 → 0.9.14

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
@@ -4,9 +4,9 @@ import { access, appendFile, chmod, mkdir, readFile, readdir, rename, rm, stat,
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { WebSocketServer } from "ws";
6
6
  import { homedir, networkInterfaces } from "node:os";
7
+ import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
7
8
  import z from "@deepseek-ai/schemastery";
8
9
  import { spawn } from "node:child_process";
9
- import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { connect } from "node:http2";
12
12
  import { createServer } from "node:http";
@@ -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
  *
@@ -41,7 +41,9 @@ const ERROR_CODES = {
41
41
  E_NOT_FOUND: "session or request not found",
42
42
  E_BUSY: "session is busy",
43
43
  E_UNSUPPORTED: "protocol version or capability unsupported",
44
- E_INTERNAL: "internal error"
44
+ E_INTERNAL: "internal error",
45
+ E_STEER_UNAVAILABLE: "queued item can no longer steer the current turn",
46
+ E_TOO_LARGE: "response exceeds the client frame limit"
45
47
  };
46
48
  /** Bridge 未携带 code 的结果(内部守卫失败等)落到这一档。 */
47
49
  const WIRE_ERROR_FALLBACK = "E_INTERNAL";
@@ -89,6 +91,9 @@ const DOMAIN_TABLES = {
89
91
  "schedule_not_found": "E_NOT_FOUND",
90
92
  "delivery_cursor_not_found": "E_NOT_FOUND",
91
93
  "schedule_conflict": "E_BUSY",
94
+ "schedule_execution_pending": "E_BUSY",
95
+ "schedule/target-archived": "E_NOT_FOUND",
96
+ "schedule/input-invalid": "E_PROTOCOL",
92
97
  "invalid_prompt": "E_PROTOCOL",
93
98
  "invalid_selector": "E_PROTOCOL",
94
99
  "invalid_rule": "E_PROTOCOL",
@@ -116,6 +121,13 @@ const DOMAIN_TABLES = {
116
121
  "session-not-found": "E_NOT_FOUND",
117
122
  "agent-busy": "E_BUSY",
118
123
  "session-conflict": "E_BUSY"
124
+ },
125
+ queue: {
126
+ "session/queue-item-not-found": "E_NOT_FOUND",
127
+ "session/steer-unavailable": "E_STEER_UNAVAILABLE",
128
+ "session/agent-busy": "E_BUSY",
129
+ "session/writer-held": "E_BUSY",
130
+ "gateway/bad-request": "E_PROTOCOL"
119
131
  }
120
132
  };
121
133
  /**
@@ -201,9 +213,10 @@ function pendingResponseMessage(kind, reason) {
201
213
  case "transport": return "host connection failed while answering " + kind;
202
214
  }
203
215
  }
204
- //#endregion
205
- //#region src/connection-policy.ts
216
+ // src/connection-policy.ts
206
217
  const AUTH_TIMEOUT_MS = 35e3;
218
+ /** 附件 ack 在 `{mediaType,data}` 之外的信封字段(v/type/id/ts)余量。 */
219
+ const ATTACHMENT_ENVELOPE_HEADROOM_BYTES = 1024;
207
220
  const IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
208
221
  "image/png",
209
222
  "image/jpeg",
@@ -249,6 +262,10 @@ function isEnvelope(value) {
249
262
  */
250
263
  const PUSH_SCOPE_BY_TYPE = {
251
264
  "s2c.session.event": "sessions.read",
265
+ "s2c.session.delta": "sessions.read",
266
+ "s2c.plugin.update.status": "sessions.read",
267
+ "s2c.plugin.update.progress": "sessions.manage",
268
+ "s2c.session.queue": "sessions.read",
252
269
  "s2c.skills.changed": "sessions.read",
253
270
  "s2c.commands.changed": "sessions.read",
254
271
  "s2c.models.changed": "sessions.read",
@@ -275,8 +292,20 @@ function sanitizeImageName(value) {
275
292
  function sanitizeDocumentField(value, maxChars) {
276
293
  return value.replace(/[\u0000-\u001F\u007F]/g, " ").trim().slice(0, maxChars);
277
294
  }
278
- //#endregion
279
- //#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
280
309
  const DEFAULT_AUTH_RATE_POLICY = {
281
310
  windowMs: 6e4,
282
311
  attemptsPerSource: 12,
@@ -393,8 +422,7 @@ var AuthRateLimiter = class {
393
422
  }
394
423
  }
395
424
  };
396
- //#endregion
397
- //#region src/diagnostics.ts
425
+ // src/diagnostics.ts
398
426
  /**
399
427
  * 连接诊断事件模型(Diagnostics)——插件唯一的「失败可观测」面。
400
428
  *
@@ -720,8 +748,7 @@ function buildHealthPayload(input) {
720
748
  }
721
749
  };
722
750
  }
723
- //#endregion
724
- //#region src/device-auth.ts
751
+ // src/device-auth.ts
725
752
  function getPairingCodeTtlMs() {
726
753
  return Number(process.env.DEEPPILOT_PAIRING_TTL_MS) || 3e5;
727
754
  }
@@ -810,6 +837,30 @@ function normalizeDeviceScopes(value) {
810
837
  const allowed = new Set(DEVICE_SCOPES);
811
838
  return [...new Set(value.filter((scope) => typeof scope === "string" && allowed.has(scope)))];
812
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
+ }
813
864
  /** One active, single-use pairing grant per plugin runtime. */
814
865
  var PairingCodeManager = class {
815
866
  active = null;
@@ -845,8 +896,7 @@ function createAuthChallenge(audience, now = Date.now()) {
845
896
  expiresAt: now + AUTH_CHALLENGE_TTL_MS
846
897
  };
847
898
  }
848
- //#endregion
849
- //#region src/wire-interaction.ts
899
+ // src/wire-interaction.ts
850
900
  /**
851
901
  * 交互特性的 wire 行:回答待决的 approval 与 question。
852
902
  *
@@ -906,8 +956,7 @@ const interactionRows = [{
906
956
  ctx.send("s2c.ack", {});
907
957
  }
908
958
  }];
909
- //#endregion
910
- //#region src/wire-device.ts
959
+ // src/wire-device.ts
911
960
  /**
912
961
  * 设备生命周期特性的 wire 行:自撤销(解绑)。
913
962
  *
@@ -945,8 +994,7 @@ const deviceRows = [{
945
994
  ctx.close(4401, "device revoked");
946
995
  }
947
996
  }];
948
- //#endregion
949
- //#region src/token.ts
997
+ // src/token.ts
950
998
  /** Expand a leading ~ using the process home directory. */
951
999
  function expandHome(p) {
952
1000
  if (p === "~") return homedir();
@@ -1023,7 +1071,20 @@ var DeviceStore = class DeviceStore {
1023
1071
  const store = new DeviceStore(filePath);
1024
1072
  try {
1025
1073
  const raw = JSON.parse(await readFile(expandHome(filePath), "utf8"));
1026
- 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
+ }
1027
1088
  } catch (error) {
1028
1089
  if (error?.code === "ENOENT") {} else console.log(formatLogLine("warn", "device registry unreadable, starting empty: " + String(error)));
1029
1090
  }
@@ -1248,8 +1309,7 @@ var DeviceStore = class DeviceStore {
1248
1309
  console.log(formatLogLine("warn", "device registry write failed" + (typeof code === "string" ? " code=" + code : "") + ": " + message.slice(0, 160)));
1249
1310
  }
1250
1311
  };
1251
- //#endregion
1252
- //#region src/wire-push.ts
1312
+ // src/wire-push.ts
1253
1313
  /** Live Activity 需要的三个 scope:通知注册 + 会话读取 + 交互应答。 */
1254
1314
  const LIVE_ACTIVITY_SCOPES = [
1255
1315
  "notifications.register",
@@ -1396,21 +1456,83 @@ const pushRows = [
1396
1456
  }
1397
1457
  }
1398
1458
  ];
1399
- //#endregion
1400
- //#region src/wire-schedule.ts
1459
+ // src/wire-schedule.ts
1401
1460
  /**
1402
- * 定时任务特性的 wire 行:列表、历史、创建、修改、删除。
1461
+ * 定时任务特性的 wire 行:会话级(列表、历史、创建、修改、删除)与宿主级
1462
+ * (list/view/create/edit/run/limits)两套。
1403
1463
  *
1404
1464
  * scope 取 `schedule.manage` + `sessions.read`:PROTOCOL.md scope 映射表写明
1405
1465
  * 「`schedule.manage` 定时任务/提醒的列表、历史、创建、修改和删除;同时需要
1406
1466
  * `sessions.read` 才能读取任务内容」。迁移前这条组合一半住在 requiredScope、
1407
1467
  * 一半住在 connection.ts 调用点的一行补充规则里(334-336),现在是一行声明。
1408
1468
  *
1409
- * 五个帧共享一个能力位 `schedules`;该位现在只要求 Schedule 服务存在
1410
- * (见 host-capabilities.ts 的 G4 修正)。
1469
+ * 五个会话级帧共享能力位 `schedules`;该位现在只要求 Schedule 服务存在
1470
+ * (见 host-capabilities.ts 的 G4 修正)。六个宿主级帧共享 `schedulesHost`:
1471
+ * 该位由 adapter 判别位推导,false/缺失时新帧一律 E_UNSUPPORTED,客户端只能
1472
+ * 走会话级帧(PROTOCOL.md「Schedule 宿主级任务扩展」)。
1411
1473
  */
1412
1474
  /** PROTOCOL.md scope 映射表:schedule.manage 需搭配 sessions.read。 */
1413
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
+ }
1414
1536
  const scheduleRows = [
1415
1537
  {
1416
1538
  type: "c2s.schedule.list",
@@ -1442,14 +1564,16 @@ const scheduleRows = [
1442
1564
  validate: (payload) => {
1443
1565
  const checked = payloadObject(payload);
1444
1566
  if (!checked.ok) return checked;
1445
- 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");
1446
1568
  return checked;
1447
1569
  },
1448
1570
  handle: async (ctx, payload) => {
1449
- 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);
1450
1574
  if (!result.ok) return ctx.fail(result.code, result.message);
1451
1575
  ctx.send("s2c.schedule.history", {
1452
- sessionId: payload.sessionId,
1576
+ ...sessionId !== void 0 ? { sessionId } : {},
1453
1577
  history: result.value
1454
1578
  });
1455
1579
  }
@@ -1541,10 +1665,159 @@ const scheduleRows = [
1541
1665
  ...result.replayed ? { replayed: true } : {}
1542
1666
  });
1543
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
+ }
1544
1818
  }
1545
1819
  ];
1546
- //#endregion
1547
- //#region src/document-payload.ts
1820
+ // src/document-payload.ts
1548
1821
  const PREFIX = "[DeepPilot document:";
1549
1822
  const SUFFIX = "]";
1550
1823
  /**
@@ -1581,8 +1854,7 @@ function projectedDocument(text) {
1581
1854
  return;
1582
1855
  }
1583
1856
  }
1584
- //#endregion
1585
- //#region src/host-event-projection.ts
1857
+ // src/host-event-projection.ts
1586
1858
  const MAX_MESSAGE_PROJECTION_BYTES = 262144;
1587
1859
  /** One durable host event sequence becomes exactly one phone message row.
1588
1860
  * Keep the last projection when a host history response repeats an event. */
@@ -1670,7 +1942,8 @@ function projectEvent(sessionId, event) {
1670
1942
  name: String(data?.name ?? "tool"),
1671
1943
  state: "running",
1672
1944
  summary: summarizeArgs(data?.arguments),
1673
- ...data?.callId ? { callId: String(data.callId) } : {}
1945
+ ...data?.callId ? { callId: String(data.callId) } : {},
1946
+ ...editField(data?.name, data?.arguments)
1674
1947
  },
1675
1948
  ts: tsOf(event)
1676
1949
  }
@@ -1870,6 +2143,10 @@ function chunkText(data) {
1870
2143
  const direct = data;
1871
2144
  return typeof direct.text === "string" ? direct.text : "";
1872
2145
  }
2146
+ function editField(name, raw) {
2147
+ const edit = toolEditOf(name, raw);
2148
+ return edit ? { edit } : {};
2149
+ }
1873
2150
  function summarizeArgs(raw) {
1874
2151
  if (typeof raw !== "string" || raw.length === 0) return "";
1875
2152
  try {
@@ -1881,6 +2158,68 @@ function summarizeArgs(raw) {
1881
2158
  return truncate(raw, 90);
1882
2159
  }
1883
2160
  }
2161
+ /** 宿主的文件编辑工具名(DSH `dsh-tool-str-replace-editor`)。 */
2162
+ const EDITOR_TOOL_NAME = "str_replace_editor";
2163
+ /**
2164
+ * 把编辑类工具调用的原始参数归一化成 `ToolEditProjection`。
2165
+ *
2166
+ * 只认 `str_replace_editor` 的 `str_replace` / `create` / `insert`(`view` 不改文件),
2167
+ * 其他工具或无法解析的参数返回 undefined,工具行保持原样。与 iOS `ToolEditDiff.parse`
2168
+ * 的解析口径一致:`new_str` 缺失视为删除(空串)。
2169
+ */
2170
+ function toolEditOf(name, raw) {
2171
+ if (name !== EDITOR_TOOL_NAME || typeof raw !== "string" || raw.length === 0) return void 0;
2172
+ let args;
2173
+ try {
2174
+ const parsed = JSON.parse(raw);
2175
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
2176
+ args = parsed;
2177
+ } catch {
2178
+ return;
2179
+ }
2180
+ const path = args.path;
2181
+ if (typeof path !== "string" || path.length === 0) return void 0;
2182
+ let edit;
2183
+ switch (args.command) {
2184
+ case "str_replace":
2185
+ if (typeof args.old_str !== "string") return void 0;
2186
+ edit = {
2187
+ path,
2188
+ operation: "replace",
2189
+ oldText: args.old_str,
2190
+ newText: typeof args.new_str === "string" ? args.new_str : ""
2191
+ };
2192
+ break;
2193
+ case "create":
2194
+ if (typeof args.file_text !== "string") return void 0;
2195
+ edit = {
2196
+ path,
2197
+ operation: "create",
2198
+ newText: args.file_text
2199
+ };
2200
+ break;
2201
+ case "insert":
2202
+ if (typeof args.new_str !== "string") return void 0;
2203
+ edit = {
2204
+ path,
2205
+ operation: "insert",
2206
+ newText: args.new_str,
2207
+ ...Number.isSafeInteger(args.insert_line) ? { insertLine: args.insert_line } : {}
2208
+ };
2209
+ break;
2210
+ default: return;
2211
+ }
2212
+ return [edit.oldText, edit.newText].some((text) => text !== void 0 && Buffer.byteLength(text, "utf8") > 16384) ? withoutEditBodies(edit) : edit;
2213
+ }
2214
+ /** 去掉改动正文,只留路径、操作与 truncated 标记(超限或整条消息超限时用)。 */
2215
+ function withoutEditBodies(edit) {
2216
+ return {
2217
+ path: edit.path,
2218
+ operation: edit.operation,
2219
+ ...edit.insertLine !== void 0 ? { insertLine: edit.insertLine } : {},
2220
+ truncated: true
2221
+ };
2222
+ }
1884
2223
  function truncate(text, max) {
1885
2224
  return text.length <= max ? text : text.slice(0, max - 1) + "…";
1886
2225
  }
@@ -1924,7 +2263,8 @@ function projectHistory(events) {
1924
2263
  tool: {
1925
2264
  name: String(data?.name ?? "tool"),
1926
2265
  state: "running",
1927
- summary: summarizeArgs(data?.arguments)
2266
+ summary: summarizeArgs(data?.arguments),
2267
+ ...editField(data?.name, data?.arguments)
1928
2268
  }
1929
2269
  };
1930
2270
  messages.push(row);
@@ -1973,7 +2313,8 @@ function limitMessageProjection(message) {
1973
2313
  ...message.tool ? { tool: {
1974
2314
  ...message.tool,
1975
2315
  name: truncateUtf8(message.tool.name, 4096),
1976
- summary: truncateUtf8(message.tool.summary, 65536)
2316
+ summary: truncateUtf8(message.tool.summary, 65536),
2317
+ ...message.tool.edit ? { edit: withoutEditBodies(message.tool.edit) } : {}
1977
2318
  } } : {},
1978
2319
  ...message.attachments ? { attachments: message.attachments.slice(0, 16).map((attachment) => ({
1979
2320
  ...attachment,
@@ -2117,8 +2458,7 @@ function historyWindow(source, options) {
2117
2458
  hasMore: older.length > 0
2118
2459
  };
2119
2460
  }
2120
- //#endregion
2121
- //#region src/wire-session.ts
2461
+ // src/wire-session.ts
2122
2462
  /**
2123
2463
  * 会话特性的 wire 行:列表/归档、打开与关闭、创建/分支/重命名/取消、历史与
2124
2464
  * 附件、模型目录与切换、prompt 投递。
@@ -2452,6 +2792,9 @@ const sessionRows = [
2452
2792
  const invalid = requireSessionId(checked.value);
2453
2793
  if (invalid) return invalid;
2454
2794
  if (!isText(checked.value.attachmentId)) return reject$1("E_PROTOCOL", "invalid attachmentId");
2795
+ const maxFrameBytes = checked.value.maxFrameBytes;
2796
+ if (maxFrameBytes !== void 0 && (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes <= 0)) return reject$1("E_PROTOCOL", "invalid maxFrameBytes");
2797
+ if (checked.value.binary !== void 0 && typeof checked.value.binary !== "boolean") return reject$1("E_PROTOCOL", "invalid binary");
2455
2798
  return checked;
2456
2799
  },
2457
2800
  handle: async (ctx, payload) => {
@@ -2459,6 +2802,34 @@ const sessionRows = [
2459
2802
  if (attachmentId.length === 0) return ctx.fail("E_PROTOCOL", "sessionId and attachmentId required");
2460
2803
  const image = await ctx.bridge.attachmentData(payload.sessionId, attachmentId);
2461
2804
  if (!image) return ctx.fail("E_NOT_FOUND", "attachment unavailable");
2805
+ if (payload.binary === true && ctx.sendBinary) {
2806
+ const bytes = Buffer.from(image.data, "base64");
2807
+ if (bytes.length === 0) return ctx.fail("E_NOT_FOUND", "attachment unavailable");
2808
+ if (bytes.length > 12582912) return ctx.fail("E_TOO_LARGE", `attachment ${bytes.length} bytes exceeds limit`);
2809
+ let index = 0;
2810
+ try {
2811
+ for (let offset = 0; offset < bytes.length; offset += BINARY_CHUNK_BYTES) {
2812
+ await ctx.waitForWritable?.();
2813
+ ctx.sendBinary("s2c.session.attachment.chunk", { index: index++ }, bytes.subarray(offset, offset + BINARY_CHUNK_BYTES));
2814
+ }
2815
+ } catch {
2816
+ return;
2817
+ }
2818
+ const ack = {
2819
+ ...image.mediaType !== void 0 ? { mediaType: image.mediaType } : {},
2820
+ chunks: index,
2821
+ bytes: bytes.length
2822
+ };
2823
+ return ctx.send("s2c.ack", ack);
2824
+ }
2825
+ const clientLimit = payload.maxFrameBytes ?? 1048576;
2826
+ const frameBytes = image.data.length + (image.mediaType?.length ?? 0) + ATTACHMENT_ENVELOPE_HEADROOM_BYTES;
2827
+ if (frameBytes > Math.min(clientLimit, 2097152)) return ctx.fail("E_TOO_LARGE", `attachment frame ${frameBytes} bytes exceeds limit`);
2828
+ try {
2829
+ await ctx.waitForWritable?.();
2830
+ } catch {
2831
+ return;
2832
+ }
2462
2833
  ctx.send("s2c.ack", image);
2463
2834
  }
2464
2835
  },
@@ -2560,6 +2931,7 @@ const sessionRows = [
2560
2931
  if (invalid) return invalid;
2561
2932
  const p = checked.value;
2562
2933
  if (p.clientSendId !== void 0 && !validSendId(p.clientSendId)) return reject$1("E_PROTOCOL", "invalid clientSendId");
2934
+ if (p.mode !== void 0 && p.mode !== "queue" && p.mode !== "steer") return reject$1("E_PROTOCOL", "invalid prompt mode");
2563
2935
  if (!isOptionalField(p, "text", (v) => isText(v, 262144, false)) || !isOptionalField(p, "images", Array.isArray) || !isOptionalField(p, "documents", Array.isArray)) return reject$1("E_PROTOCOL", "invalid prompt fields");
2564
2936
  for (const key of ["images", "documents"]) {
2565
2937
  const items = p[key];
@@ -2607,6 +2979,7 @@ const sessionRows = [
2607
2979
  text,
2608
2980
  images,
2609
2981
  documents,
2982
+ ...p.mode === "steer" ? { mode: "steer" } : {},
2610
2983
  ...p.clientSendId !== void 0 ? { clientSendId: p.clientSendId } : {}
2611
2984
  });
2612
2985
  },
@@ -2616,26 +2989,59 @@ const sessionRows = [
2616
2989
  const images = payload.images;
2617
2990
  const documents = payload.documents;
2618
2991
  const clientSendId = payload.clientSendId;
2992
+ const steer = payload.mode === "steer";
2993
+ if (steer && ctx.bridge.capabilities.promptQueue !== true) return ctx.fail("E_UNSUPPORTED", "prompt queue not supported");
2994
+ const mode = steer ? "steer" : "queue";
2619
2995
  if (clientSendId !== void 0) {
2620
2996
  const receipt = await ctx.bridge.promptDeliveries.dispatch(ctx.deviceId, clientSendId, {
2621
2997
  sessionId,
2622
2998
  content: {
2623
2999
  text,
2624
3000
  images,
2625
- documents
3001
+ documents,
3002
+ ...steer ? { mode } : {}
2626
3003
  }
2627
- }, () => ctx.bridge.sendPrompt(sessionId, text, images, documents));
3004
+ }, () => ctx.bridge.sendPrompt(sessionId, text, images, documents, {
3005
+ mode,
3006
+ clientSendId
3007
+ }));
2628
3008
  ctx.send("s2c.ack", receipt);
2629
3009
  return;
2630
3010
  }
2631
- const userSeq = await ctx.bridge.sendPrompt(sessionId, text, images, documents);
3011
+ const userSeq = await ctx.bridge.sendPrompt(sessionId, text, images, documents, { mode });
2632
3012
  if (!userSeq.ok) return ctx.fail(userSeq.code, userSeq.message);
2633
3013
  ctx.send("s2c.ack", { userSeq: userSeq.value });
2634
3014
  }
3015
+ },
3016
+ {
3017
+ type: "c2s.session.queue.update",
3018
+ stage: "authenticated",
3019
+ scopes: ["prompt.send"],
3020
+ capability: "promptQueue",
3021
+ doc: "PROTOCOL.md c2s.session.queue.update",
3022
+ validate: (payload) => {
3023
+ const checked = payloadObject(payload);
3024
+ if (!checked.ok) return checked;
3025
+ const invalid = requireSessionId(checked.value);
3026
+ if (invalid) return invalid;
3027
+ if (!isText(checked.value.itemId, 256)) return reject$1("E_PROTOCOL", "invalid itemId");
3028
+ const action = checked.value.action;
3029
+ if (action === "edit") return reject$1("E_UNSUPPORTED", "queue edit not supported");
3030
+ if (action !== "remove" && action !== "steer") return reject$1("E_PROTOCOL", "invalid queue action");
3031
+ return accept({
3032
+ sessionId: checked.value.sessionId,
3033
+ itemId: checked.value.itemId,
3034
+ action
3035
+ });
3036
+ },
3037
+ handle: async (ctx, payload) => {
3038
+ const result = await ctx.bridge.updateQueue(payload.sessionId, payload.itemId, payload.action);
3039
+ if (!result.ok) return ctx.fail(result.code, result.message);
3040
+ ctx.send("s2c.ack", result.value);
3041
+ }
2635
3042
  }
2636
3043
  ];
2637
- //#endregion
2638
- //#region src/wire-command.ts
3044
+ // src/wire-command.ts
2639
3045
  function sessionPayload(value) {
2640
3046
  const result = payloadObject(value);
2641
3047
  return result.ok && !isText(result.value.sessionId) ? reject$1("E_PROTOCOL", "会话标识无效") : result;
@@ -2701,38 +3107,60 @@ const commandRows = [...[
2701
3107
  stage: "authenticated",
2702
3108
  scopes: ["sessions.read"],
2703
3109
  capability: "commands",
3110
+ conditionalCapabilities: ["sessionMarkdown"],
2704
3111
  doc: "PROTOCOL.md c2s.session.export",
2705
- validate: sessionPayload,
3112
+ validate: (value) => {
3113
+ const result = sessionPayload(value);
3114
+ if (!result.ok) return result;
3115
+ if (result.value.format !== void 0 && result.value.format !== "zip" && result.value.format !== "markdown") return reject$1("E_PROTOCOL", "导出格式无效");
3116
+ if (result.value.binary !== void 0 && typeof result.value.binary !== "boolean") return reject$1("E_PROTOCOL", "传输方式无效");
3117
+ return result;
3118
+ },
2706
3119
  handle: async (ctx, payload) => {
3120
+ const format = payload.format;
3121
+ if (format === "markdown" && ctx.bridge.capabilities.sessionMarkdown !== true) return ctx.fail("E_UNSUPPORTED", "当前主机不支持导出对话正文");
2707
3122
  const signal = AbortSignal.any([ctx.signal ?? new AbortController().signal, AbortSignal.timeout(6e5)]);
2708
3123
  let reader;
2709
3124
  const cancel = () => {
2710
3125
  reader?.cancel().catch(() => {});
2711
3126
  };
2712
3127
  try {
2713
- reader = (await ctx.bridge.exportSession(payload.sessionId, signal)).body.getReader();
3128
+ reader = (format === void 0 ? await ctx.bridge.exportSession(payload.sessionId, signal) : await ctx.bridge.exportSession(payload.sessionId, signal, format)).body.getReader();
2714
3129
  signal.addEventListener("abort", cancel, { once: true });
3130
+ const binary = payload.binary === true && ctx.sendBinary !== void 0;
2715
3131
  let index = 0, bytes = 0;
3132
+ const sendChunk = async (chunk) => {
3133
+ bytes += chunk.length;
3134
+ await ctx.waitForWritable?.();
3135
+ if (binary) ctx.sendBinary("s2c.session.export.chunk", { index: index++ }, chunk);
3136
+ else ctx.send("s2c.session.export.chunk", {
3137
+ index: index++,
3138
+ data: Buffer.from(chunk).toString("base64")
3139
+ });
3140
+ };
3141
+ let carry;
2716
3142
  while (true) {
2717
3143
  signal.throwIfAborted();
2718
3144
  const next = await reader.read();
2719
3145
  if (next.done) break;
2720
- for (let offset = 0; offset < next.value.length; offset += 49152) {
2721
- const chunk = next.value.subarray(offset, offset + 49152);
2722
- bytes += chunk.length;
2723
- await ctx.waitForWritable?.();
2724
- ctx.send("s2c.session.export.chunk", {
2725
- index: index++,
2726
- data: Buffer.from(chunk).toString("base64")
2727
- });
3146
+ if (!binary) {
3147
+ for (let offset = 0; offset < next.value.length; offset += 49152) await sendChunk(next.value.subarray(offset, offset + 49152));
3148
+ continue;
3149
+ }
3150
+ carry = carry ? Buffer.concat([carry, next.value]) : Buffer.from(next.value);
3151
+ while (carry.length >= BINARY_CHUNK_BYTES) {
3152
+ await sendChunk(carry.subarray(0, BINARY_CHUNK_BYTES));
3153
+ carry = carry.subarray(BINARY_CHUNK_BYTES);
2728
3154
  }
2729
3155
  }
2730
3156
  signal.throwIfAborted();
3157
+ if (carry !== void 0 && carry.length > 0) await sendChunk(carry);
2731
3158
  ctx.send("s2c.ack", {
2732
3159
  sessionId: payload.sessionId,
2733
- fileName: "dsh-session-log.zip",
3160
+ fileName: format === "markdown" ? "dsh-session.md" : "dsh-session-log.zip",
2734
3161
  chunks: index,
2735
- bytes
3162
+ bytes,
3163
+ ...format === void 0 ? {} : { mediaType: format === "markdown" ? "text/markdown; charset=utf-8" : "application/zip" }
2736
3164
  });
2737
3165
  } catch (error) {
2738
3166
  const failure = hostFailureOf(error, "command/export-cancelled");
@@ -2749,64 +3177,626 @@ const commandRows = [...[
2749
3177
  }
2750
3178
  }
2751
3179
  }];
2752
- /** 第一个缺失的 scope;消息与迁移前一致(`scope <name> required`)。 */
2753
- function scopeRejection(row, scopes) {
2754
- for (const scope of row.scopes) if (!scopes.has(scope)) return {
2755
- code: "E_FORBIDDEN",
2756
- message: `scope ${scope} required`
2757
- };
2758
- }
2759
- /** 能力位关闭时拒绝。校验先于本检查,与迁移前的次序一致。 */
2760
- function capabilityRejection(row, capabilities) {
2761
- if (row.capability === void 0) return void 0;
2762
- if (capabilities[row.capability] === true) return void 0;
2763
- return {
2764
- code: "E_UNSUPPORTED",
2765
- message: "capability unavailable on this host version"
2766
- };
2767
- }
2768
- function widgetPolicyOf(row) {
2769
- return row.widgetPolicy ?? "deny";
2770
- }
2771
- /** 校验一帧载荷;未登记 validate 的行原样通过。 */
2772
- function validatePayload(row, payload) {
2773
- if (row.validate === void 0) return {
2774
- ok: true,
2775
- value: payload ?? {}
2776
- };
2777
- return row.validate(payload);
2778
- }
2779
- /** 执行一帧。检查(阶段/scope/能力/校验)由 connection 按序先行完成。 */
2780
- async function dispatchFrame(row, ctx, payload) {
2781
- await row.handle(ctx, payload);
2782
- }
2783
- /**
2784
- * 载荷必须是普通对象。此前这条判定住在 request-validation 里,对
2785
- * `c2s.sessions.list` 等无参帧豁免、对 `c2s.sessions.archived` 却不通融
2786
- * (同语义两种待遇,见 G2);现在豁免由各行的 validate 自己声明。
2787
- */
2788
- function payloadObject(value) {
2789
- if (value === void 0 || value === null) return {
2790
- ok: true,
2791
- value: {}
2792
- };
2793
- if (typeof value !== "object" || Array.isArray(value)) return {
2794
- ok: false,
2795
- code: "E_PROTOCOL",
2796
- message: "payload must be an object"
2797
- };
3180
+ // src/plugin-update.ts
3181
+ const PACKAGE = "dsh-deeppilot";
3182
+ const PHASES = [
3183
+ "preparing",
3184
+ "installing",
3185
+ "verifying",
3186
+ "restart-required",
3187
+ "done",
3188
+ "rolled-back",
3189
+ "failed"
3190
+ ];
3191
+ const CELLS = Symbol.for("deeppilot.plugin-update.cells.v1");
3192
+ const globals = globalThis;
3193
+ const cells = globals[CELLS] ??= /* @__PURE__ */ new Map();
3194
+ const ownerKey = (deviceId) => createHash("sha256").update(deviceId).digest("hex");
3195
+ const validVersion = (value) => typeof value === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value) && value.length <= 128;
3196
+ const validUpdateRequestId = (value) => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value);
3197
+ const terminal = (phase) => [
3198
+ "done",
3199
+ "failed",
3200
+ "rolled-back",
3201
+ "restart-required"
3202
+ ].includes(phase);
3203
+ const publicRecord = ({ ownerKey: _owner, restartVersion: _restartVersion, restartPhase: _restartPhase, restartBoot: _restartBoot, ...record }) => ({ ...record });
3204
+ var PluginUpdateError = class extends Error {
3205
+ code;
3206
+ constructor(code, message) {
3207
+ super(message);
3208
+ this.code = code;
3209
+ }
3210
+ };
3211
+ /** 精确镜像 alpha2 公开 registryPlan,避免静态依赖让旧宿主无法加载插件;由真实函数对照测试守卫。 */
3212
+ function updateRegistryPlan(requested, configured) {
3213
+ const own = configured.resolved === null ? null : normalizeUpdateRegistry(configured.resolved);
3214
+ const fallbacks = configured.fallbackRegistries.map(normalizeUpdateRegistry);
3215
+ const ownIsPublic = own !== null && (own === "https://registry.npmjs.org/" || fallbacks.includes(own));
3216
+ const keyOf = (registry) => registry === null ? own : normalizeUpdateRegistry(registry);
3217
+ const known = [];
3218
+ const keys = [];
3219
+ for (const registry of [configured.registry, ...configured.fallbackRegistries]) {
3220
+ if (registry === null && !ownIsPublic) continue;
3221
+ const key = keyOf(registry);
3222
+ if (keys.includes(key)) continue;
3223
+ known.push(registry === null ? null : normalizeUpdateRegistry(registry));
3224
+ keys.push(key);
3225
+ }
3226
+ const first = requested === void 0 ? configured.registry : requested;
3227
+ const firstKey = keyOf(first);
3228
+ const normalizedFirst = first === null ? null : normalizeUpdateRegistry(first);
3229
+ if ((first === null || firstKey === own) && !ownIsPublic) return [normalizedFirst];
3230
+ if (!keys.includes(firstKey)) return [normalizedFirst];
3231
+ return [normalizedFirst, ...known.filter((_registry, index) => keys[index] !== firstKey)];
3232
+ }
3233
+ function normalizeUpdateRegistry(registry) {
3234
+ const parsed = new URL(registry);
3235
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("registry unavailable");
3236
+ if (!parsed.pathname.endsWith("/")) parsed.pathname += "/";
3237
+ return parsed.href;
3238
+ }
3239
+ function updateRegistrySources(requested, configured) {
3240
+ return [...new Set(updateRegistryPlan(requested, configured).map((registry) => {
3241
+ const resolved = registry === null ? configured.resolved : registry;
3242
+ if (!resolved) throw new Error("registry unresolved");
3243
+ return normalizeUpdateRegistry(resolved);
3244
+ }))];
3245
+ }
3246
+ /** 只接受具有完整规范摘要的强 SRI;宽松 base64 解码不能把截断值或弱摘要当作安装凭据。 */
3247
+ function strongIntegrity(value) {
3248
+ if (typeof value !== "string") return void 0;
3249
+ const match = /^(sha256|sha384|sha512)-([A-Za-z0-9+/]+={0,2})$/.exec(value);
3250
+ if (!match) return void 0;
3251
+ const digest = Buffer.from(match[2], "base64");
3252
+ const bytes = {
3253
+ sha256: 32,
3254
+ sha384: 48,
3255
+ sha512: 64
3256
+ }[match[1]];
3257
+ return digest.length === bytes && digest.toString("base64") === match[2] ? value : void 0;
3258
+ }
3259
+ /** 精确版本事实由元数据读取;软件源地址、上游响应体及包管理器诊断不进入对外错误。 */
3260
+ async function readTargetManifestFacts(registry, version, fetcher) {
3261
+ const base = new URL(registry);
3262
+ if (!["http:", "https:"].includes(base.protocol) || base.username || base.password || base.search || base.hash || !validVersion(version)) throw new Error("registry unavailable");
3263
+ base.pathname = base.pathname.replace(/\/?$/, "/") + encodeURIComponent(PACKAGE) + "/" + encodeURIComponent(version);
3264
+ const response = await fetcher(base, {
3265
+ signal: AbortSignal.timeout(1e4),
3266
+ redirect: "error",
3267
+ headers: { accept: "application/json" }
3268
+ });
3269
+ if (!response.ok || !response.body) throw new Error("manifest unavailable");
3270
+ const reader = response.body.getReader();
3271
+ const chunks = [];
3272
+ let bytes = 0;
3273
+ try {
3274
+ while (true) {
3275
+ const next = await reader.read();
3276
+ if (next.done) break;
3277
+ bytes += next.value.length;
3278
+ if (bytes > 1048576) throw new Error("manifest too large");
3279
+ chunks.push(next.value);
3280
+ }
3281
+ } finally {
3282
+ await reader.cancel().catch(() => {});
3283
+ reader.releaseLock();
3284
+ }
3285
+ const manifest = JSON.parse(Buffer.concat(chunks).toString("utf8"));
3286
+ if (manifest.name !== PACKAGE || manifest.version !== version || !manifest.dsh?.bundle) throw new Error("manifest mismatch");
3287
+ const protocol = manifest.deepPilot?.protocolVersion;
2798
3288
  return {
2799
- ok: true,
2800
- value
3289
+ protocolVersion: typeof protocol === "number" && Number.isSafeInteger(protocol) && protocol > 0 ? protocol : void 0,
3290
+ integrity: strongIntegrity(manifest.dist?.integrity)
2801
3291
  };
2802
3292
  }
2803
- /** 字符串字段:类型正确、不超长、非空(可放宽)。 */
2804
- function isText(value, max = 4096, nonempty = true) {
2805
- return typeof value === "string" && value.length <= max && (!nonempty || value.trim().length > 0);
3293
+ /** 检查界面只预览首个软件源的协议;实际安装必须通过全部可回退源的强校验。 */
3294
+ async function readTargetProtocol(registry, version, fetcher = fetch) {
3295
+ return (await readTargetManifestFacts(registry, version, fetcher)).protocolVersion;
2806
3296
  }
2807
- /** 可选字段:缺省合法,给出时必须满足 check。 */
2808
- function isOptionalField(payload, key, check) {
2809
- return payload[key] === void 0 || check(payload[key]);
3297
+ var PluginUpdater = class {
3298
+ options;
3299
+ state;
3300
+ path;
3301
+ listVersions;
3302
+ updateBundle;
3303
+ listBundles;
3304
+ registries;
3305
+ constructor(options) {
3306
+ this.options = options;
3307
+ this.path = resolve(options.journalPath);
3308
+ this.listVersions = options.manager.listVersions.bind(options.manager);
3309
+ this.updateBundle = options.manager.updateBundle.bind(options.manager);
3310
+ this.listBundles = options.manager.listBundles.bind(options.manager);
3311
+ this.registries = options.manager.registries.bind(options.manager);
3312
+ let state = cells.get(this.path);
3313
+ if (!state) {
3314
+ state = {
3315
+ bootId: randomUUID(),
3316
+ records: [],
3317
+ listeners: /* @__PURE__ */ new Set(),
3318
+ runningVersion: options.currentVersion,
3319
+ healthy: true
3320
+ };
3321
+ if (existsSync(this.path)) try {
3322
+ if (statSync(this.path).size > 262144) throw new Error("oversized journal");
3323
+ const data = JSON.parse(readFileSync(this.path, "utf8"));
3324
+ if (data.version !== 1 || !Array.isArray(data.records) || data.records.length > 128 || data.records.some((record) => !record || !validUpdateRequestId(record.clientRequestId) || !validVersion(record.fromVersion) || !validVersion(record.toVersion) || !PHASES.includes(record.phase) || !Number.isSafeInteger(record.startedAt) || record.startedAt < 0 || typeof record.message !== "string" || record.message.length > 1024 || !/^[a-f0-9]{64}$/.test(record.ownerKey) || record.restartVersion !== void 0 && !validVersion(record.restartVersion) || record.restartBoot !== void 0 && (typeof record.restartBoot !== "string" || record.restartBoot.length > 128) || record.restartPhase !== void 0 && record.restartPhase !== "done" && record.restartPhase !== "rolled-back") || new Set(data.records.map((record) => record.clientRequestId)).size !== data.records.length) throw new Error("invalid journal");
3325
+ state.records = data.records;
3326
+ } catch {
3327
+ state.healthy = false;
3328
+ }
3329
+ cells.set(this.path, state);
3330
+ }
3331
+ this.state = state;
3332
+ state.runningVersion = options.currentVersion;
3333
+ }
3334
+ save() {
3335
+ mkdirSync(dirname(this.path), {
3336
+ recursive: true,
3337
+ mode: 448
3338
+ });
3339
+ const temp = this.path + "." + randomUUID() + ".tmp";
3340
+ const fd = openSync(temp, "wx", 384);
3341
+ try {
3342
+ writeFileSync(fd, JSON.stringify({
3343
+ version: 1,
3344
+ records: this.state.records
3345
+ }));
3346
+ fsyncSync(fd);
3347
+ } finally {
3348
+ closeSync(fd);
3349
+ }
3350
+ renameSync(temp, this.path);
3351
+ const directory = openSync(dirname(this.path), "r");
3352
+ try {
3353
+ fsyncSync(directory);
3354
+ } catch (error) {
3355
+ if (![
3356
+ "EPERM",
3357
+ "EISDIR",
3358
+ "EINVAL"
3359
+ ].includes(String(error.code))) throw error;
3360
+ } finally {
3361
+ closeSync(directory);
3362
+ }
3363
+ }
3364
+ transition(record, phase, message) {
3365
+ const before = { ...record };
3366
+ record.phase = phase;
3367
+ record.message = message;
3368
+ if (phase === "restart-required") record.restartBoot = this.state.bootId;
3369
+ try {
3370
+ this.save();
3371
+ } catch (error) {
3372
+ Object.assign(record, before);
3373
+ record.restartBoot = before.restartBoot;
3374
+ throw error;
3375
+ }
3376
+ for (const listener of this.state.listeners) try {
3377
+ listener({
3378
+ clientRequestId: record.clientRequestId,
3379
+ phase,
3380
+ message
3381
+ }, record.ownerKey);
3382
+ } catch {}
3383
+ }
3384
+ subscribe(deviceId, listener) {
3385
+ const key = ownerKey(deviceId);
3386
+ const wrapped = (event, owner) => {
3387
+ if (owner === key) listener(event);
3388
+ };
3389
+ this.state.listeners.add(wrapped);
3390
+ return () => {
3391
+ this.state.listeners.delete(wrapped);
3392
+ };
3393
+ }
3394
+ recover() {
3395
+ if (this.state.active || !this.state.healthy) return;
3396
+ for (const record of this.state.records) if (record.phase === "restart-required" && record.restartBoot !== this.state.bootId && this.options.currentVersion === (record.restartVersion ?? record.toVersion)) this.transition(record, record.restartPhase ?? "done", record.restartPhase === "rolled-back" ? "重启后已运行旧版本,宿主回滚完成" : "重启后已运行目标版本,更新完成");
3397
+ else if (!terminal(record.phase)) {
3398
+ if (this.options.currentVersion === record.toVersion) this.transition(record, "done", "已重新连接到目标版本,更新完成");
3399
+ else this.transition(record, "failed", "上次更新已中断,结果无法确认;请检查当前版本,原请求不会自动重试");
3400
+ }
3401
+ }
3402
+ async check(deviceId) {
3403
+ let reason;
3404
+ try {
3405
+ this.recover();
3406
+ } catch {
3407
+ reason = "更新日志保存失败,暂时无法确认更新结果";
3408
+ }
3409
+ if (!this.state.healthy) reason = "更新日志无法读取,已停用安装以保护上次更新记录";
3410
+ const finish = (status) => {
3411
+ const journal = [...this.state.records].reverse().find((record) => record.ownerKey === ownerKey(deviceId));
3412
+ return {
3413
+ ...status,
3414
+ ...journal ? { journal: publicRecord(journal) } : {}
3415
+ };
3416
+ };
3417
+ const base = {
3418
+ currentVersion: this.options.currentVersion,
3419
+ protocolVersion: 2,
3420
+ latestVersion: null,
3421
+ updateAvailable: false,
3422
+ compatible: false,
3423
+ protocolCompatible: false,
3424
+ versions: [],
3425
+ checkedAt: Date.now()
3426
+ };
3427
+ try {
3428
+ const listed = await this.listVersions(PACKAGE);
3429
+ if (listed.status !== "listed") return finish({
3430
+ ...base,
3431
+ reason: reason ?? "暂时无法读取可更新版本,请稍后重试"
3432
+ });
3433
+ const registry = listed.registry ?? (await this.registries()).resolved;
3434
+ if (!registry) return finish({
3435
+ ...base,
3436
+ reason: reason ?? "无法确定软件源,暂时不能核对目标版本协议"
3437
+ });
3438
+ const versions = await Promise.all(listed.versions.slice(0, 10).map(async (entry) => {
3439
+ let protocolVersion;
3440
+ try {
3441
+ protocolVersion = await readTargetProtocol(registry, entry.version, this.options.fetch);
3442
+ } catch {}
3443
+ return {
3444
+ version: entry.version,
3445
+ compatible: entry.bundle && entry.incompatible === void 0,
3446
+ ...protocolVersion === void 0 ? {} : { protocolVersion }
3447
+ };
3448
+ }));
3449
+ const latest = versions[0];
3450
+ return finish({
3451
+ ...base,
3452
+ latestVersion: latest?.version ?? null,
3453
+ updateAvailable: versions.length > 0,
3454
+ compatible: latest?.compatible ?? false,
3455
+ protocolCompatible: latest?.protocolVersion === 2,
3456
+ versions,
3457
+ ...reason ? { reason } : versions.some((entry) => entry.protocolVersion === void 0) ? { reason: "部分目标版本缺少可核验的协议声明,暂时不可安装" } : {}
3458
+ });
3459
+ } catch {
3460
+ return finish({
3461
+ ...base,
3462
+ reason: reason ?? "暂时无法检查更新,请确认网络后重试"
3463
+ });
3464
+ }
3465
+ }
3466
+ /** The apply frame is the phone's explicit confirmation. check() never mutates packages. */
3467
+ async apply(deviceId, version, clientRequestId) {
3468
+ if (!validVersion(version) || !validUpdateRequestId(clientRequestId)) throw new PluginUpdateError("E_PROTOCOL", "更新版本或请求标识无效");
3469
+ if (!this.state.healthy) throw new PluginUpdateError("E_INTERNAL", "更新日志无法读取,暂时不能安装");
3470
+ const previous = this.state.records.find((record) => record.clientRequestId === clientRequestId);
3471
+ if (previous) {
3472
+ if (previous.ownerKey !== ownerKey(deviceId)) throw new PluginUpdateError("E_FORBIDDEN", "更新请求不属于当前设备");
3473
+ if (previous.toVersion !== version) throw new PluginUpdateError("E_PROTOCOL", "同一更新请求不能更换目标版本");
3474
+ this.recover();
3475
+ return publicRecord(previous);
3476
+ }
3477
+ const pending = this.state.pending;
3478
+ if (pending?.record.clientRequestId === clientRequestId) {
3479
+ if (pending.record.ownerKey !== ownerKey(deviceId)) throw new PluginUpdateError("E_FORBIDDEN", "更新请求不属于当前设备");
3480
+ if (pending.record.toVersion !== version) throw new PluginUpdateError("E_PROTOCOL", "同一更新请求不能更换目标版本");
3481
+ return pending.accepted;
3482
+ }
3483
+ if (this.state.active) throw new PluginUpdateError("E_BUSY", "已有插件更新正在进行,请稍后检查结果");
3484
+ if (this.state.records.length >= 128) throw new PluginUpdateError("E_INTERNAL", "更新记录已满,请在电脑端检查后再更新");
3485
+ const record = {
3486
+ clientRequestId,
3487
+ fromVersion: this.options.currentVersion,
3488
+ toVersion: version,
3489
+ phase: "preparing",
3490
+ startedAt: Date.now(),
3491
+ message: "目标校验已通过,准备开始安装",
3492
+ ownerKey: ownerKey(deviceId)
3493
+ };
3494
+ let accept;
3495
+ let refuse;
3496
+ const accepted = new Promise((resolve_, reject_) => {
3497
+ accept = resolve_;
3498
+ refuse = reject_;
3499
+ });
3500
+ this.state.pending = {
3501
+ record,
3502
+ accepted
3503
+ };
3504
+ const operation = Promise.resolve().then(async () => {
3505
+ let registry;
3506
+ try {
3507
+ registry = await this.prepare(record);
3508
+ this.state.records.push(record);
3509
+ try {
3510
+ this.save();
3511
+ } catch {
3512
+ this.state.records.splice(this.state.records.indexOf(record), 1);
3513
+ throw new PluginUpdateError("E_INTERNAL", "更新日志保存失败,尚未开始安装");
3514
+ }
3515
+ } catch (error) {
3516
+ if (this.state.active === operation) this.state.active = void 0;
3517
+ if (this.state.pending?.record === record) this.state.pending = void 0;
3518
+ refuse(error instanceof PluginUpdateError ? error : new PluginUpdateError("E_INTERNAL", "无法核对目标版本,尚未开始安装"));
3519
+ return;
3520
+ }
3521
+ if (this.state.pending?.record === record) this.state.pending = void 0;
3522
+ accept(publicRecord(record));
3523
+ await new Promise((resolve_) => setImmediate(resolve_));
3524
+ try {
3525
+ await this.run(record, registry);
3526
+ } catch {
3527
+ try {
3528
+ this.transition(record, "failed", "更新结果无法确认,请检查当前版本;原请求不会自动重试");
3529
+ } catch {}
3530
+ }
3531
+ }).finally(() => {
3532
+ if (this.state.active === operation) this.state.active = void 0;
3533
+ });
3534
+ this.state.active = operation;
3535
+ return accepted;
3536
+ }
3537
+ /** Useful to isolated tests; production never waits for this from disposal. */
3538
+ async settled() {
3539
+ await this.state.active;
3540
+ }
3541
+ /** 所有可执行目标校验在受理前完成;宿主安装及其回滚处理仍属于独立的后续任务。 */
3542
+ async prepare(record) {
3543
+ let listed;
3544
+ try {
3545
+ listed = await this.listVersions(PACKAGE);
3546
+ } catch {
3547
+ throw new PluginUpdateError("E_INTERNAL", "无法核对可更新版本,尚未开始安装");
3548
+ }
3549
+ if (listed.status !== "listed" || listed.installed !== record.fromVersion) throw new PluginUpdateError("E_PROTOCOL", "安装版本已变化或无法读取,请重新检查更新");
3550
+ const target = listed.versions.find((entry) => entry.version === record.toVersion);
3551
+ if (!target?.bundle || target.incompatible !== void 0) throw new PluginUpdateError("E_UNSUPPORTED", "目标版本不存在或不兼容当前宿主,尚未开始安装");
3552
+ try {
3553
+ const sources = updateRegistrySources(listed.registry, await this.registries());
3554
+ let integrity;
3555
+ for (const source of sources) {
3556
+ const facts = await readTargetManifestFacts(source, record.toVersion, this.options.fetch ?? fetch);
3557
+ if (facts.protocolVersion !== 2) throw new PluginUpdateError("E_UNSUPPORTED", "候选软件源的目标协议不兼容或缺少声明,尚未开始安装");
3558
+ if (!facts.integrity) throw new PluginUpdateError("E_UNSUPPORTED", "候选软件源缺少有效的强完整性摘要,尚未开始安装");
3559
+ if (integrity !== void 0 && facts.integrity !== integrity) throw new PluginUpdateError("E_UNSUPPORTED", "候选软件源的目标包完整性摘要不一致,尚未开始安装");
3560
+ integrity = facts.integrity;
3561
+ }
3562
+ const current = updateRegistrySources(listed.registry, await this.registries());
3563
+ if (JSON.stringify(current) !== JSON.stringify(sources)) throw new PluginUpdateError("E_BUSY", "软件源配置已变化,请重新检查更新");
3564
+ } catch (error) {
3565
+ if (error instanceof PluginUpdateError) throw error;
3566
+ throw new PluginUpdateError("E_INTERNAL", "无法核验全部候选软件源的目标协议与完整性,尚未开始安装");
3567
+ }
3568
+ return listed.registry;
3569
+ }
3570
+ async run(record, registry) {
3571
+ this.transition(record, "installing", "宿主正在安装目标版本,连接可能暂时中断");
3572
+ const outcome = await this.updateBundle(PACKAGE, record.toVersion, {
3573
+ registry,
3574
+ requestId: record.clientRequestId
3575
+ });
3576
+ const result = outcome.result;
3577
+ if (outcome.from !== record.fromVersion || outcome.to !== record.toVersion) return this.transition(record, "failed", "宿主返回的更新版本不一致,请检查当前安装");
3578
+ if (outcome.restore) {
3579
+ const restored = outcome.restore;
3580
+ if (restored.application === "restart-required") {
3581
+ record.restartVersion = record.fromVersion;
3582
+ record.restartPhase = "rolled-back";
3583
+ return this.transition(record, "restart-required", "目标安装失败,旧版已重新安装;请在电脑端重启 DSH 后确认恢复");
3584
+ }
3585
+ if (restored.application === "applied" && (restored.version === void 0 || restored.version === record.fromVersion)) {
3586
+ if ((await this.listBundles()).find((bundle) => bundle.name === PACKAGE)?.version === record.fromVersion) return this.transition(record, "rolled-back", "目标安装失败,宿主已自动恢复旧版本");
3587
+ return this.transition(record, "failed", "宿主已尝试回滚,但无法核对实际旧版本,请在电脑端检查插件");
3588
+ }
3589
+ return this.transition(record, "failed", "目标安装失败,宿主恢复旧版也未完成,请在电脑端检查插件");
3590
+ }
3591
+ if (outcome.step !== "install" || result.application === "failed" || result.application === "cancelled" || result.application === "overridden") return this.transition(record, "failed", installFailureMessage(result));
3592
+ if (result.version !== void 0 && result.version !== record.toVersion) return this.transition(record, "failed", "实际安装版本与目标不一致,请在电脑端检查");
3593
+ if (result.application === "restart-required") return this.transition(record, "restart-required", "目标版本已安装,宿主需要重启;请在电脑端重启 DSH 后重新连接");
3594
+ this.transition(record, "verifying", "正在核对实际安装版本");
3595
+ if ((await this.listBundles()).find((bundle) => bundle.name === PACKAGE)?.version !== record.toVersion) return this.transition(record, "failed", "无法确认实际安装版本,请在电脑端检查");
3596
+ if (this.state.runningVersion !== record.toVersion) return this.transition(record, "restart-required", "目标版本已安装,当前插件尚未重新加载;请在电脑端重启 DSH 后重新连接");
3597
+ this.transition(record, "done", "目标版本已加载,更新完成");
3598
+ }
3599
+ };
3600
+ function installFailureMessage(result) {
3601
+ if (result.error?.code === "incompatible-version" || result.error?.incompatible?.length || result.packageResult?.incompatible?.length) return "目标版本不兼容当前宿主,安装已拒绝(incompatible-version)";
3602
+ if (result.pendingBuilds?.length || result.packageResult?.kind === "build-blocked") return "安装需要在电脑端明确批准构建脚本,更新未完成(build-blocked)";
3603
+ const kind = result.packageResult?.kind;
3604
+ const reasons = {
3605
+ "pnpm-missing": "电脑端缺少包管理器,更新未完成",
3606
+ timeout: "安装等待超时,请检查电脑端后重试",
3607
+ "not-found": "软件源尚未提供目标包,更新未完成",
3608
+ "no-matching-version": "软件源尚未提供目标版本,更新未完成",
3609
+ network: "安装无法连接软件源,请恢复网络后重新检查",
3610
+ "disk-full": "电脑端磁盘空间不足,更新未完成",
3611
+ permission: "电脑端没有写入插件的权限,更新未完成",
3612
+ integrity: "目标包完整性校验失败,更新未完成"
3613
+ };
3614
+ if (kind && reasons[kind]) return `${reasons[kind]}(${kind})`;
3615
+ if (result.application === "cancelled") return "宿主已取消目标版本安装,请检查当前版本";
3616
+ return "宿主未完成目标版本安装,请检查当前版本";
3617
+ }
3618
+ function supportsPluginUpdate(value) {
3619
+ if (!value || typeof value !== "object") return false;
3620
+ return [
3621
+ "listVersions",
3622
+ "updateBundle",
3623
+ "listBundles",
3624
+ "registries"
3625
+ ].every((key) => typeof value[key] === "function");
3626
+ }
3627
+ // src/wire-plugin-update.ts
3628
+ function payloadObject$1(value) {
3629
+ if (value === void 0 || value === null) return {
3630
+ ok: true,
3631
+ value: {}
3632
+ };
3633
+ if (typeof value !== "object" || Array.isArray(value)) return {
3634
+ ok: false,
3635
+ code: "E_PROTOCOL",
3636
+ message: "更新请求必须是对象"
3637
+ };
3638
+ return {
3639
+ ok: true,
3640
+ value
3641
+ };
3642
+ }
3643
+ const updaters = /* @__PURE__ */ new WeakMap();
3644
+ /** Bridge replacement and optional host service discovery can arrive in either order. */
3645
+ function setPluginUpdater(bridge, updater) {
3646
+ if (updater) updaters.set(bridge, updater);
3647
+ else updaters.delete(bridge);
3648
+ }
3649
+ const observers = /* @__PURE__ */ new WeakMap();
3650
+ function observe(ctx, updater, deviceId) {
3651
+ if (!ctx.canReceive("sessions.manage")) return;
3652
+ const signal = ctx.signal;
3653
+ if (signal?.aborted || signal && observers.get(signal)?.has(updater)) return;
3654
+ let stop = () => {};
3655
+ const unsubscribe = updater.subscribe(deviceId, (event) => {
3656
+ if (!ctx.canReceive("sessions.manage")) {
3657
+ stop();
3658
+ return;
3659
+ }
3660
+ ctx.send("s2c.plugin.update.progress", event);
3661
+ if (!signal && finished(event.phase)) stop();
3662
+ });
3663
+ stop = () => {
3664
+ unsubscribe();
3665
+ signal?.removeEventListener("abort", stop);
3666
+ if (signal) observers.get(signal)?.delete(updater);
3667
+ };
3668
+ if (signal) {
3669
+ let connections = observers.get(signal);
3670
+ if (!connections) {
3671
+ connections = /* @__PURE__ */ new Set();
3672
+ observers.set(signal, connections);
3673
+ }
3674
+ connections.add(updater);
3675
+ signal.addEventListener("abort", stop, { once: true });
3676
+ }
3677
+ }
3678
+ const finished = (phase) => phase === void 0 || [
3679
+ "done",
3680
+ "failed",
3681
+ "rolled-back",
3682
+ "restart-required"
3683
+ ].includes(phase);
3684
+ const pluginUpdateRows = [{
3685
+ type: "c2s.plugin.update.check",
3686
+ stage: "authenticated",
3687
+ scopes: ["sessions.read"],
3688
+ widgetPolicy: "deny",
3689
+ capability: "pluginUpdate",
3690
+ doc: "PROTOCOL.md c2s.plugin.update.check",
3691
+ validate: payloadObject$1,
3692
+ handle: async (ctx) => {
3693
+ const updater = updaters.get(ctx.bridge);
3694
+ if (!updater) return ctx.fail("E_UNSUPPORTED", "当前宿主不支持插件自更新");
3695
+ if (!ctx.deviceId) return ctx.fail("E_FORBIDDEN", "请先完成设备认证");
3696
+ observe(ctx, updater, ctx.deviceId);
3697
+ const status = await updater.check(ctx.deviceId);
3698
+ ctx.send("s2c.plugin.update.status", status);
3699
+ }
3700
+ }, {
3701
+ type: "c2s.plugin.update.apply",
3702
+ stage: "authenticated",
3703
+ scopes: ["sessions.manage"],
3704
+ widgetPolicy: "deny",
3705
+ capability: "pluginUpdate",
3706
+ doc: "PROTOCOL.md c2s.plugin.update.apply",
3707
+ validate: (value) => {
3708
+ const result = payloadObject$1(value);
3709
+ if (!result.ok) return result;
3710
+ const version = result.value.version;
3711
+ if (typeof version !== "string" || version.length > 128 || version.trim().length === 0 || !validUpdateRequestId(result.value.clientRequestId)) return {
3712
+ ok: false,
3713
+ code: "E_PROTOCOL",
3714
+ message: "更新版本或请求标识无效"
3715
+ };
3716
+ return result;
3717
+ },
3718
+ handle: async (ctx, payload) => {
3719
+ const updater = updaters.get(ctx.bridge);
3720
+ if (!updater) return ctx.fail("E_UNSUPPORTED", "当前宿主不支持插件自更新");
3721
+ if (!ctx.deviceId) return ctx.fail("E_FORBIDDEN", "请先完成设备认证");
3722
+ try {
3723
+ const record = await updater.apply(ctx.deviceId, payload.version, payload.clientRequestId);
3724
+ ctx.send("s2c.ack", {
3725
+ clientRequestId: record.clientRequestId,
3726
+ accepted: true
3727
+ });
3728
+ if (ctx.canReceive("sessions.manage")) {
3729
+ ctx.send("s2c.plugin.update.progress", {
3730
+ clientRequestId: record.clientRequestId,
3731
+ phase: record.phase,
3732
+ message: record.message
3733
+ });
3734
+ if (!finished(record.phase)) observe(ctx, updater, ctx.deviceId);
3735
+ }
3736
+ } catch (error) {
3737
+ if (error instanceof PluginUpdateError) return ctx.fail(error.code, error.message);
3738
+ ctx.fail("E_INTERNAL", "暂时无法启动插件更新,请检查当前版本");
3739
+ }
3740
+ }
3741
+ }];
3742
+ /** 第一个缺失的 scope;消息与迁移前一致(`scope <name> required`)。 */
3743
+ function scopeRejection(row, scopes) {
3744
+ for (const scope of row.scopes) if (!scopes.has(scope)) return {
3745
+ code: "E_FORBIDDEN",
3746
+ message: `scope ${scope} required`
3747
+ };
3748
+ }
3749
+ /** 能力位关闭时拒绝。校验先于本检查,与迁移前的次序一致。 */
3750
+ function capabilityRejection(row, capabilities) {
3751
+ if (row.capability === void 0) return void 0;
3752
+ if (capabilities[row.capability] === true) return void 0;
3753
+ return {
3754
+ code: "E_UNSUPPORTED",
3755
+ message: "capability unavailable on this host version"
3756
+ };
3757
+ }
3758
+ function widgetPolicyOf(row) {
3759
+ return row.widgetPolicy ?? "deny";
3760
+ }
3761
+ /** 校验一帧载荷;未登记 validate 的行原样通过。 */
3762
+ function validatePayload(row, payload) {
3763
+ if (row.validate === void 0) return {
3764
+ ok: true,
3765
+ value: payload ?? {}
3766
+ };
3767
+ return row.validate(payload);
3768
+ }
3769
+ /** 执行一帧。检查(阶段/scope/能力/校验)由 connection 按序先行完成。 */
3770
+ async function dispatchFrame(row, ctx, payload) {
3771
+ await row.handle(ctx, payload);
3772
+ }
3773
+ /**
3774
+ * 载荷必须是普通对象。此前这条判定住在 request-validation 里,对
3775
+ * `c2s.sessions.list` 等无参帧豁免、对 `c2s.sessions.archived` 却不通融
3776
+ * (同语义两种待遇,见 G2);现在豁免由各行的 validate 自己声明。
3777
+ */
3778
+ function payloadObject(value) {
3779
+ if (value === void 0 || value === null) return {
3780
+ ok: true,
3781
+ value: {}
3782
+ };
3783
+ if (typeof value !== "object" || Array.isArray(value)) return {
3784
+ ok: false,
3785
+ code: "E_PROTOCOL",
3786
+ message: "payload must be an object"
3787
+ };
3788
+ return {
3789
+ ok: true,
3790
+ value
3791
+ };
3792
+ }
3793
+ /** 字符串字段:类型正确、不超长、非空(可放宽)。 */
3794
+ function isText(value, max = 4096, nonempty = true) {
3795
+ return typeof value === "string" && value.length <= max && (!nonempty || value.trim().length > 0);
3796
+ }
3797
+ /** 可选字段:缺省合法,给出时必须满足 check。 */
3798
+ function isOptionalField(payload, key, check) {
3799
+ return payload[key] === void 0 || check(payload[key]);
2810
3800
  }
2811
3801
  function isInteger(value, min, max) {
2812
3802
  return typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max;
@@ -2828,6 +3818,7 @@ function accept(value) {
2828
3818
  const WIRE_FRAME_ROWS = [
2829
3819
  ...sessionRows,
2830
3820
  ...commandRows,
3821
+ ...pluginUpdateRows,
2831
3822
  ...scheduleRows,
2832
3823
  ...interactionRows,
2833
3824
  ...pushRows,
@@ -2837,8 +3828,7 @@ const ROWS_BY_TYPE = new Map(WIRE_FRAME_ROWS.map((row) => [row.type, row]));
2837
3828
  function registryRowFor(type) {
2838
3829
  return ROWS_BY_TYPE.get(type);
2839
3830
  }
2840
- //#endregion
2841
- //#region src/connection-gate.ts
3831
+ // src/connection-gate.ts
2842
3832
  /**
2843
3833
  * 连接门(ConnectionGate)——「一个匿名 socket 被允许做什么」的唯一属主。
2844
3834
  *
@@ -3130,6 +4120,7 @@ var ConnectionGate = class {
3130
4120
  debug: host.debug,
3131
4121
  log: (message) => host.log(message),
3132
4122
  send: (type, payload) => host.send(type, payload, env.id),
4123
+ ...host.sendBinary ? { sendBinary: (type, payload, body) => host.sendBinary(type, payload, body, env.id) } : {},
3133
4124
  fail: (code, message) => host.fail(env.id, code, message),
3134
4125
  close: (code, reason) => host.close(code, reason),
3135
4126
  canReceive: (scope) => host.canReceive(scope),
@@ -3157,8 +4148,7 @@ var ConnectionGate = class {
3157
4148
  };
3158
4149
  }
3159
4150
  };
3160
- //#endregion
3161
- //#region src/connection.ts
4151
+ // src/connection.ts
3162
4152
  /**
3163
4153
  * One connected phone. Implements BridgeSink so the HostBridge can push
3164
4154
  * projected frames and replays.
@@ -3230,6 +4220,7 @@ var BridgeConnection = class {
3230
4220
  },
3231
4221
  log: (message) => deps.log(message),
3232
4222
  send: (type, payload, id) => connection.send(type, payload, id),
4223
+ sendBinary: (type, payload, body, id) => connection.send(type, payload, id, void 0, body),
3233
4224
  fail: (id, code, message) => connection.fail(id, code, message),
3234
4225
  close: (code, reason) => connection.close(code, reason),
3235
4226
  canReceive: (scope) => connection.canReceive(scope),
@@ -3323,7 +4314,7 @@ var BridgeConnection = class {
3323
4314
  ...payload,
3324
4315
  hostAudience: this.deps.audience
3325
4316
  };
3326
- if (type === "s2c.session.event") {
4317
+ if (type === "s2c.session.event" || type === "s2c.session.queue") {
3327
4318
  const sessionId = payload?.sessionId;
3328
4319
  if (typeof sessionId === "string") {
3329
4320
  const buffered = this.openingSessionEvents.get(sessionId);
@@ -3399,7 +4390,8 @@ var BridgeConnection = class {
3399
4390
  this.ws.terminate();
3400
4391
  }
3401
4392
  }
3402
- send(type, payload, id, seq) {
4393
+ /** `body` 存在时按二进制帧发送(见 binary-frame.ts),头部仍是同一个 envelope。 */
4394
+ send(type, payload, id, seq, body) {
3403
4395
  const envelope = {
3404
4396
  v: 2,
3405
4397
  type,
@@ -3413,7 +4405,7 @@ var BridgeConnection = class {
3413
4405
  this.close(1013, "client too slow");
3414
4406
  return;
3415
4407
  }
3416
- this.ws.send(JSON.stringify(envelope));
4408
+ this.ws.send(body === void 0 ? JSON.stringify(envelope) : encodeBinaryFrame(envelope, body));
3417
4409
  }
3418
4410
  fail(id, code, message) {
3419
4411
  this.send("s2c.error", {
@@ -3447,8 +4439,30 @@ var BridgeConnection = class {
3447
4439
  function normalizeFunnelConnectionLimit(value) {
3448
4440
  return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 16 ? value : 8;
3449
4441
  }
3450
- //#endregion
3451
- //#region src/remote-supervisor.ts
4442
+ // src/tailscale-version.ts
4443
+ /**
4444
+ * 内嵌 helper 上报的 tailscale.com 模块版本。
4445
+ *
4446
+ * 版本的唯一来源是 helper 启动事件(`helper/main.go` 读 Go 构建信息里的
4447
+ * `tailscale.com` 依赖版本),不是 go.mod、也不是插件常量:用户可能通过
4448
+ * `remote.helperPath` 换成自己编译的 helper,界面必须显示实际在跑的那一个。
4449
+ *
4450
+ * 两条边界都要守卫:
4451
+ * - 宿主侧 `parseHelperEvent` 把 helper 的 stdout 当成不可信输入,格式不对
4452
+ * 就丢弃,避免把任意文本渲染进设置页;
4453
+ * - 设置页侧 `reportSchema` 同样校验 Host RPC 返回值,旧 Host 不带该字段
4454
+ * 时保持 undefined(页面显示「未获取」),不当作错误。
4455
+ *
4456
+ * 接受 Go 模块版本形态:`v1.104.2`、`v1.104.2-pre.1`、
4457
+ * `v0.0.0-20260101000000-abcdef123456`。长度上限防止异常输入撑爆界面。
4458
+ */
4459
+ const TAILSCALE_VERSION_PATTERN = /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]{1,64})?$/;
4460
+ const MAX_TAILSCALE_VERSION_LENGTH = 80;
4461
+ /** 判断外部输入是否是合法的 Tailscale 模块版本字符串。 */
4462
+ function isTailscaleVersion(value) {
4463
+ return typeof value === "string" && value.length <= MAX_TAILSCALE_VERSION_LENGTH && TAILSCALE_VERSION_PATTERN.test(value);
4464
+ }
4465
+ // src/remote-supervisor.ts
3452
4466
  const RESTART_DELAYS_MS = [
3453
4467
  1e3,
3454
4468
  2e3,
@@ -3636,7 +4650,8 @@ function parseHelperEvent(line) {
3636
4650
  phase: value.phase,
3637
4651
  ...typeof value.publicURL === "string" ? { publicURL: value.publicURL } : {},
3638
4652
  ...typeof value.authURL === "string" ? { authURL: value.authURL } : {},
3639
- ...typeof value.message === "string" ? { message: value.message.slice(0, 500) } : {}
4653
+ ...typeof value.message === "string" ? { message: value.message.slice(0, 500) } : {},
4654
+ ...isTailscaleVersion(value.tailscaleVersion) ? { tailscaleVersion: value.tailscaleVersion } : {}
3640
4655
  };
3641
4656
  } catch {
3642
4657
  return null;
@@ -3876,8 +4891,7 @@ var RemoteSupervisor = class {
3876
4891
  else if (next.phase === "error" || next.phase === "unavailable") this.options.log(`remote Funnel ${next.phase}: ${next.message ?? "unknown error"}`);
3877
4892
  }
3878
4893
  };
3879
- //#endregion
3880
- //#region src/local-policy.ts
4894
+ // src/local-policy.ts
3881
4895
  const DEFAULT_LOCAL_PORT = 3098;
3882
4896
  const MIN_LOCAL_PORT = 1024;
3883
4897
  const MAX_LOCAL_PORT = 65535;
@@ -3895,8 +4909,7 @@ function localListenError(error, port) {
3895
4909
  if (code === "EACCES") return `permission denied while opening local port ${port}`;
3896
4910
  return error instanceof Error ? error.message : String(error);
3897
4911
  }
3898
- //#endregion
3899
- //#region src/config.ts
4912
+ // src/config.ts
3900
4913
  /** Operator-run relay used by distributed builds; overridable via config. */
3901
4914
  const DEFAULT_RELAY_URL = "https://pilot.hailab.dev";
3902
4915
  const Config = z.object({
@@ -3985,8 +4998,7 @@ function normalizeOptions(options) {
3985
4998
  if (options && typeof options === "object") return plainConfig(options);
3986
4999
  return plainConfig(Config(void 0) ?? {});
3987
5000
  }
3988
- //#endregion
3989
- //#region src/apns.ts
5001
+ // src/apns.ts
3990
5002
  /**
3991
5003
  * Minimal APNs provider client (HTTP/2) with zero npm dependencies.
3992
5004
  *
@@ -4223,8 +5235,7 @@ var ApnsClient = class {
4223
5235
  return token.length <= 10 ? "…" : token.slice(0, 6) + "…" + token.slice(-4);
4224
5236
  }
4225
5237
  };
4226
- //#endregion
4227
- //#region src/relay-url.ts
5238
+ // src/relay-url.ts
4228
5239
  /**
4229
5240
  * Normalize an operator-configured relay URL without a backtracking regular
4230
5241
  * expression. Scanning from the end keeps even pathological inputs linear.
@@ -4235,8 +5246,7 @@ function normalizeRelayBaseUrl(value) {
4235
5246
  while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end -= 1;
4236
5247
  return end === trimmed.length ? trimmed : trimmed.slice(0, end);
4237
5248
  }
4238
- //#endregion
4239
- //#region src/relay-client.ts
5249
+ // src/relay-client.ts
4240
5250
  var RelayClient = class {
4241
5251
  base;
4242
5252
  token;
@@ -4329,8 +5339,7 @@ var RelayClient = class {
4329
5339
  }
4330
5340
  }
4331
5341
  };
4332
- //#endregion
4333
- //#region src/widget-push.ts
5342
+ // src/widget-push.ts
4334
5343
  /** Coalesce bursts and bound continuous updates, without starving the trailing
4335
5344
  * state. One scheduler per host, not per token or per streaming text frame. */
4336
5345
  var WidgetPushScheduler = class {
@@ -4367,8 +5376,7 @@ var WidgetPushScheduler = class {
4367
5376
  this.timer = void 0;
4368
5377
  }
4369
5378
  };
4370
- //#endregion
4371
- //#region src/live-activity.ts
5379
+ // src/live-activity.ts
4372
5380
  function liveActivityState(session) {
4373
5381
  const total = Math.max(0, session?.todos?.total ?? session?.todoItems?.length ?? 0);
4374
5382
  const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
@@ -4440,8 +5448,7 @@ var LiveActivityPushManager = class {
4440
5448
  this.scheduler.dispose();
4441
5449
  }
4442
5450
  };
4443
- //#endregion
4444
- //#region src/push-policy.ts
5451
+ // src/push-policy.ts
4445
5452
  /** Prune only when the provider supplies an authoritative token-lifecycle verdict. */
4446
5453
  function shouldPrunePushToken(outcome, reason) {
4447
5454
  return outcome === "invalid-token" && (reason === "Unregistered" || reason === "ExpiredToken");
@@ -4477,8 +5484,7 @@ function pushContent(notification, mode) {
4477
5484
  body: bodies[notification.category] ?? "Open DeepPilot for an update."
4478
5485
  };
4479
5486
  }
4480
- //#endregion
4481
- //#region src/relay-test.ts
5487
+ // src/relay-test.ts
4482
5488
  /**
4483
5489
  * Relay connectivity self-test used by the settings page "测试中继" button.
4484
5490
  *
@@ -4604,8 +5610,7 @@ async function runRelayProbe(options) {
4604
5610
  steps
4605
5611
  };
4606
5612
  }
4607
- //#endregion
4608
- //#region src/push-gateway.ts
5613
+ // src/push-gateway.ts
4609
5614
  /**
4610
5615
  * 推送网关(PushGateway)——离线推送的单一属主。
4611
5616
  *
@@ -5095,8 +6100,7 @@ var PushGateway = class {
5095
6100
  await this.ensureRelayEnrolled(url);
5096
6101
  }
5097
6102
  };
5098
- //#endregion
5099
- //#region src/phone-server.ts
6103
+ // src/phone-server.ts
5100
6104
  /**
5101
6105
  * A deliberately narrow transport listener. Both the LAN endpoint and the
5102
6106
  * loopback-only Funnel origin use this factory, so neither can accidentally
@@ -5175,8 +6179,7 @@ function closeServer(server) {
5175
6179
  if (server === void 0 || !server.listening) return Promise.resolve();
5176
6180
  return new Promise((resolve) => server.close(() => resolve()));
5177
6181
  }
5178
- //#endregion
5179
- //#region src/transport-reconciler.ts
6182
+ // src/transport-reconciler.ts
5180
6183
  var TransportReconciler = class {
5181
6184
  spec;
5182
6185
  config;
@@ -5404,8 +6407,7 @@ function createRemoteTransport(deps) {
5404
6407
  statusOf: (supervisor) => supervisor?.status() ?? fallbackStatus()
5405
6408
  }, deps.config, deps.log, fallbackStatus());
5406
6409
  }
5407
- //#endregion
5408
- //#region src/dispatch-journal.ts
6410
+ // src/dispatch-journal.ts
5409
6411
  /**
5410
6412
  * at-most-once dispatch journal:prompt 投递与非 prompt 变更的**同一个** module。
5411
6413
  *
@@ -5837,8 +6839,7 @@ function scheduleMutationCodec(persistValues) {
5837
6839
  }))
5838
6840
  };
5839
6841
  }
5840
- //#endregion
5841
- //#region src/notification-text.ts
6842
+ // src/notification-text.ts
5842
6843
  /**
5843
6844
  * 通知正文的纯文本投影。
5844
6845
  *
@@ -5881,8 +6882,7 @@ function decodePercentRuns(text) {
5881
6882
  }
5882
6883
  });
5883
6884
  }
5884
- //#endregion
5885
- //#region src/host-api.ts
6885
+ // src/host-api.ts
5886
6886
  /**
5887
6887
  * Subagent sessions are host-internal workers of a parent conversation.
5888
6888
  * They must never surface on the phone: not in the project/session list,
@@ -5905,8 +6905,12 @@ function unwrapStreamItem(item) {
5905
6905
  } : nested;
5906
6906
  return item;
5907
6907
  }
5908
- //#endregion
5909
- //#region src/host-capabilities.ts
6908
+ // src/host-capabilities.ts
6909
+ const pluginUpdateProbes = /* @__PURE__ */ new WeakMap();
6910
+ function setPluginUpdateProbe(proxy, probe) {
6911
+ if (probe) pluginUpdateProbes.set(proxy, probe);
6912
+ else pluginUpdateProbes.delete(proxy);
6913
+ }
5910
6914
  /** 每位一行:Host 上必须存在的依赖。缺失即该位为 false,相关帧回 E_UNSUPPORTED。 */
5911
6915
  const HOST_CAPABILITY_PROBES = {
5912
6916
  models: (proxy) => typeof proxy.sessions.models === "function",
@@ -5916,7 +6920,12 @@ const HOST_CAPABILITY_PROBES = {
5916
6920
  sessionFork: (proxy) => typeof proxy.sessions.fork === "function",
5917
6921
  sessionRestore: (proxy) => typeof proxy.workspace?.unarchiveSession === "function",
5918
6922
  projectSelection: (proxy) => typeof proxy.workspace?.list === "function" && typeof proxy.workspace?.create === "function",
5919
- schedules: (proxy) => proxy.schedule !== void 0
6923
+ schedules: (proxy) => proxy.schedule !== void 0,
6924
+ schedulesHost: (proxy) => proxy.schedule?.mode === "host",
6925
+ promptQueue: (proxy) => typeof proxy.sessions.updateQueue === "function",
6926
+ sessionMarkdown: (proxy) => proxy.commands?.markdownAvailable === true && typeof proxy.commands?.export === "function",
6927
+ assistantStream: (proxy) => proxy.assistantStreamAvailable === true,
6928
+ pluginUpdate: (proxy) => pluginUpdateProbes.get(proxy)?.() === true
5920
6929
  };
5921
6930
  /**
5922
6931
  * 按当前 apiProxy 与推送出口算出 welcome 能力位。`push` 位描述「当前离线推送
@@ -5926,6 +6935,7 @@ function capabilityBits(proxy, pushOutlet) {
5926
6935
  return {
5927
6936
  historyPaging: true,
5928
6937
  replay: true,
6938
+ binaryFrames: true,
5929
6939
  approvals: true,
5930
6940
  questions: true,
5931
6941
  pendingSnapshot: true,
@@ -5939,66 +6949,327 @@ function capabilityBits(proxy, pushOutlet) {
5939
6949
  sessionRestore: HOST_CAPABILITY_PROBES.sessionRestore(proxy),
5940
6950
  projectSelection: HOST_CAPABILITY_PROBES.projectSelection(proxy),
5941
6951
  schedules: HOST_CAPABILITY_PROBES.schedules(proxy),
6952
+ schedulesHost: HOST_CAPABILITY_PROBES.schedulesHost(proxy),
6953
+ promptQueue: HOST_CAPABILITY_PROBES.promptQueue(proxy),
6954
+ sessionMarkdown: HOST_CAPABILITY_PROBES.sessionMarkdown(proxy),
6955
+ assistantStream: HOST_CAPABILITY_PROBES.assistantStream(proxy),
6956
+ pluginUpdate: HOST_CAPABILITY_PROBES.pluginUpdate(proxy),
5942
6957
  push: pushOutlet?.isAvailable() === true,
5943
6958
  widgetPush: true,
5944
6959
  liveActivityPush: true,
5945
6960
  deviceRevoke: true
5946
6961
  };
5947
6962
  }
5948
- //#endregion
5949
- //#region src/host-bridge.ts
5950
- const MAX_RING_DEFAULT = 2e3;
6963
+ // src/prompt-queue.ts
5951
6964
  /**
5952
- * Process-wide bridge state: session mirror, pending approvals/questions,
5953
- * and the per-device replay ring. Consumes the in-process mux/host streams
5954
- * and fans projected pushes out to every registered sink.
6965
+ * 运行中排队与转向(PROTOCOL.md「运行中排队与转向扩展」)的纯函数部分。
6966
+ *
6967
+ * 职责:
6968
+ * - Host `requestId` 与 wire `clientSendId` 的互转:Bridge 发送带 clientSendId 的
6969
+ * prompt 时以 `dp:` 前缀拼出确定的 requestId,Host 把它记为用户来源的 `rpcId`;
6970
+ * 快照时反向剥掉前缀,发起设备即可把待发送项与队列项对上。
6971
+ * - 把 DSH `inbox` 投影(`{ "next-turn": Message[], "next-step": Message[] }`,
6972
+ * 来源是持久化的 `agent/inbox/spliced` 事件)折叠成 `s2c.session.queue` 的条目。
6973
+ *
6974
+ * 不持有状态:缓存、去重与推送时机在 host-bridge。
5955
6975
  */
5956
- let BRIDGE_SEQ = 0;
5957
- var HostBridge = class {
5958
- apiProxy;
5959
- historyBufferMax;
5960
- log;
5961
- id = ++BRIDGE_SEQ;
5962
- summaries = /* @__PURE__ */ new Map();
5963
- activeTools = /* @__PURE__ */ new Map();
5964
- approvals = /* @__PURE__ */ new Map();
5965
- questions = /* @__PURE__ */ new Map();
5966
- archivedSessionIds = /* @__PURE__ */ new Set();
5967
- /** Mirrors archived rows from the last sessions.list so the phone can browse
5968
- * and restore them. Excluded from `summaries` and from the live broadcast. */
5969
- archivedSummaries = /* @__PURE__ */ new Map();
5970
- subagentSessionIds = /* @__PURE__ */ new Set();
5971
- sinks = /* @__PURE__ */ new Set();
5972
- ring = [];
5973
- cursor = 0;
5974
- userReceiptSeq = 0;
5975
- abort = new AbortController();
5976
- started = false;
5977
- disposed = false;
5978
- promptDeliveries;
5979
- scheduleMutations;
5980
- forkMutations;
5981
- constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath, scheduleJournalPath, forkJournalPath, log) {
5982
- this.apiProxy = apiProxy;
5983
- this.historyBufferMax = historyBufferMax;
5984
- this.log = log;
5985
- this.promptDeliveries = openDispatchJournal({
5986
- path: deliveryJournalPath,
5987
- codec: promptDeliveryCodec,
5988
- joinInFlight: true,
5989
- maxFileBytes: 8388608,
5990
- log: this.log
5991
- });
5992
- this.scheduleMutations = openDispatchJournal({
5993
- path: scheduleJournalPath,
5994
- codec: scheduleMutationCodec(false),
5995
- joinInFlight: false,
5996
- maxFileBytes: 4194304,
5997
- log: this.log
5998
- });
5999
- this.forkMutations = openDispatchJournal({
6000
- path: forkJournalPath,
6001
- codec: scheduleMutationCodec(true),
6976
+ const REQUEST_ID_PREFIX = "dp:";
6977
+ /** 快照正文上限(Unicode 码点)。只用于列表展示,完整正文仍在 Host 日志里。 */
6978
+ const MAX_QUEUE_TEXT_CODE_POINTS = 4096;
6979
+ /** clientSendId -> Host requestId。 */
6980
+ function promptRequestId(clientSendId) {
6981
+ return REQUEST_ID_PREFIX + clientSendId;
6982
+ }
6983
+ /** Host 来源 rpcId -> clientSendId;不是本 Bridge 用 clientSendId 发出的返回 undefined。 */
6984
+ function clientSendIdOf(rpcId) {
6985
+ if (typeof rpcId !== "string" || !rpcId.startsWith(REQUEST_ID_PREFIX)) return void 0;
6986
+ const id = rpcId.slice(3);
6987
+ return validSendId(id) ? id : void 0;
6988
+ }
6989
+ /**
6990
+ * inbox 投影 -> 队列条目,保持 Host 的先进先出顺序:先 `next-turn`(排队),
6991
+ * 再 `next-step`(转向)。值形状不对时返回 undefined,调用方据此不推送,
6992
+ * 而不是把「读不懂」当成「队列为空」——客户端会把消失的项当作已发出。
6993
+ */
6994
+ function projectQueue(value) {
6995
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
6996
+ const inbox = value;
6997
+ const nextTurn = inbox["next-turn"];
6998
+ const nextStep = inbox["next-step"];
6999
+ if (!Array.isArray(nextTurn) || !Array.isArray(nextStep)) return void 0;
7000
+ const items = [];
7001
+ for (const [list, placement] of [[nextTurn, "queued"], [nextStep, "steering"]]) for (const message of list) {
7002
+ const item = queueItemOf(message, placement);
7003
+ if (item) items.push(item);
7004
+ }
7005
+ return items;
7006
+ }
7007
+ function queueItemOf(message, placement) {
7008
+ if (message === null || typeof message !== "object" || typeof message.id !== "string" || message.id.length === 0) return;
7009
+ const texts = [];
7010
+ let imageCount = 0;
7011
+ let documentCount = 0;
7012
+ for (const block of Array.isArray(message.content) ? message.content : []) {
7013
+ if (block === null || typeof block !== "object") continue;
7014
+ if (block.type === "image") imageCount += 1;
7015
+ else if (block.type === "text" && typeof block.text === "string") {
7016
+ if (projectedDocument(block.text) !== void 0) documentCount += 1;
7017
+ else texts.push(block.text);
7018
+ }
7019
+ }
7020
+ const codePoints = Array.from(texts.join("\n"));
7021
+ const truncated = codePoints.length > MAX_QUEUE_TEXT_CODE_POINTS;
7022
+ const source = message.source;
7023
+ const clientSendId = source?.kind === "user" ? clientSendIdOf(source.rpcId) : void 0;
7024
+ return {
7025
+ itemId: message.id,
7026
+ placement,
7027
+ text: truncated ? codePoints.slice(0, MAX_QUEUE_TEXT_CODE_POINTS).join("") : codePoints.join(""),
7028
+ ...truncated ? { truncated: true } : {},
7029
+ imageCount,
7030
+ documentCount,
7031
+ ...clientSendId !== void 0 ? { clientSendId } : {}
7032
+ };
7033
+ }
7034
+ /** Bound by serialized UTF-8 size, including escaping, without splitting a Unicode code point. */
7035
+ function bounded(payload) {
7036
+ if (Buffer.byteLength(JSON.stringify(payload)) <= 262144) return payload;
7037
+ const points = Array.from(payload.text);
7038
+ let low = 0;
7039
+ let high = points.length;
7040
+ while (low < high) {
7041
+ const mid = Math.ceil((low + high) / 2);
7042
+ const candidate = {
7043
+ ...payload,
7044
+ text: points.slice(0, mid).join(""),
7045
+ truncated: true
7046
+ };
7047
+ if (Buffer.byteLength(JSON.stringify(candidate)) <= 262144) low = mid;
7048
+ else high = mid - 1;
7049
+ }
7050
+ return {
7051
+ ...payload,
7052
+ text: points.slice(0, low).join(""),
7053
+ truncated: true
7054
+ };
7055
+ }
7056
+ function isFrame(value) {
7057
+ if (!value || typeof value !== "object") return false;
7058
+ const frame = value;
7059
+ if (typeof frame.attemptId !== "string" || !frame.attemptId || !Number.isSafeInteger(frame.revision)) return false;
7060
+ if (frame.type === "start") return Number.isSafeInteger(frame.turn) && Number.isSafeInteger(frame.step);
7061
+ if (frame.type !== "chunk" && frame.type !== "end") return false;
7062
+ if (!Number.isSafeInteger(frame.index) || frame.index < 0) return false;
7063
+ if (frame.type === "chunk") return Number.isSafeInteger(frame.time) && !!frame.chunk && typeof frame.chunk.type === "string";
7064
+ return frame.outcome?.kind === "abandoned" || frame.outcome?.kind === "committed" && ["assistant/message", "assistant/attempt"].includes(frame.outcome.eventType) && Number.isSafeInteger(frame.outcome.seq);
7065
+ }
7066
+ /**
7067
+ * One process-local attempt per session. Only the small pending window retains text;
7068
+ * the durable session event remains the source of truth for the completed reply.
7069
+ */
7070
+ var AssistantStream = class {
7071
+ emit;
7072
+ isViewed;
7073
+ attempts = /* @__PURE__ */ new Map();
7074
+ constructor(emit, isViewed) {
7075
+ this.emit = emit;
7076
+ this.isViewed = isViewed;
7077
+ }
7078
+ accept(sessionId, value) {
7079
+ if (!sessionId || !isFrame(value)) return;
7080
+ const frame = value;
7081
+ let state = this.attempts.get(sessionId);
7082
+ if (frame.type === "start") {
7083
+ if (state?.attemptId === frame.attemptId && frame.revision <= state.revision) return;
7084
+ this.abandon(sessionId);
7085
+ state = {
7086
+ attemptId: frame.attemptId,
7087
+ streamId: randomUUID(),
7088
+ revision: frame.revision,
7089
+ nextIndex: 0,
7090
+ turn: frame.turn,
7091
+ step: frame.step,
7092
+ pending: [],
7093
+ pendingBytes: 0
7094
+ };
7095
+ this.attempts.set(sessionId, state);
7096
+ this.emit({
7097
+ sessionId,
7098
+ streamId: state.streamId,
7099
+ kind: "stream.start",
7100
+ text: "",
7101
+ ts: Date.now()
7102
+ });
7103
+ return;
7104
+ }
7105
+ if (!state || state.attemptId !== frame.attemptId || frame.revision <= state.revision || frame.index < state.nextIndex) return;
7106
+ state.revision = frame.revision;
7107
+ if (frame.type === "end") {
7108
+ if (frame.outcome.kind === "committed") this.flush(sessionId);
7109
+ this.clearPending(sessionId);
7110
+ this.attempts.delete(sessionId);
7111
+ this.emit({
7112
+ sessionId,
7113
+ streamId: state.streamId,
7114
+ kind: "stream.end",
7115
+ text: "",
7116
+ ts: Date.now(),
7117
+ outcome: frame.outcome
7118
+ });
7119
+ return;
7120
+ }
7121
+ state.nextIndex = frame.index + 1;
7122
+ if (state.finalSeq !== void 0 || !this.isViewed(sessionId)) return;
7123
+ const kind = frame.chunk.type === "text-delta" ? "message.delta" : frame.chunk.type === "reasoning-delta" ? "thinking.delta" : void 0;
7124
+ if (!kind || typeof frame.chunk.text !== "string" || frame.chunk.text.length === 0) return;
7125
+ const previous = state.pending.at(-1)?.kind === kind ? state.pending.at(-1) : void 0;
7126
+ if (previous?.truncated) return;
7127
+ const payload = bounded({
7128
+ sessionId,
7129
+ streamId: state.streamId,
7130
+ kind,
7131
+ text: (previous?.text ?? "") + frame.chunk.text,
7132
+ ts: frame.time
7133
+ });
7134
+ const next = {
7135
+ kind,
7136
+ text: payload.text,
7137
+ ts: payload.ts,
7138
+ ...payload.truncated ? { truncated: true } : {}
7139
+ };
7140
+ const oldBytes = previous ? Buffer.byteLength(JSON.stringify(previous)) : 0;
7141
+ const newBytes = Buffer.byteLength(JSON.stringify(next));
7142
+ if (state.pendingBytes - oldBytes + newBytes > 524288) this.flush(sessionId);
7143
+ if (previous && state.pending.at(-1) === previous) {
7144
+ state.pending[state.pending.length - 1] = next;
7145
+ state.pendingBytes -= oldBytes;
7146
+ } else if (previous) {
7147
+ const tail = bounded({
7148
+ sessionId,
7149
+ streamId: state.streamId,
7150
+ kind,
7151
+ text: frame.chunk.text,
7152
+ ts: frame.time
7153
+ });
7154
+ next.text = tail.text;
7155
+ next.truncated = tail.truncated ? true : void 0;
7156
+ state.pending.push(next);
7157
+ } else state.pending.push(next);
7158
+ state.pendingBytes += Buffer.byteLength(JSON.stringify(next));
7159
+ state.timer ??= setTimeout(() => this.flush(sessionId), 50);
7160
+ state.timer.unref?.();
7161
+ }
7162
+ /** The Host appends the durable event BEFORE publishing committed end. */
7163
+ beforeEvent(sessionId, event) {
7164
+ const state = this.attempts.get(sessionId);
7165
+ if (!state || event.type !== "assistant/message" && event.type !== "assistant/attempt") return void 0;
7166
+ const data = event.data;
7167
+ if (data?.turn !== state.turn || data?.step !== state.step) return void 0;
7168
+ this.flush(sessionId);
7169
+ state.finalSeq = event.seq;
7170
+ return state.streamId;
7171
+ }
7172
+ flush(sessionId) {
7173
+ const state = this.attempts.get(sessionId);
7174
+ if (!state) return;
7175
+ if (state.timer) clearTimeout(state.timer);
7176
+ state.timer = void 0;
7177
+ const pending = state.pending;
7178
+ state.pending = [];
7179
+ state.pendingBytes = 0;
7180
+ for (const text of pending) this.emit({
7181
+ sessionId,
7182
+ streamId: state.streamId,
7183
+ ...text
7184
+ });
7185
+ }
7186
+ /** Closing the last viewer must not deliver its buffered text on a later open. */
7187
+ clearPending(sessionId) {
7188
+ const state = this.attempts.get(sessionId);
7189
+ if (!state) return;
7190
+ if (state.timer) clearTimeout(state.timer);
7191
+ state.timer = void 0;
7192
+ state.pending = [];
7193
+ state.pendingBytes = 0;
7194
+ }
7195
+ abandon(sessionId) {
7196
+ const state = this.attempts.get(sessionId);
7197
+ if (!state) return;
7198
+ this.clearPending(sessionId);
7199
+ this.attempts.delete(sessionId);
7200
+ this.emit({
7201
+ sessionId,
7202
+ streamId: state.streamId,
7203
+ kind: "stream.end",
7204
+ text: "",
7205
+ ts: Date.now(),
7206
+ outcome: { kind: "abandoned" }
7207
+ });
7208
+ }
7209
+ dispose() {
7210
+ for (const sessionId of this.attempts.keys()) this.clearPending(sessionId);
7211
+ this.attempts.clear();
7212
+ }
7213
+ };
7214
+ // src/host-bridge.ts
7215
+ const MAX_RING_DEFAULT = 2e3;
7216
+ /**
7217
+ * Process-wide bridge state: session mirror, pending approvals/questions,
7218
+ * and the per-device replay ring. Consumes the in-process mux/host streams
7219
+ * and fans projected pushes out to every registered sink.
7220
+ */
7221
+ let BRIDGE_SEQ = 0;
7222
+ var HostBridge = class {
7223
+ apiProxy;
7224
+ historyBufferMax;
7225
+ log;
7226
+ id = ++BRIDGE_SEQ;
7227
+ summaries = /* @__PURE__ */ new Map();
7228
+ activeTools = /* @__PURE__ */ new Map();
7229
+ approvals = /* @__PURE__ */ new Map();
7230
+ questions = /* @__PURE__ */ new Map();
7231
+ archivedSessionIds = /* @__PURE__ */ new Set();
7232
+ /** Mirrors archived rows from the last sessions.list so the phone can browse
7233
+ * and restore them. Excluded from `summaries` and from the live broadcast. */
7234
+ archivedSummaries = /* @__PURE__ */ new Map();
7235
+ subagentSessionIds = /* @__PURE__ */ new Set();
7236
+ /**
7237
+ * 每个会话最近一次已知的队列(inbox 投影的折叠结果)及其指纹。来源是实时
7238
+ * 投影通知或打开会话时读到的基线;指纹用来丢掉内容没变的重复通知。
7239
+ */
7240
+ queues = /* @__PURE__ */ new Map();
7241
+ sinks = /* @__PURE__ */ new Set();
7242
+ assistantStream = new AssistantStream((payload) => this.pushAssistantDelta(payload), (sessionId) => this.hasStreamViewer(sessionId));
7243
+ ring = [];
7244
+ cursor = 0;
7245
+ userReceiptSeq = 0;
7246
+ abort = new AbortController();
7247
+ started = false;
7248
+ disposed = false;
7249
+ promptDeliveries;
7250
+ scheduleMutations;
7251
+ forkMutations;
7252
+ constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath, scheduleJournalPath, forkJournalPath, log) {
7253
+ this.apiProxy = apiProxy;
7254
+ this.historyBufferMax = historyBufferMax;
7255
+ this.log = log;
7256
+ this.promptDeliveries = openDispatchJournal({
7257
+ path: deliveryJournalPath,
7258
+ codec: promptDeliveryCodec,
7259
+ joinInFlight: true,
7260
+ maxFileBytes: 8388608,
7261
+ log: this.log
7262
+ });
7263
+ this.scheduleMutations = openDispatchJournal({
7264
+ path: scheduleJournalPath,
7265
+ codec: scheduleMutationCodec(false),
7266
+ joinInFlight: false,
7267
+ maxFileBytes: 4194304,
7268
+ log: this.log
7269
+ });
7270
+ this.forkMutations = openDispatchJournal({
7271
+ path: forkJournalPath,
7272
+ codec: scheduleMutationCodec(true),
6002
7273
  joinInFlight: false,
6003
7274
  maxFileBytes: 4194304,
6004
7275
  log: this.log
@@ -6032,6 +7303,7 @@ var HostBridge = class {
6032
7303
  }
6033
7304
  removeSink(sink) {
6034
7305
  this.sinks.delete(sink);
7306
+ this.dropSinkSessions(sink);
6035
7307
  }
6036
7308
  /** Whether the ring still holds everything after the cursor. */
6037
7309
  canResumeFrom(cursor) {
@@ -6051,9 +7323,21 @@ var HostBridge = class {
6051
7323
  }
6052
7324
  markSinkClosed(sink, sessionId) {
6053
7325
  this.sinkSessions.get(sink)?.delete(sessionId);
7326
+ if (!this.hasStreamViewer(sessionId)) this.assistantStream.clearPending(sessionId);
6054
7327
  }
6055
7328
  dropSinkSessions(sink) {
7329
+ const sessions = this.sinkSessions.get(sink);
6056
7330
  this.sinkSessions.delete(sink);
7331
+ for (const sessionId of sessions ?? []) if (!this.hasStreamViewer(sessionId)) this.assistantStream.clearPending(sessionId);
7332
+ }
7333
+ hasStreamViewer(sessionId) {
7334
+ if (this.disposed || this.subagentSessionIds.has(sessionId)) return false;
7335
+ return [...this.sinks].some((sink) => this.isViewedBy(sink, sessionId) && sink.canReceive("sessions.read"));
7336
+ }
7337
+ /** Instant publications bypass record(): no cursor, ring, notification or push side effect. */
7338
+ pushAssistantDelta(payload) {
7339
+ if (this.disposed || this.subagentSessionIds.has(payload.sessionId)) return;
7340
+ for (const sink of this.sinks) if (this.isViewedBy(sink, payload.sessionId) && sink.canReceive("sessions.read")) sink.push("s2c.session.delta", payload);
6057
7341
  }
6058
7342
  isViewedBy(sink, sessionId) {
6059
7343
  return this.sinkSessions.get(sink)?.has(sessionId) ?? false;
@@ -6200,6 +7484,7 @@ var HostBridge = class {
6200
7484
  if (this.disposed) return;
6201
7485
  this.disposed = true;
6202
7486
  this.abort.abort();
7487
+ this.assistantStream.dispose();
6203
7488
  this.sinks.clear();
6204
7489
  this.sinkSessions.clear();
6205
7490
  this.pushOutlet = void 0;
@@ -6218,7 +7503,9 @@ var HostBridge = class {
6218
7503
  const frame = unwrapStreamItem(item);
6219
7504
  this.onMuxFrame(frame);
6220
7505
  }
6221
- } catch {}
7506
+ } catch {} finally {
7507
+ this.assistantStream.dispose();
7508
+ }
6222
7509
  }
6223
7510
  onHostFrame(frame) {
6224
7511
  switch (frame.type) {
@@ -6257,12 +7544,30 @@ var HostBridge = class {
6257
7544
  case "commands/changed":
6258
7545
  this.record("s2c.commands.changed", {});
6259
7546
  break;
7547
+ case "session/assistant-stream": {
7548
+ const sessionId = String(frame.sessionId ?? "");
7549
+ if (frame.isSubagent === true) this.subagentSessionIds.add(sessionId);
7550
+ if (this.subagentSessionIds.has(sessionId)) {
7551
+ this.assistantStream.abandon(sessionId);
7552
+ break;
7553
+ }
7554
+ this.assistantStream.accept(sessionId, frame.value);
7555
+ break;
7556
+ }
7557
+ case "session/assistant-stream-disposed":
7558
+ this.assistantStream.abandon(String(frame.sessionId ?? ""));
7559
+ break;
6260
7560
  case "session/event": {
6261
7561
  const event = frame.event;
6262
7562
  const sessionId = String(frame.sessionId ?? "");
6263
7563
  if (!event || !sessionId) break;
6264
7564
  if (frame.isSubagent === true) this.subagentSessionIds.add(sessionId);
6265
- if (this.subagentSessionIds.has(sessionId)) break;
7565
+ if (this.subagentSessionIds.has(sessionId)) {
7566
+ this.assistantStream.abandon(sessionId);
7567
+ break;
7568
+ }
7569
+ const streamId = this.assistantStream.beforeEvent(sessionId, event);
7570
+ if (event.type === "turn/end") this.assistantStream.abandon(sessionId);
6266
7571
  if (event.type === "agent-preset/selected") this.record("s2c.skills.changed", { sessionId });
6267
7572
  this.noteActivity(sessionId, event);
6268
7573
  this.captureAssistantText(sessionId, event);
@@ -6272,7 +7577,10 @@ var HostBridge = class {
6272
7577
  sessionId,
6273
7578
  kind: projection.kind,
6274
7579
  seq: event.seq,
6275
- data: projection.data
7580
+ data: {
7581
+ ...projection.data,
7582
+ ...streamId && projection.kind === "message.final" ? { streamId } : {}
7583
+ }
6276
7584
  });
6277
7585
  if (projection?.kind === "turn.end") this.emitTurnCompletedNotify(sessionId, projection.data.ok === true);
6278
7586
  break;
@@ -6293,6 +7601,7 @@ var HostBridge = class {
6293
7601
  ...current ? { current } : {}
6294
7602
  });
6295
7603
  }
7604
+ if (p.key === "inbox") this.applyInbox(p.sessionId, p.value, "live");
6296
7605
  this.applyProjection(p.sessionId, String(p.key ?? ""), p.value);
6297
7606
  break;
6298
7607
  }
@@ -6468,6 +7777,43 @@ var HostBridge = class {
6468
7777
  row.lastActivityTs = Date.now();
6469
7778
  this.pushSummary(row);
6470
7779
  }
7780
+ /**
7781
+ * 折叠一次 inbox 投影。只有实时变化才广播 s2c.session.queue;基线只补缓存,
7782
+ * 打开会话的那台设备另由 pushQueueSnapshot 点对点拿到快照。
7783
+ *
7784
+ * 基线只补缺不覆盖:实时通知带的是完整投影值,收到过就以它为准;基线读取与
7785
+ * 实时变化并发时,较早算出的基线不得盖掉较新的实时值。
7786
+ */
7787
+ applyInbox(sessionId, value, origin) {
7788
+ if (this.capabilities.promptQueue !== true) return;
7789
+ if (this.subagentSessionIds.has(sessionId)) return;
7790
+ if (origin === "baseline" && this.queues.has(sessionId)) return;
7791
+ const items = projectQueue(value);
7792
+ if (items === void 0) return;
7793
+ const fingerprint = JSON.stringify(items);
7794
+ const previous = this.queues.get(sessionId);
7795
+ this.queues.set(sessionId, {
7796
+ items,
7797
+ fingerprint
7798
+ });
7799
+ if (origin === "live" && previous?.fingerprint !== fingerprint) this.record("s2c.session.queue", {
7800
+ sessionId,
7801
+ items
7802
+ });
7803
+ }
7804
+ /**
7805
+ * 打开会话时补发的快照:不占 seq,与 tail 同属打开响应。还没读到过该会话的
7806
+ * 投影(基线失败、Host 不提供或子代理会话)时不发,因为未知不等于空队列。
7807
+ */
7808
+ pushQueueSnapshot(sink, sessionId) {
7809
+ if (this.disposed) return;
7810
+ const queue = this.queues.get(sessionId);
7811
+ if (queue === void 0) return;
7812
+ sink.push("s2c.session.queue", {
7813
+ sessionId,
7814
+ items: queue.items
7815
+ });
7816
+ }
6471
7817
  applyProjection(sessionId, key, value) {
6472
7818
  const row = this.summaries.get(sessionId);
6473
7819
  if (!row) return;
@@ -6594,7 +7940,7 @@ var HostBridge = class {
6594
7940
  hasMore: Boolean(result.hasMore) || page.dropped > 0
6595
7941
  });
6596
7942
  this.deriveTitleFallback(sessionId, messages);
6597
- this.refreshProjections(sessionId);
7943
+ this.refreshProjections(sessionId).then(() => this.pushQueueSnapshot(sink, sessionId));
6598
7944
  return true;
6599
7945
  } catch {
6600
7946
  return false;
@@ -6620,8 +7966,14 @@ var HostBridge = class {
6620
7966
  return;
6621
7967
  }
6622
7968
  const baseline = result.value;
6623
- if (baseline === null || baseline === void 0) return;
6624
- for (const [key, value] of Object.entries(baseline.values ?? {})) this.applyProjection(sessionId, key, value);
7969
+ if (baseline === null || baseline === void 0) {
7970
+ this.queues.delete(sessionId);
7971
+ return;
7972
+ }
7973
+ for (const [key, value] of Object.entries(baseline.values ?? {})) {
7974
+ if (key === "inbox") this.applyInbox(sessionId, value, "baseline");
7975
+ this.applyProjection(sessionId, key, value);
7976
+ }
6625
7977
  } catch (error) {
6626
7978
  this.diagnostic("sessions.projections threw: " + String(error?.message ?? error));
6627
7979
  }
@@ -6692,12 +8044,15 @@ var HostBridge = class {
6692
8044
  return wireErrorOf("command", hostFailureOf(error, "command/invalid-response"));
6693
8045
  }
6694
8046
  }
6695
- async exportSession(sessionId, signal) {
8047
+ async exportSession(sessionId, signal, format) {
6696
8048
  const api = this.apiProxy.commands;
6697
8049
  if (!api) throw Object.assign(/* @__PURE__ */ new Error("当前主机不支持日志导出"), { code: "command/export-unsupported" });
6698
8050
  return api.export({
6699
8051
  rpcId: randomUUID(),
6700
- payload: { sessionId }
8052
+ payload: {
8053
+ sessionId,
8054
+ ...format === void 0 ? {} : { format }
8055
+ }
6701
8056
  }, signal);
6702
8057
  }
6703
8058
  /** 由会话目录决定技能可见性;不读取正文,也不把本机路径发给移动端。 */
@@ -6913,6 +8268,47 @@ var HostBridge = class {
6913
8268
  };
6914
8269
  }
6915
8270
  }
8271
+ /**
8272
+ * 删除或转向一条排队项。ack 只表示 Host 已提交变更,真实状态以随后的
8273
+ * s2c.session.queue 为准;不做幂等,项已消失时 Host 回 queue-item-not-found。
8274
+ */
8275
+ async updateQueue(sessionId, itemId, action) {
8276
+ const update = this.apiProxy.sessions.updateQueue;
8277
+ if (typeof update !== "function") return {
8278
+ ok: false,
8279
+ code: "E_UNSUPPORTED",
8280
+ message: "prompt queue unavailable on this host version"
8281
+ };
8282
+ try {
8283
+ const response = await update.call(this.apiProxy.sessions, {
8284
+ rpcId: randomUUID(),
8285
+ payload: {
8286
+ sessionId,
8287
+ itemId,
8288
+ action: { kind: action }
8289
+ }
8290
+ });
8291
+ if (!response.result) return {
8292
+ ok: false,
8293
+ code: "E_INTERNAL",
8294
+ message: "queue update returned no result"
8295
+ };
8296
+ if (!response.result.ok) return wireErrorOf("queue", response.result.error);
8297
+ return {
8298
+ ok: true,
8299
+ value: {
8300
+ sessionId,
8301
+ itemId
8302
+ }
8303
+ };
8304
+ } catch (error) {
8305
+ return {
8306
+ ok: false,
8307
+ code: "E_INTERNAL",
8308
+ message: String(error)
8309
+ };
8310
+ }
8311
+ }
6916
8312
  async cancelSession(sessionId) {
6917
8313
  const cancel = this.apiProxy.sessions.cancel;
6918
8314
  if (typeof cancel !== "function") return {
@@ -7139,6 +8535,17 @@ var HostBridge = class {
7139
8535
  replayed: result.replayed
7140
8536
  };
7141
8537
  }
8538
+ /**
8539
+ * 当前 adapter 是否宿主级计划任务模型。
8540
+ *
8541
+ * 能力位 `schedulesHost`(host-capabilities.ts)与新帧、旧帧映射全部读这一个
8542
+ * 判别位:探测只发生一次(DshApiProxy.resolveSchedule),这里不再看控制器
8543
+ * 方法,也不看版本号字符串。
8544
+ */
8545
+ get hostSchedule() {
8546
+ const schedule = this.apiProxy.schedule;
8547
+ return schedule?.mode === "host" ? schedule : void 0;
8548
+ }
7142
8549
  async listSchedules(sessionId) {
7143
8550
  const schedule = this.apiProxy.schedule;
7144
8551
  if (schedule === void 0) return {
@@ -7146,6 +8553,7 @@ var HostBridge = class {
7146
8553
  code: "E_UNSUPPORTED",
7147
8554
  message: "schedules unavailable on this host version"
7148
8555
  };
8556
+ if (schedule.mode === "host") return this.listHostSchedulesAsLegacy(schedule, sessionId);
7149
8557
  try {
7150
8558
  const response = await schedule.list({
7151
8559
  rpcId: randomUUID(),
@@ -7169,6 +8577,345 @@ var HostBridge = class {
7169
8577
  };
7170
8578
  }
7171
8579
  }
8580
+ /** 宿主级列表 -> 旧会话级视图:过滤 session 目标、丢弃 ended、按 scheduledAt 升序。 */
8581
+ async listHostSchedulesAsLegacy(schedule, sessionId) {
8582
+ try {
8583
+ const tasks = [];
8584
+ let offset = 0;
8585
+ for (;;) {
8586
+ const response = await schedule.list({
8587
+ rpcId: randomUUID(),
8588
+ payload: {
8589
+ limit: HOST_SCHEDULE_PAGE_SIZE,
8590
+ offset
8591
+ }
8592
+ });
8593
+ if (!response.result) return {
8594
+ ok: false,
8595
+ code: "E_INTERNAL",
8596
+ message: "schedule list returned no result"
8597
+ };
8598
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8599
+ const page = response.result.value;
8600
+ tasks.push(...page.tasks);
8601
+ offset += page.tasks.length;
8602
+ if (page.tasks.length === 0 || offset >= page.total) break;
8603
+ }
8604
+ const views = [];
8605
+ for (const task of tasks) {
8606
+ if (task.target.kind !== "session" || task.target.sessionId !== sessionId) continue;
8607
+ if (task.status === "ended") continue;
8608
+ const view = toLegacyScheduleView(task);
8609
+ if (view !== void 0) views.push(view);
8610
+ }
8611
+ views.sort((left, right) => left.scheduledAt < right.scheduledAt ? -1 : left.scheduledAt > right.scheduledAt ? 1 : 0);
8612
+ return {
8613
+ ok: true,
8614
+ value: views
8615
+ };
8616
+ } catch (error) {
8617
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
8618
+ }
8619
+ }
8620
+ async listHostSchedules(request = {}) {
8621
+ const schedule = this.hostSchedule;
8622
+ if (schedule === void 0) return {
8623
+ ok: false,
8624
+ code: "E_UNSUPPORTED",
8625
+ message: "host-level schedules unavailable on this host version"
8626
+ };
8627
+ try {
8628
+ const response = await schedule.list({
8629
+ rpcId: randomUUID(),
8630
+ payload: request
8631
+ });
8632
+ if (!response.result) return {
8633
+ ok: false,
8634
+ code: "E_INTERNAL",
8635
+ message: "schedule host list returned no result"
8636
+ };
8637
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8638
+ return {
8639
+ ok: true,
8640
+ value: response.result.value
8641
+ };
8642
+ } catch (error) {
8643
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
8644
+ }
8645
+ }
8646
+ async viewHostSchedule(id) {
8647
+ const schedule = this.hostSchedule;
8648
+ if (schedule === void 0) return {
8649
+ ok: false,
8650
+ code: "E_UNSUPPORTED",
8651
+ message: "host-level schedules unavailable on this host version"
8652
+ };
8653
+ try {
8654
+ const response = await schedule.view({
8655
+ rpcId: randomUUID(),
8656
+ payload: { id }
8657
+ });
8658
+ if (!response.result) return {
8659
+ ok: false,
8660
+ code: "E_INTERNAL",
8661
+ message: "schedule host view returned no result"
8662
+ };
8663
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8664
+ const value = response.result.value;
8665
+ if (value.kind === "not-found") return {
8666
+ ok: false,
8667
+ code: "E_NOT_FOUND",
8668
+ message: "schedule not found"
8669
+ };
8670
+ return {
8671
+ ok: true,
8672
+ value: value.task
8673
+ };
8674
+ } catch (error) {
8675
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
8676
+ }
8677
+ }
8678
+ async createHostSchedule(deviceId, payload) {
8679
+ const schedule = this.hostSchedule;
8680
+ if (schedule === void 0) return {
8681
+ ok: false,
8682
+ code: "E_UNSUPPORTED",
8683
+ message: "host-level schedules unavailable on this host version"
8684
+ };
8685
+ const { clientRequestId, ...request } = payload;
8686
+ const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
8687
+ const response = await schedule.create({
8688
+ rpcId: randomUUID(),
8689
+ payload: {
8690
+ title: request.title,
8691
+ prompt: request.prompt,
8692
+ timing: request.timing,
8693
+ ...request.target !== void 0 ? { target: request.target } : {}
8694
+ }
8695
+ });
8696
+ if (!response.result) return {
8697
+ ok: false,
8698
+ code: "E_INTERNAL",
8699
+ message: "schedule host create returned no result"
8700
+ };
8701
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8702
+ return {
8703
+ ok: true,
8704
+ value: response.result.value
8705
+ };
8706
+ });
8707
+ if (result.ok) {
8708
+ this.record("s2c.schedule.changed", {});
8709
+ return {
8710
+ ok: true,
8711
+ value: result.value,
8712
+ replayed: result.replayed
8713
+ };
8714
+ }
8715
+ return {
8716
+ ok: false,
8717
+ code: result.code,
8718
+ message: result.message ?? result.code,
8719
+ replayed: result.replayed
8720
+ };
8721
+ }
8722
+ async editHostSchedule(deviceId, payload) {
8723
+ const schedule = this.hostSchedule;
8724
+ if (schedule === void 0) return {
8725
+ ok: false,
8726
+ code: "E_UNSUPPORTED",
8727
+ message: "host-level schedules unavailable on this host version"
8728
+ };
8729
+ const { clientRequestId, ...request } = payload;
8730
+ const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
8731
+ const expected = request.expectedUpdatedAt !== void 0 ? { expectedUpdatedAt: request.expectedUpdatedAt } : {};
8732
+ const response = await schedule.edit({
8733
+ rpcId: randomUUID(),
8734
+ payload: request.action === "update" ? {
8735
+ id: request.id,
8736
+ action: "update",
8737
+ patch: request.patch,
8738
+ ...expected
8739
+ } : {
8740
+ id: request.id,
8741
+ action: "delete",
8742
+ ...expected
8743
+ }
8744
+ });
8745
+ if (!response.result) return {
8746
+ ok: false,
8747
+ code: "E_INTERNAL",
8748
+ message: "schedule host edit returned no result"
8749
+ };
8750
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8751
+ const value = response.result.value;
8752
+ if (value.kind === "conflict") return {
8753
+ ok: false,
8754
+ code: "E_BUSY",
8755
+ message: "schedule changed since it was read"
8756
+ };
8757
+ if (value.kind === "not-found") return {
8758
+ ok: false,
8759
+ code: "E_NOT_FOUND",
8760
+ message: "schedule not found"
8761
+ };
8762
+ if (request.action === "delete") {
8763
+ if (value.kind !== "deleted") return {
8764
+ ok: false,
8765
+ code: "E_INTERNAL",
8766
+ message: "schedule delete returned an updated task"
8767
+ };
8768
+ return {
8769
+ ok: true,
8770
+ value: {
8771
+ deleted: true,
8772
+ id: value.id
8773
+ }
8774
+ };
8775
+ }
8776
+ if (value.kind !== "task") return {
8777
+ ok: false,
8778
+ code: "E_INTERNAL",
8779
+ message: "schedule update returned a deletion"
8780
+ };
8781
+ return {
8782
+ ok: true,
8783
+ value: { task: value.task }
8784
+ };
8785
+ });
8786
+ if (result.ok) {
8787
+ this.record("s2c.schedule.changed", {});
8788
+ return {
8789
+ ok: true,
8790
+ value: result.value,
8791
+ replayed: result.replayed
8792
+ };
8793
+ }
8794
+ return {
8795
+ ok: false,
8796
+ code: result.code,
8797
+ message: result.message ?? result.code,
8798
+ replayed: result.replayed
8799
+ };
8800
+ }
8801
+ async runHostSchedule(deviceId, payload) {
8802
+ const schedule = this.hostSchedule;
8803
+ if (schedule === void 0) return {
8804
+ ok: false,
8805
+ code: "E_UNSUPPORTED",
8806
+ message: "host-level schedules unavailable on this host version"
8807
+ };
8808
+ const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, { id: payload.id }, async () => {
8809
+ const response = await schedule.run({
8810
+ rpcId: randomUUID(),
8811
+ payload: { id: payload.id }
8812
+ });
8813
+ if (!response.result) return {
8814
+ ok: false,
8815
+ code: "E_INTERNAL",
8816
+ message: "schedule host run returned no result"
8817
+ };
8818
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8819
+ const value = response.result.value;
8820
+ if (value.kind === "pending") return {
8821
+ ok: false,
8822
+ code: "E_BUSY",
8823
+ message: "schedule execution already pending"
8824
+ };
8825
+ if (value.kind === "not-found") return {
8826
+ ok: false,
8827
+ code: "E_NOT_FOUND",
8828
+ message: "schedule not found"
8829
+ };
8830
+ return {
8831
+ ok: true,
8832
+ value: value.task
8833
+ };
8834
+ });
8835
+ if (result.ok) {
8836
+ this.record("s2c.schedule.changed", {});
8837
+ return {
8838
+ ok: true,
8839
+ value: result.value,
8840
+ replayed: result.replayed
8841
+ };
8842
+ }
8843
+ return {
8844
+ ok: false,
8845
+ code: result.code,
8846
+ message: result.message ?? result.code,
8847
+ replayed: result.replayed
8848
+ };
8849
+ }
8850
+ /**
8851
+ * 删除宿主上全部已结束(`status="ended"`)的任务,返回删除条数。
8852
+ *
8853
+ * 与其他变更帧共用 scheduleMutations journal:同一 clientRequestId 重放时直接回放
8854
+ * 首次结果,不会再删一次。成功后广播 `s2c.schedule.changed`;失败时同样广播——上游
8855
+ * 文档说明失败的批次无法确定哪些行已删除,客户端应重新读取列表。
8856
+ */
8857
+ async deleteEndedHostSchedules(deviceId, payload) {
8858
+ const schedule = this.hostSchedule;
8859
+ if (schedule === void 0) return {
8860
+ ok: false,
8861
+ code: "E_UNSUPPORTED",
8862
+ message: "host-level schedules unavailable on this host version"
8863
+ };
8864
+ const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, { operation: "deleteEnded" }, async () => {
8865
+ const response = await schedule.deleteEnded({
8866
+ rpcId: randomUUID(),
8867
+ payload: {}
8868
+ });
8869
+ if (!response.result) return {
8870
+ ok: false,
8871
+ code: "E_INTERNAL",
8872
+ message: "schedule delete ended returned no result"
8873
+ };
8874
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8875
+ return {
8876
+ ok: true,
8877
+ value: response.result.value
8878
+ };
8879
+ });
8880
+ this.record("s2c.schedule.changed", {});
8881
+ if (result.ok) return {
8882
+ ok: true,
8883
+ value: result.value ?? {},
8884
+ replayed: result.replayed
8885
+ };
8886
+ return {
8887
+ ok: false,
8888
+ code: result.code,
8889
+ message: result.message ?? result.code,
8890
+ replayed: result.replayed
8891
+ };
8892
+ }
8893
+ async scheduleLimits() {
8894
+ const schedule = this.hostSchedule;
8895
+ if (schedule === void 0) return {
8896
+ ok: false,
8897
+ code: "E_UNSUPPORTED",
8898
+ message: "host-level schedules unavailable on this host version"
8899
+ };
8900
+ try {
8901
+ const response = await schedule.limits({
8902
+ rpcId: randomUUID(),
8903
+ payload: {}
8904
+ });
8905
+ if (!response.result) return {
8906
+ ok: false,
8907
+ code: "E_INTERNAL",
8908
+ message: "schedule limits returned no result"
8909
+ };
8910
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8911
+ return {
8912
+ ok: true,
8913
+ value: response.result.value
8914
+ };
8915
+ } catch (error) {
8916
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
8917
+ }
8918
+ }
7172
8919
  async scheduleHistory(sessionId, id, limit, before) {
7173
8920
  const schedule = this.apiProxy.schedule;
7174
8921
  if (schedule === void 0) return {
@@ -7176,6 +8923,28 @@ var HostBridge = class {
7176
8923
  code: "E_UNSUPPORTED",
7177
8924
  message: "schedules unavailable on this host version"
7178
8925
  };
8926
+ if (schedule.mode === "host") try {
8927
+ const response = await schedule.history({
8928
+ rpcId: randomUUID(),
8929
+ payload: {
8930
+ id,
8931
+ limit,
8932
+ ...before !== void 0 ? { before } : {}
8933
+ }
8934
+ });
8935
+ if (!response.result) return {
8936
+ ok: false,
8937
+ code: "E_INTERNAL",
8938
+ message: "schedule history returned no result"
8939
+ };
8940
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
8941
+ return {
8942
+ ok: true,
8943
+ value: response.result.value
8944
+ };
8945
+ } catch (error) {
8946
+ return wireErrorOf("schedule", hostFailureOf(error, "internal"));
8947
+ }
7179
8948
  try {
7180
8949
  const response = await schedule.history({
7181
8950
  rpcId: randomUUID(),
@@ -7203,14 +8972,76 @@ var HostBridge = class {
7203
8972
  message: String(error)
7204
8973
  };
7205
8974
  }
7206
- }
7207
- async createSchedule(deviceId, payload) {
7208
- const schedule = this.apiProxy.schedule;
7209
- if (schedule === void 0) return {
7210
- ok: false,
7211
- code: "E_UNSUPPORTED",
7212
- message: "schedules unavailable on this host version"
7213
- };
8975
+ }
8976
+ async createSchedule(deviceId, payload) {
8977
+ const schedule = this.apiProxy.schedule;
8978
+ if (schedule === void 0) return {
8979
+ ok: false,
8980
+ code: "E_UNSUPPORTED",
8981
+ message: "schedules unavailable on this host version"
8982
+ };
8983
+ if (schedule.mode === "host") {
8984
+ const { clientRequestId, sessionId, title, prompt, ...selectors } = payload;
8985
+ const timing = hostTimingFromLegacySelector(selectors);
8986
+ if (timing === void 0) return {
8987
+ ok: false,
8988
+ code: "E_PROTOCOL",
8989
+ message: "unsupported schedule selector"
8990
+ };
8991
+ const request = {
8992
+ title,
8993
+ prompt,
8994
+ timing,
8995
+ target: {
8996
+ kind: "session",
8997
+ sessionId
8998
+ }
8999
+ };
9000
+ const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
9001
+ const response = await schedule.create({
9002
+ rpcId: randomUUID(),
9003
+ payload: {
9004
+ title,
9005
+ prompt,
9006
+ timing,
9007
+ target: {
9008
+ kind: "session",
9009
+ sessionId
9010
+ }
9011
+ }
9012
+ });
9013
+ if (!response.result) return {
9014
+ ok: false,
9015
+ code: "E_INTERNAL",
9016
+ message: "schedule create returned no result"
9017
+ };
9018
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9019
+ const legacy = toLegacyScheduleView(response.result.value);
9020
+ if (legacy === void 0) return {
9021
+ ok: false,
9022
+ code: "E_PROTOCOL",
9023
+ message: "schedule cannot be represented in the session-scoped view"
9024
+ };
9025
+ return {
9026
+ ok: true,
9027
+ value: legacy
9028
+ };
9029
+ });
9030
+ if (result.ok) {
9031
+ this.record("s2c.schedule.changed", {});
9032
+ return {
9033
+ ok: true,
9034
+ value: result.value,
9035
+ replayed: result.replayed
9036
+ };
9037
+ }
9038
+ return {
9039
+ ok: false,
9040
+ code: result.code,
9041
+ message: result.message ?? result.code,
9042
+ replayed: result.replayed
9043
+ };
9044
+ }
7214
9045
  const { clientRequestId, sessionId, ...request } = payload;
7215
9046
  const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, {
7216
9047
  sessionId,
@@ -7256,6 +9087,106 @@ var HostBridge = class {
7256
9087
  code: "E_UNSUPPORTED",
7257
9088
  message: "schedules unavailable on this host version"
7258
9089
  };
9090
+ if (schedule.mode === "host") {
9091
+ const request = {
9092
+ sessionId: payload.sessionId,
9093
+ id: payload.id,
9094
+ expected: payload.expected,
9095
+ title: payload.title,
9096
+ prompt: payload.prompt,
9097
+ change: payload.change
9098
+ };
9099
+ const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, request, async () => {
9100
+ const current = await schedule.view({
9101
+ rpcId: randomUUID(),
9102
+ payload: { id: payload.id }
9103
+ });
9104
+ if (!current.result) return {
9105
+ ok: false,
9106
+ code: "E_INTERNAL",
9107
+ message: "schedule view returned no result"
9108
+ };
9109
+ if (!current.result.ok) return wireErrorOf("schedule", current.result.error);
9110
+ if (current.result.value.kind === "not-found") return {
9111
+ ok: false,
9112
+ code: "E_NOT_FOUND",
9113
+ message: "schedule not found"
9114
+ };
9115
+ const currentTask = current.result.value.task;
9116
+ if (!legacyScheduleMatchesExpected(currentTask, payload.expected)) return {
9117
+ ok: false,
9118
+ code: "E_BUSY",
9119
+ message: "schedule changed since it was read"
9120
+ };
9121
+ const patch = hostPatchFromLegacyUpdate(payload);
9122
+ if (Object.keys(patch).length === 0) {
9123
+ const legacy = toLegacyScheduleView(currentTask);
9124
+ if (legacy === void 0) return {
9125
+ ok: false,
9126
+ code: "E_PROTOCOL",
9127
+ message: "schedule cannot be represented in the session-scoped view"
9128
+ };
9129
+ return {
9130
+ ok: true,
9131
+ value: legacy
9132
+ };
9133
+ }
9134
+ const response = await schedule.edit({
9135
+ rpcId: randomUUID(),
9136
+ payload: {
9137
+ id: payload.id,
9138
+ action: "update",
9139
+ patch
9140
+ }
9141
+ });
9142
+ if (!response.result) return {
9143
+ ok: false,
9144
+ code: "E_INTERNAL",
9145
+ message: "schedule edit returned no result"
9146
+ };
9147
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9148
+ const value = response.result.value;
9149
+ if (value.kind === "conflict") return {
9150
+ ok: false,
9151
+ code: "E_BUSY",
9152
+ message: "schedule changed since it was read"
9153
+ };
9154
+ if (value.kind === "not-found") return {
9155
+ ok: false,
9156
+ code: "E_NOT_FOUND",
9157
+ message: "schedule not found"
9158
+ };
9159
+ if (value.kind !== "task") return {
9160
+ ok: false,
9161
+ code: "E_INTERNAL",
9162
+ message: "schedule edit returned an unexpected deletion"
9163
+ };
9164
+ const legacy = toLegacyScheduleView(value.task);
9165
+ if (legacy === void 0) return {
9166
+ ok: false,
9167
+ code: "E_PROTOCOL",
9168
+ message: "schedule cannot be represented in the session-scoped view"
9169
+ };
9170
+ return {
9171
+ ok: true,
9172
+ value: legacy
9173
+ };
9174
+ });
9175
+ if (result.ok) {
9176
+ this.record("s2c.schedule.changed", {});
9177
+ return {
9178
+ ok: true,
9179
+ value: result.value,
9180
+ replayed: result.replayed
9181
+ };
9182
+ }
9183
+ return {
9184
+ ok: false,
9185
+ code: result.code,
9186
+ message: result.message ?? result.code,
9187
+ replayed: result.replayed
9188
+ };
9189
+ }
7259
9190
  const { clientRequestId, ...request } = payload;
7260
9191
  const result = await this.scheduleMutations.dispatch(deviceId, clientRequestId, request, async () => {
7261
9192
  const response = await schedule.update({
@@ -7301,6 +9232,63 @@ var HostBridge = class {
7301
9232
  code: "E_UNSUPPORTED",
7302
9233
  message: "schedules unavailable on this host version"
7303
9234
  };
9235
+ if (schedule.mode === "host") {
9236
+ const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
9237
+ sessionId: payload.sessionId,
9238
+ id: payload.id
9239
+ }, async () => {
9240
+ const response = await schedule.edit({
9241
+ rpcId: randomUUID(),
9242
+ payload: {
9243
+ id: payload.id,
9244
+ action: "delete"
9245
+ }
9246
+ });
9247
+ if (!response.result) return {
9248
+ ok: false,
9249
+ code: "E_INTERNAL",
9250
+ message: "schedule delete returned no result"
9251
+ };
9252
+ if (!response.result.ok) return wireErrorOf("schedule", response.result.error);
9253
+ const value = response.result.value;
9254
+ if (value.kind === "deleted") return {
9255
+ ok: true,
9256
+ value: {
9257
+ id: value.id,
9258
+ deleted: true
9259
+ }
9260
+ };
9261
+ if (value.kind === "not-found") return {
9262
+ ok: false,
9263
+ code: "E_NOT_FOUND",
9264
+ message: "schedule not found"
9265
+ };
9266
+ if (value.kind === "conflict") return {
9267
+ ok: false,
9268
+ code: "E_BUSY",
9269
+ message: "schedule changed since it was read"
9270
+ };
9271
+ return {
9272
+ ok: false,
9273
+ code: "E_INTERNAL",
9274
+ message: "schedule delete returned an updated task"
9275
+ };
9276
+ });
9277
+ if (result.ok) {
9278
+ this.record("s2c.schedule.changed", {});
9279
+ return {
9280
+ ok: true,
9281
+ value: result.value,
9282
+ replayed: result.replayed
9283
+ };
9284
+ }
9285
+ return {
9286
+ ok: false,
9287
+ code: result.code,
9288
+ message: result.message ?? result.code,
9289
+ replayed: result.replayed
9290
+ };
9291
+ }
7304
9292
  const result = await this.scheduleMutations.dispatch(deviceId, payload.clientRequestId, {
7305
9293
  sessionId: payload.sessionId,
7306
9294
  id: payload.id
@@ -7346,7 +9334,7 @@ var HostBridge = class {
7346
9334
  replayed: result.replayed
7347
9335
  };
7348
9336
  }
7349
- async sendPrompt(sessionId, text, images = [], documents = []) {
9337
+ async sendPrompt(sessionId, text, images = [], documents = [], options = {}) {
7350
9338
  try {
7351
9339
  const content = [];
7352
9340
  if (text.trim().length > 0) content.push({
@@ -7362,10 +9350,10 @@ var HostBridge = class {
7362
9350
  text: documentPromptBlock(document)
7363
9351
  });
7364
9352
  const response = await this.apiProxy.sessions.prompt({
7365
- rpcId: randomUUID(),
9353
+ rpcId: options.clientSendId !== void 0 ? promptRequestId(options.clientSendId) : randomUUID(),
7366
9354
  payload: {
7367
9355
  sessionId,
7368
- mode: "queue",
9356
+ mode: options.mode ?? "queue",
7369
9357
  content,
7370
9358
  clientTimeZone: localTimeZone()
7371
9359
  }
@@ -7685,8 +9673,236 @@ function projectSessionModels(value) {
7685
9673
  };
7686
9674
  }
7687
9675
  /** Project a history page (raw events) into MessageProjection rows. */
7688
- //#endregion
7689
- //#region src/dsh-remote-interactions.ts
9676
+ /** 宿主级列表全量拉取时的页大小(Host 允许的最大值)。 */
9677
+ const HOST_SCHEDULE_PAGE_SIZE = 500;
9678
+ /** 参与旧帧冲突比较的字段:`state`/`deliveryMode` 是派生字段,不参与。 */
9679
+ const LEGACY_PROJECTION_FIELDS = [
9680
+ "id",
9681
+ "kind",
9682
+ "title",
9683
+ "prompt",
9684
+ "scheduledAt",
9685
+ "afterSeconds",
9686
+ "everySeconds",
9687
+ "time",
9688
+ "timeZone",
9689
+ "weekdays",
9690
+ "expression"
9691
+ ];
9692
+ function isPlainObject$1(value) {
9693
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9694
+ }
9695
+ function legacyNumber(...values) {
9696
+ for (const value of values) if (typeof value === "number" && Number.isFinite(value)) return value;
9697
+ }
9698
+ function legacyString(...values) {
9699
+ for (const value of values) if (typeof value === "string" && value.length > 0) return value;
9700
+ }
9701
+ function normalizeAtInput(value) {
9702
+ if (typeof value === "string" && value.length > 0) return value;
9703
+ if (!isPlainObject$1(value)) return void 0;
9704
+ const date = legacyString(value.date);
9705
+ const time = legacyString(value.time);
9706
+ const zone = legacyString(value.time_zone, value.timeZone);
9707
+ return date !== void 0 && time !== void 0 && zone !== void 0 ? {
9708
+ date,
9709
+ time,
9710
+ time_zone: zone
9711
+ } : void 0;
9712
+ }
9713
+ function normalizeLocalTime(value) {
9714
+ if (!isPlainObject$1(value)) return void 0;
9715
+ const time = legacyString(value.time);
9716
+ const zone = legacyString(value.time_zone, value.timeZone);
9717
+ return time !== void 0 && zone !== void 0 ? {
9718
+ time,
9719
+ time_zone: zone
9720
+ } : void 0;
9721
+ }
9722
+ function normalizeWeekdays(value) {
9723
+ if (!Array.isArray(value) || value.length === 0) return void 0;
9724
+ const days = value.filter((day) => typeof day === "number" && Number.isSafeInteger(day) && day >= 1 && day <= 7);
9725
+ return days.length === value.length ? days : void 0;
9726
+ }
9727
+ /** 旧创建帧的选择器 -> 宿主级 timing。旧 wire 校验保证恰好一个选择器。 */
9728
+ function hostTimingFromLegacySelector(selectors) {
9729
+ const after = legacyNumber(selectors.after_seconds);
9730
+ if (after !== void 0) return {
9731
+ kind: "after",
9732
+ after_seconds: after
9733
+ };
9734
+ if (selectors.at !== void 0) {
9735
+ const at = normalizeAtInput(selectors.at);
9736
+ return at === void 0 ? void 0 : {
9737
+ kind: "at",
9738
+ at
9739
+ };
9740
+ }
9741
+ const every = legacyNumber(selectors.every_seconds);
9742
+ if (every !== void 0) return {
9743
+ kind: "every",
9744
+ every_seconds: every
9745
+ };
9746
+ if (selectors.daily !== void 0) {
9747
+ const daily = normalizeLocalTime(selectors.daily);
9748
+ return daily === void 0 ? void 0 : {
9749
+ kind: "daily",
9750
+ daily
9751
+ };
9752
+ }
9753
+ if (selectors.weekly !== void 0) {
9754
+ if (!isPlainObject$1(selectors.weekly)) return void 0;
9755
+ const time = normalizeLocalTime(selectors.weekly);
9756
+ const weekdays = normalizeWeekdays(selectors.weekly.weekdays);
9757
+ return time === void 0 || weekdays === void 0 ? void 0 : {
9758
+ kind: "weekly",
9759
+ weekly: {
9760
+ ...time,
9761
+ weekdays
9762
+ }
9763
+ };
9764
+ }
9765
+ if (selectors.cron !== void 0) {
9766
+ if (!isPlainObject$1(selectors.cron)) return void 0;
9767
+ const expression = legacyString(selectors.cron.expression);
9768
+ const zone = legacyString(selectors.cron.time_zone, selectors.cron.timeZone);
9769
+ return expression === void 0 || zone === void 0 ? void 0 : {
9770
+ kind: "cron",
9771
+ cron: {
9772
+ expression,
9773
+ time_zone: zone
9774
+ }
9775
+ };
9776
+ }
9777
+ }
9778
+ /**
9779
+ * 旧更新帧的 `change` -> 宿主级 timing。
9780
+ *
9781
+ * 两种历史写法都认:选择器式(`{kind:'daily', daily:{...}}`,与创建帧同形)与
9782
+ * 扁平式(`{kind:'daily', time, timeZone}` / `{kind:'cron', expression}`)。
9783
+ */
9784
+ function hostTimingFromLegacyChange(change) {
9785
+ if (!isPlainObject$1(change)) return void 0;
9786
+ const selector = hostTimingFromLegacySelector(change);
9787
+ if (selector !== void 0) return selector;
9788
+ switch (change.kind) {
9789
+ case "daily": {
9790
+ const time = legacyString(change.time);
9791
+ const zone = legacyString(change.time_zone, change.timeZone);
9792
+ return time === void 0 || zone === void 0 ? void 0 : {
9793
+ kind: "daily",
9794
+ daily: {
9795
+ time,
9796
+ time_zone: zone
9797
+ }
9798
+ };
9799
+ }
9800
+ case "weekly": {
9801
+ const time = legacyString(change.time);
9802
+ const zone = legacyString(change.time_zone, change.timeZone);
9803
+ const weekdays = normalizeWeekdays(change.weekdays);
9804
+ return time === void 0 || zone === void 0 || weekdays === void 0 ? void 0 : {
9805
+ kind: "weekly",
9806
+ weekly: {
9807
+ time,
9808
+ time_zone: zone,
9809
+ weekdays
9810
+ }
9811
+ };
9812
+ }
9813
+ case "cron": {
9814
+ const expression = legacyString(change.expression);
9815
+ const zone = legacyString(change.time_zone, change.timeZone);
9816
+ return expression === void 0 || zone === void 0 ? void 0 : {
9817
+ kind: "cron",
9818
+ cron: {
9819
+ expression,
9820
+ time_zone: zone
9821
+ }
9822
+ };
9823
+ }
9824
+ case "rrule": {
9825
+ const rrule = legacyString(change.rrule);
9826
+ return rrule === void 0 ? void 0 : {
9827
+ kind: "rrule",
9828
+ rrule
9829
+ };
9830
+ }
9831
+ default: return;
9832
+ }
9833
+ }
9834
+ /** 旧更新帧的 title/prompt/change -> 宿主级 patch。 */
9835
+ function hostPatchFromLegacyUpdate(payload) {
9836
+ const patch = {};
9837
+ if (typeof payload.title === "string") patch.title = payload.title;
9838
+ if (typeof payload.prompt === "string") patch.prompt = payload.prompt;
9839
+ const timing = hostTimingFromLegacyChange(payload.change);
9840
+ if (timing !== void 0) patch.timing = timing;
9841
+ return patch;
9842
+ }
9843
+ /**
9844
+ * 宿主级任务视图 -> 旧会话级视图。
9845
+ *
9846
+ * `state` 只由 `scheduledAt` 是否已过推导(PROTOCOL.md 映射表);`rrule` 在旧枚举
9847
+ * 里没有对应 kind,返回 undefined 让调用方跳过——旧客户端按枚举解码列表,塞进
9848
+ * 未知 kind 会让整页解码失败,静默隐藏比让旧 App 打不开列表更安全。
9849
+ */
9850
+ function toLegacyScheduleView(task) {
9851
+ const instant = Date.parse(task.scheduledAt);
9852
+ const base = {
9853
+ id: task.id,
9854
+ title: task.title,
9855
+ prompt: task.prompt,
9856
+ scheduledAt: task.scheduledAt,
9857
+ state: Number.isFinite(instant) && instant < Date.now() ? "overdue" : "scheduled",
9858
+ deliveryMode: "host"
9859
+ };
9860
+ switch (task.timing.kind) {
9861
+ case "after": return {
9862
+ ...base,
9863
+ kind: "after",
9864
+ afterSeconds: task.timing.after_seconds
9865
+ };
9866
+ case "at": return {
9867
+ ...base,
9868
+ kind: "at"
9869
+ };
9870
+ case "every": return {
9871
+ ...base,
9872
+ kind: "every",
9873
+ everySeconds: task.timing.every_seconds
9874
+ };
9875
+ case "daily": return {
9876
+ ...base,
9877
+ kind: "daily",
9878
+ time: task.timing.daily.time,
9879
+ timeZone: task.timing.daily.time_zone
9880
+ };
9881
+ case "weekly": return {
9882
+ ...base,
9883
+ kind: "weekly",
9884
+ time: task.timing.weekly.time,
9885
+ timeZone: task.timing.weekly.time_zone,
9886
+ weekdays: [...task.timing.weekly.weekdays]
9887
+ };
9888
+ case "cron": return {
9889
+ ...base,
9890
+ kind: "cron",
9891
+ expression: task.timing.cron.expression,
9892
+ timeZone: task.timing.cron.time_zone
9893
+ };
9894
+ default: return;
9895
+ }
9896
+ }
9897
+ /** 旧帧的冲突保护:当前任务的旧投影与客户端回传的 `expected` 逐字段相等。 */
9898
+ function legacyScheduleMatchesExpected(task, expected) {
9899
+ const view = toLegacyScheduleView(task);
9900
+ if (view === void 0) return false;
9901
+ if (!isPlainObject$1(expected)) return false;
9902
+ const actual = view;
9903
+ return LEGACY_PROJECTION_FIELDS.every((key) => JSON.stringify(actual[key]) === JSON.stringify(expected[key]));
9904
+ }
9905
+ // src/dsh-remote-interactions.ts
7690
9906
  /**
7691
9907
  * Resident DSH Remote Events client for phone interactions.
7692
9908
  *
@@ -7874,8 +10090,7 @@ async function startDshRemoteInteractions(hostCtx, handlers) {
7874
10090
  throw error;
7875
10091
  }
7876
10092
  }
7877
- //#endregion
7878
- //#region src/command-api.ts
10093
+ // src/command-api.ts
7879
10094
  /**
7880
10095
  * 给本域自造的失败带上 Host 风格的码。
7881
10096
  *
@@ -7897,6 +10112,11 @@ var DshCommandApi = class {
7897
10112
  get available() {
7898
10113
  return this.ctx.get("commands") !== void 0 && typeof this.ctx.get("typertGateway")?.invoke === "function";
7899
10114
  }
10115
+ get markdownAvailable() {
10116
+ const connection = this.connection();
10117
+ const route = connection?.fetchRoutes instanceof Map ? connection.fetchRoutes.get("/api/session.markdown") : void 0;
10118
+ return typeof connection?.createSharedFetchHandler === "function" && route?.methods instanceof Set && route.methods.has("GET") && typeof route.fetch === "function" && typeof this.ctx.get("sessionQuery")?.readSession === "function";
10119
+ }
7900
10120
  connection() {
7901
10121
  return this.ctx.get("connection");
7902
10122
  }
@@ -8007,14 +10227,17 @@ var DshCommandApi = class {
8007
10227
  } });
8008
10228
  if (result?.ok !== true) throw fail(typeof result?.error?.code === "string" ? result.error.code : "command/invalid-response", typeof result?.error?.message === "string" ? result.error.message : "反馈未能保存");
8009
10229
  }
8010
- async export(sessionId, signal) {
8011
- const response = await this.connection().createSharedFetchHandler("/api").fetch(new Request("http://localhost/api/session.export?includeDescendants=true&sessionId=" + encodeURIComponent(sessionId), { signal }), this.owner());
8012
- if (!response.ok || !response.body || !response.headers.get("content-type")?.includes("application/zip")) throw fail("command/export-unavailable", "主机未能生成会话日志归档");
10230
+ async export(sessionId, signal, format) {
10231
+ const markdown = format === "markdown";
10232
+ const path = markdown ? "/api/session.markdown?sessionId=" : "/api/session.export?includeDescendants=true&sessionId=";
10233
+ const response = await this.connection().createSharedFetchHandler("/api").fetch(new Request("http://localhost" + path + encodeURIComponent(sessionId), { signal }), this.owner());
10234
+ const contentType = response.headers.get("content-type");
10235
+ const matches = markdown ? contentType?.split(";")[0]?.trim().toLowerCase() === "text/markdown" : contentType?.includes("application/zip");
10236
+ if (!response.ok || !response.body || !matches) throw fail("command/export-unavailable", markdown ? "主机未能生成会话对话正文" : "主机未能生成会话日志归档");
8013
10237
  return response;
8014
10238
  }
8015
10239
  };
8016
- //#endregion
8017
- //#region src/dsh-api-proxy.ts
10240
+ // src/dsh-api-proxy.ts
8018
10241
  /**
8019
10242
  * Adapter for the DSH rc.2 controller API.
8020
10243
  *
@@ -8023,9 +10246,26 @@ var DshCommandApi = class {
8023
10246
  * Session/Workspace controllers and Gateway-backed Remote Events, keeping
8024
10247
  * the phone protocol isolated from the Host API.
8025
10248
  */
10249
+ /**
10250
+ * 形状探测:宿主级模型只有 `edit`(没有 `update`)。不按版本号字符串判断——
10251
+ * 版本号无法说明 Schedule bundle 是否挂载、也无法覆盖回退组合。
10252
+ */
10253
+ function isHostScheduleController(controller) {
10254
+ const value = controller;
10255
+ return typeof value.edit === "function" && typeof value.update !== "function";
10256
+ }
8026
10257
  /** Direct-controller adapter with the shape HostBridge consumes. */
8027
10258
  var DshApiProxy = class {
8028
10259
  ctx;
10260
+ /** Probe the Host's generated reflection, not this adapter's listener registration. */
10261
+ get assistantStreamAvailable() {
10262
+ const typert = this.ctx.get("typert");
10263
+ try {
10264
+ return typert?.getPackage?.("@deepseek-ai/dsh-api-session-controller", "host")?.model?.services?.some((service) => service.key === "sessionController" && service.types?.some((type) => type.name === "SessionAssistantStreamFrame")) === true;
10265
+ } catch {
10266
+ return false;
10267
+ }
10268
+ }
8029
10269
  commandApi;
8030
10270
  get commands() {
8031
10271
  if (!this.commandApi.available) return void 0;
@@ -8034,7 +10274,8 @@ var DshApiProxy = class {
8034
10274
  execute: (request) => this.call(() => this.commandApi.execute(request.payload.sessionId, request.payload.line, request.payload.attachments, request.payload.expectedDefinitionId)),
8035
10275
  permissions: (request) => this.call(() => this.commandApi.permissions(request.payload.sessionId)),
8036
10276
  feedback: (request) => this.call(() => this.commandApi.feedback(request.payload.sessionId, request.payload.text, request.payload.category)),
8037
- export: (request, signal) => this.commandApi.export(request.payload.sessionId, signal)
10277
+ markdownAvailable: this.commandApi.markdownAvailable,
10278
+ export: (request, signal) => this.commandApi.export(request.payload.sessionId, signal, request.payload.format)
8038
10279
  };
8039
10280
  }
8040
10281
  session;
@@ -8052,6 +10293,7 @@ var DshApiProxy = class {
8052
10293
  this.session = session;
8053
10294
  this.workspaceController = ctx.get("workspaceController");
8054
10295
  if (this.workspaceController === void 0) delete this.workspace?.unarchiveSession;
10296
+ if (typeof this.session.updateQueue !== "function") delete this.sessions.updateQueue;
8055
10297
  this.directoryPicker = ctx.get("directoryPickerController");
8056
10298
  this.shouldSurfaceInteraction = options.shouldSurfaceInteraction ?? (() => true);
8057
10299
  this.resolveSchedule();
@@ -8082,14 +10324,16 @@ var DshApiProxy = class {
8082
10324
  const controller = this.ctx.get("schedule");
8083
10325
  if (controller === void 0) return;
8084
10326
  this.scheduleController = controller;
8085
- this.scheduleApi = this.createScheduleApi(controller);
10327
+ this.scheduleApi = isHostScheduleController(controller) ? this.createHostScheduleApi(controller) : this.createLegacyScheduleApi(controller);
8086
10328
  }
8087
10329
  get schedule() {
8088
10330
  this.resolveSchedule();
8089
10331
  return this.scheduleApi;
8090
10332
  }
8091
- createScheduleApi(controller) {
10333
+ /** 会话级适配器:行为与宿主级扩展加入前逐字节一致,只多一个判别位。 */
10334
+ createLegacyScheduleApi(controller) {
8092
10335
  return {
10336
+ mode: "legacy",
8093
10337
  list: async (request) => this.call(async () => ({
8094
10338
  sessionId: request.payload.sessionId,
8095
10339
  tasks: (await controller.list({ sessionId: request.payload.sessionId })).map(toScheduleTask)
@@ -8138,6 +10382,83 @@ var DshApiProxy = class {
8138
10382
  };
8139
10383
  }
8140
10384
  /**
10385
+ * 宿主级适配器:把 DSH `ScheduleService` 的分页列表/结果对象归一化成 Bridge
10386
+ * 消费的稳定视图。
10387
+ *
10388
+ * 失败分两类:`view/edit/run/history` 的「找不到」是**结果对象**里的 code
10389
+ * (非变更 miss,不抛错),这里翻译成 outcome 或带 code 的 Error;
10390
+ * `create/edit/run` 的输入非法与目标已归档是 RemoteError 抛出,由
10391
+ * `this.call` 收成 `{code}`,再由 Bridge 按错误词表映射。
10392
+ */
10393
+ createHostScheduleApi(controller) {
10394
+ return {
10395
+ mode: "host",
10396
+ list: async (request) => this.call(async () => {
10397
+ const payload = request.payload ?? {};
10398
+ const value = await controller.list({
10399
+ ...payload.active !== void 0 ? { active: payload.active } : {},
10400
+ ...payload.limit !== void 0 ? { limit: payload.limit } : {},
10401
+ ...payload.offset !== void 0 ? { offset: payload.offset } : {}
10402
+ });
10403
+ const items = Array.isArray(value.items) ? value.items : [];
10404
+ return {
10405
+ tasks: items.map(toHostScheduleTask),
10406
+ shown: finiteCount(value.shown, items.length),
10407
+ total: finiteCount(value.total, items.length),
10408
+ limit: finiteCount(value.limit, payload.limit ?? 100),
10409
+ offset: finiteCount(value.offset, payload.offset ?? 0)
10410
+ };
10411
+ }),
10412
+ view: async (request) => this.call(async () => {
10413
+ return toHostScheduleViewOutcome(await controller.view({ id: request.payload.id }), request.payload.id);
10414
+ }),
10415
+ create: async (request) => this.call(async () => {
10416
+ const payload = request.payload;
10417
+ return toHostScheduleTask(await controller.create({
10418
+ title: payload.title,
10419
+ prompt: payload.prompt,
10420
+ timing: payload.timing,
10421
+ ...payload.target !== void 0 ? { target: normalizeTargetRequest(payload.target) } : {}
10422
+ }));
10423
+ }),
10424
+ edit: async (request) => this.call(async () => {
10425
+ const payload = request.payload;
10426
+ const expected = payload.expectedUpdatedAt !== void 0 ? { expectedUpdatedAt: payload.expectedUpdatedAt } : {};
10427
+ const edit = payload.action === "update" ? {
10428
+ id: payload.id,
10429
+ action: "update",
10430
+ patch: normalizeHostPatch(payload.patch),
10431
+ ...expected
10432
+ } : {
10433
+ id: payload.id,
10434
+ action: "delete",
10435
+ ...expected
10436
+ };
10437
+ return toHostScheduleEditOutcome(await controller.edit(edit), payload.id);
10438
+ }),
10439
+ run: async (request) => this.call(async () => {
10440
+ return toHostScheduleRunOutcome(await controller.run({ id: request.payload.id }), request.payload.id);
10441
+ }),
10442
+ history: async (request) => this.call(async () => {
10443
+ const value = await controller.history({
10444
+ id: request.payload.id,
10445
+ limit: request.payload.limit,
10446
+ ...request.payload.before !== void 0 ? { before: request.payload.before } : {}
10447
+ });
10448
+ if (typeof value.code === "string") throw Object.assign(new Error(String(value.code)), { code: value.code });
10449
+ return toScheduleHistory(value);
10450
+ }),
10451
+ limits: async () => this.call(async () => {
10452
+ const value = controller.deliveryLimits();
10453
+ return { newSessionIntervalSeconds: Math.max(0, Math.trunc(Number(value?.newSessionIntervalSeconds) || 0)) };
10454
+ }),
10455
+ deleteEnded: async () => this.call(async () => {
10456
+ const value = await controller.deleteInactive();
10457
+ return { deleted: Math.max(0, Math.trunc(Number(value?.deleted) || 0)) };
10458
+ })
10459
+ };
10460
+ }
10461
+ /**
8141
10462
  * Decide whether this Gateway Client can surface a phone card. The decision
8142
10463
  * fails open to false so a broken device registry delegates this delivery
8143
10464
  * while the official Web Client remains able to answer.
@@ -8155,16 +10476,33 @@ var DshApiProxy = class {
8155
10476
  }),
8156
10477
  history: async (request) => this.call(async () => {
8157
10478
  const sessionId = request.payload.sessionId;
10479
+ const beforeSeq = request.payload?.beforeSeq;
10480
+ const limit = normalizePageLimit(request.payload?.maxMessages);
10481
+ const inspect = this.session.inspect;
10482
+ if (inspect === void 0) {
10483
+ const page = await this.session.page({
10484
+ address: {
10485
+ kind: "session",
10486
+ sessionId
10487
+ },
10488
+ ...beforeSeq === void 0 ? {} : { throughSeq: beforeSeq - 1 },
10489
+ maxMessages: limit
10490
+ }, new AbortController().signal);
10491
+ return {
10492
+ events: page.records.map((record) => ({ event: record.event })),
10493
+ hasMore: page.hasMore
10494
+ };
10495
+ }
8158
10496
  let inspected;
8159
10497
  try {
8160
- inspected = await this.session.inspect(sessionId);
10498
+ inspected = await inspect.call(this.session, sessionId);
8161
10499
  } catch (error) {
8162
10500
  console.warn(`[deeppilot] session history unavailable for ${JSON.stringify(sessionId)}: ${toError(error).code}: ${toError(error).message}`);
8163
10501
  throw error;
8164
10502
  }
8165
10503
  const window = historyWindow(inspected.events, {
8166
- beforeSeq: request.payload?.beforeSeq,
8167
- limit: normalizePageLimit(request.payload?.maxMessages)
10504
+ beforeSeq,
10505
+ limit
8168
10506
  });
8169
10507
  return {
8170
10508
  events: window.events,
@@ -8181,6 +10519,7 @@ var DshApiProxy = class {
8181
10519
  selectModel: async (request) => this.call(() => this.session.selectModel(request.payload ?? {})),
8182
10520
  rename: async (request) => this.call(() => this.session.rename(request.payload)),
8183
10521
  cancel: async (request) => this.call(() => this.session.cancel(request.payload)),
10522
+ updateQueue: async (request) => this.call(() => this.session.updateQueue(request.payload)),
8184
10523
  attachment: async (request) => this.call(() => this.session.attachment(request.payload)),
8185
10524
  projections: async (request) => this.call(() => this.session.projections(request.payload, new AbortController().signal))
8186
10525
  };
@@ -8248,6 +10587,18 @@ var DshApiProxy = class {
8248
10587
  event
8249
10588
  });
8250
10589
  }), { global: true });
10590
+ const offAssistantStream = this.ctx.on("agent/assistant-stream", (({ agent, frame }) => {
10591
+ queue.push({
10592
+ type: "session/assistant-stream",
10593
+ sessionId: String(agent.session.id),
10594
+ isSubagent: agent.session.header?.origin === "subagent",
10595
+ value: frame
10596
+ });
10597
+ }), { global: true });
10598
+ const offAgentDisposed = this.ctx.on("agent/disposed", (({ agent }) => queue.push({
10599
+ type: "session/assistant-stream-disposed",
10600
+ sessionId: String(agent.session.id)
10601
+ })), { global: true });
8251
10602
  const offProjection = this.ctx.get("sessionProjections")?.onChanged?.((session, key, value) => {
8252
10603
  queue.push({
8253
10604
  type: "session/projection",
@@ -8273,6 +10624,8 @@ var DshApiProxy = class {
8273
10624
  yield* queue.iterate();
8274
10625
  } finally {
8275
10626
  offEvent();
10627
+ offAssistantStream();
10628
+ offAgentDisposed();
8276
10629
  offProjection?.();
8277
10630
  offSkills();
8278
10631
  offCommands.forEach((dispose) => dispose());
@@ -8518,6 +10871,206 @@ function toScheduleHistory(value) {
8518
10871
  ...typeof row.nextBefore === "string" ? { nextBefore: row.nextBefore } : {}
8519
10872
  };
8520
10873
  }
10874
+ /**
10875
+ * 请求侧 target:`new_once`/`new` 的会话由 Host 预留或新建,客户端若把查看结果
10876
+ * 里的 `sessionId` 原样回传,不向上游透传(Host 的创建/编辑请求没有这个字段)。
10877
+ */
10878
+ function normalizeTargetRequest(value) {
10879
+ if (value.kind === "session") return {
10880
+ kind: "session",
10881
+ sessionId: String(value.sessionId ?? "")
10882
+ };
10883
+ const create = toHostScheduleCreateSpec(value.create);
10884
+ return {
10885
+ kind: value.kind,
10886
+ ...create !== void 0 ? { create } : {}
10887
+ };
10888
+ }
10889
+ /** 编辑补丁归一化:字段按契约逐个透传,target 走请求侧投影。 */
10890
+ function normalizeHostPatch(value) {
10891
+ return {
10892
+ ...value.title !== void 0 ? { title: value.title } : {},
10893
+ ...value.prompt !== void 0 ? { prompt: value.prompt } : {},
10894
+ ...value.timing !== void 0 ? { timing: value.timing } : {},
10895
+ ...value.target !== void 0 ? { target: normalizeTargetRequest(value.target) } : {},
10896
+ ...value.active !== void 0 ? { active: value.active } : {}
10897
+ };
10898
+ }
10899
+ function isRecord(value) {
10900
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10901
+ }
10902
+ function toRecord(value) {
10903
+ return isRecord(value) ? value : {};
10904
+ }
10905
+ /** 非负安全整数;形状不符时退回 fallback(分页字段只用于展示)。 */
10906
+ function finiteCount(value, fallback) {
10907
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
10908
+ }
10909
+ function positiveSeconds(value) {
10910
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
10911
+ }
10912
+ function hostResultCode(value) {
10913
+ return typeof value.code === "string" ? value.code : void 0;
10914
+ }
10915
+ /** 结果对象里的「非变更 miss」不是抛错,这里翻译成 outcome 或带 code 的 Error。 */
10916
+ function throwHostResultCode(code) {
10917
+ throw Object.assign(new Error(code), { code });
10918
+ }
10919
+ function toHostScheduleCreateSpec(value) {
10920
+ const row = toRecord(value);
10921
+ const spec = {
10922
+ ...typeof row.cwd === "string" ? { cwd: row.cwd } : {},
10923
+ ...typeof row.agentPreset === "string" ? { agentPreset: row.agentPreset } : {}
10924
+ };
10925
+ return Object.keys(spec).length > 0 ? spec : void 0;
10926
+ }
10927
+ /**
10928
+ * 交付目标投影。`session` 与 `new_once` 带 sessionId(后者是创建时 Host 预留
10929
+ * 的会话);`new` 每次投递新建会话。`create` 省略时按 Host 默认。
10930
+ */
10931
+ function toHostScheduleTarget(value) {
10932
+ const target = toRecord(value);
10933
+ const create = toHostScheduleCreateSpec(target.create);
10934
+ if (target.kind === "session") return {
10935
+ kind: "session",
10936
+ sessionId: String(target.sessionId ?? "")
10937
+ };
10938
+ if (target.kind === "new_once") return {
10939
+ kind: "new_once",
10940
+ sessionId: String(target.sessionId ?? ""),
10941
+ ...create !== void 0 ? { create } : {}
10942
+ };
10943
+ return {
10944
+ kind: "new",
10945
+ ...create !== void 0 ? { create } : {}
10946
+ };
10947
+ }
10948
+ function toHostScheduleReceipt(value) {
10949
+ const row = toRecord(value);
10950
+ return {
10951
+ scheduledAt: String(row.scheduledAt ?? ""),
10952
+ deliveredAt: String(row.deliveredAt ?? ""),
10953
+ messageId: String(row.messageId ?? ""),
10954
+ targetSessionId: String(row.targetSessionId ?? "")
10955
+ };
10956
+ }
10957
+ /**
10958
+ * timing 反向投影:DSH 存储记录用 camelCase(`afterSeconds`/`timeZone`),wire
10959
+ * 用 PROTOCOL.md 的 snake_case 嵌套结构。
10960
+ *
10961
+ * `at` 存储后只剩 `scheduledAt`(原始绝对时刻不是独立字段),因此用排定时刻
10962
+ * 回填——对 active 的一次性任务它就是目标时刻。
10963
+ */
10964
+ function toHostScheduleTiming(row) {
10965
+ switch (row.kind) {
10966
+ case "after": return {
10967
+ kind: "after",
10968
+ after_seconds: positiveSeconds(row.afterSeconds)
10969
+ };
10970
+ case "every": return {
10971
+ kind: "every",
10972
+ every_seconds: positiveSeconds(row.everySeconds)
10973
+ };
10974
+ case "daily": return {
10975
+ kind: "daily",
10976
+ daily: localTimeInput(row)
10977
+ };
10978
+ case "weekly": return {
10979
+ kind: "weekly",
10980
+ weekly: {
10981
+ ...localTimeInput(row),
10982
+ weekdays: Array.isArray(row.weekdays) ? row.weekdays.filter((day) => typeof day === "number") : []
10983
+ }
10984
+ };
10985
+ case "cron": return {
10986
+ kind: "cron",
10987
+ cron: {
10988
+ expression: String(row.expression ?? ""),
10989
+ time_zone: String(row.timeZone ?? "")
10990
+ }
10991
+ };
10992
+ case "rrule": return {
10993
+ kind: "rrule",
10994
+ rrule: String(row.rrule ?? "")
10995
+ };
10996
+ default: return {
10997
+ kind: "at",
10998
+ at: String(row.scheduledAt ?? "")
10999
+ };
11000
+ }
11001
+ }
11002
+ function localTimeInput(row) {
11003
+ return {
11004
+ time: String(row.time ?? ""),
11005
+ time_zone: String(row.timeZone ?? "")
11006
+ };
11007
+ }
11008
+ /** Host `ScheduleCatalogEntry` -> wire `schedule.host.task`(丢弃 involvedSessionIds)。 */
11009
+ function toHostScheduleTask(value) {
11010
+ const row = toRecord(value);
11011
+ return {
11012
+ id: String(row.id ?? ""),
11013
+ title: String(row.title ?? ""),
11014
+ prompt: String(row.prompt ?? ""),
11015
+ timing: toHostScheduleTiming(row),
11016
+ target: toHostScheduleTarget(row.target),
11017
+ status: row.status === "paused" || row.status === "ended" ? row.status : "active",
11018
+ scheduledAt: String(row.scheduledAt ?? ""),
11019
+ ...typeof row.createdAt === "string" ? { createdAt: row.createdAt } : {},
11020
+ ...typeof row.updatedAt === "string" ? { updatedAt: row.updatedAt } : {},
11021
+ ...typeof row.executionActive === "boolean" ? { executionActive: row.executionActive } : {},
11022
+ ...typeof row.deliveryFloorEndsAt === "string" ? { deliveryFloorEndsAt: row.deliveryFloorEndsAt } : {},
11023
+ ...isRecord(row.lastDelivery) ? { lastDelivery: toHostScheduleReceipt(row.lastDelivery) } : {}
11024
+ };
11025
+ }
11026
+ function toHostScheduleViewOutcome(value, id) {
11027
+ const code = hostResultCode(value);
11028
+ if (code === "schedule_not_found") return {
11029
+ kind: "not-found",
11030
+ id
11031
+ };
11032
+ if (code !== void 0) throwHostResultCode(code);
11033
+ return {
11034
+ kind: "task",
11035
+ task: toHostScheduleTask(value)
11036
+ };
11037
+ }
11038
+ function toHostScheduleEditOutcome(value, id) {
11039
+ const code = hostResultCode(value);
11040
+ if (code === "schedule_conflict") return {
11041
+ kind: "conflict",
11042
+ id
11043
+ };
11044
+ if (code === "schedule_not_found") return {
11045
+ kind: "not-found",
11046
+ id
11047
+ };
11048
+ if (code !== void 0) throwHostResultCode(code);
11049
+ if (value.deleted === true) return {
11050
+ kind: "deleted",
11051
+ id: String(value.id ?? id)
11052
+ };
11053
+ return {
11054
+ kind: "task",
11055
+ task: toHostScheduleTask(value)
11056
+ };
11057
+ }
11058
+ function toHostScheduleRunOutcome(value, id) {
11059
+ const code = hostResultCode(value);
11060
+ if (code === "schedule_execution_pending") return {
11061
+ kind: "pending",
11062
+ id
11063
+ };
11064
+ if (code === "schedule_not_found") return {
11065
+ kind: "not-found",
11066
+ id
11067
+ };
11068
+ if (code !== void 0) throwHostResultCode(code);
11069
+ return {
11070
+ kind: "task",
11071
+ task: toHostScheduleTask(value)
11072
+ };
11073
+ }
8521
11074
  async function readWorkspaceBaseline(controller) {
8522
11075
  const abort = new AbortController();
8523
11076
  const iterator = controller.follow(abort.signal)[Symbol.asyncIterator]();
@@ -8547,8 +11100,7 @@ async function projectModels(catalog, sessionId, list) {
8547
11100
  failures: Array.isArray(value.failures) ? value.failures : []
8548
11101
  };
8549
11102
  }
8550
- //#endregion
8551
- //#region src/report-service.ts
11103
+ // src/report-service.ts
8552
11104
  /**
8553
11105
  * The Typert receiver the Gateway resolves for the DeepPilot Bridge report.
8554
11106
  * Snapshot data stays non-secret; the token crosses the boundary only through
@@ -8594,8 +11146,7 @@ var DeepPilotReportService = class extends TypertRemoteService {
8594
11146
  return this.pushTester();
8595
11147
  }
8596
11148
  };
8597
- //#endregion
8598
- //#region src/report-wire.ts
11149
+ // src/report-wire.ts
8599
11150
  /** The npm package identity both contribution registrations claim. */
8600
11151
  const REPORT_REMOTE_PACKAGE = "dsh-deeppilot";
8601
11152
  /** Canonical `<namespace>/<method>` endpoint of the report Remote. */
@@ -8677,15 +11228,18 @@ function parseRemote(value) {
8677
11228
  const publicURL = s.publicURL;
8678
11229
  const authURL = s.authURL;
8679
11230
  const message = s.message;
11231
+ const tailscaleVersion = s.tailscaleVersion;
8680
11232
  if (publicURL !== void 0 && typeof publicURL !== "string") reject("remote.publicURL");
8681
11233
  if (authURL !== void 0 && typeof authURL !== "string") reject("remote.authURL");
8682
11234
  if (message !== void 0 && typeof message !== "string") reject("remote.message");
11235
+ if (tailscaleVersion !== void 0 && !isTailscaleVersion(tailscaleVersion)) reject("remote.tailscaleVersion");
8683
11236
  return {
8684
11237
  provider,
8685
11238
  phase,
8686
11239
  ...typeof publicURL === "string" ? { publicURL } : {},
8687
11240
  ...typeof authURL === "string" ? { authURL } : {},
8688
11241
  ...typeof message === "string" ? { message } : {},
11242
+ ...typeof tailscaleVersion === "string" ? { tailscaleVersion } : {},
8689
11243
  updatedAt: int(s, "updatedAt", "remote.updatedAt")
8690
11244
  };
8691
11245
  }
@@ -8977,8 +11531,7 @@ const REPORT_HOST_CONTRIBUTION = {
8977
11531
  }
8978
11532
  ]
8979
11533
  };
8980
- //#endregion
8981
- //#region src/report-remote.ts
11534
+ // src/report-remote.ts
8982
11535
  /**
8983
11536
  * Provide the report service and register its Remote descriptor. Rides an
8984
11537
  * optional `typert` inject: profiles without the web stack never activate it.
@@ -8990,8 +11543,7 @@ function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceR
8990
11543
  remoteCtx.effect(() => () => void unregister(), "dsh-deeppilot: report remote");
8991
11544
  });
8992
11545
  }
8993
- //#endregion
8994
- //#region src/local-address.ts
11546
+ // src/local-address.ts
8995
11547
  function octetsOf(address) {
8996
11548
  const octets = address.split(".").map(Number);
8997
11549
  if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return null;
@@ -9042,8 +11594,7 @@ function localLANIPv4Addresses() {
9042
11594
  candidates.sort((left, right) => priority(left.name) - priority(right.name) || left.name.localeCompare(right.name));
9043
11595
  return [...new Set(candidates.map(({ address }) => address))];
9044
11596
  }
9045
- //#endregion
9046
- //#region src/update-check.ts
11597
+ // src/update-check.ts
9047
11598
  /**
9048
11599
  * Lightweight self-update check for dsh-deeppilot.
9049
11600
  *
@@ -9239,8 +11790,7 @@ var UpdateChecker = class {
9239
11790
  /** No-op kept for API symmetry with the host lifecycle wiring. */
9240
11791
  dispose() {}
9241
11792
  };
9242
- //#endregion
9243
- //#region src/phone-http.ts
11793
+ // src/phone-http.ts
9244
11794
  const CLIENT_IP_HEADER = "x-deeppilot-client-ip";
9245
11795
  function rejectUpgrade(socket, status, reason, retryAfterSeconds) {
9246
11796
  const body = JSON.stringify({ error: reason });
@@ -9274,8 +11824,7 @@ function requestClientIdentity(req) {
9274
11824
  }
9275
11825
  return normalizedAddress(req.socket.remoteAddress) ?? "unknown";
9276
11826
  }
9277
- //#endregion
9278
- //#region src/lan-tls.ts
11827
+ // src/lan-tls.ts
9279
11828
  const LAN_TLS_COMMON_NAME = "dsh-deeppilot";
9280
11829
  const LAN_TLS_KEY_FILE = "key.pem";
9281
11830
  const LAN_TLS_CERT_FILE = "cert.pem";
@@ -9448,8 +11997,7 @@ async function writePrivate(path, contents) {
9448
11997
  throw error;
9449
11998
  }
9450
11999
  }
9451
- //#endregion
9452
- //#region src/index.ts
12000
+ // src/index.ts
9453
12001
  /**
9454
12002
  * dsh-deeppilot — data bridge between the DSH host and DeepPilot
9455
12003
  * clients. Owns independent, narrowly routed LAN (TLS-only, pinned by paired
@@ -9726,6 +12274,23 @@ function apply(ctx, options) {
9726
12274
  return updated;
9727
12275
  }, () => pushGateway.relayTest(), () => pushGateway.selfTest());
9728
12276
  const state = {};
12277
+ let pluginUpdater;
12278
+ ctx.inject(["pluginManager"], (sub) => {
12279
+ const manager = sub.pluginManager;
12280
+ if (!supportsPluginUpdate(manager)) return;
12281
+ const updater = new PluginUpdater({
12282
+ manager,
12283
+ currentVersion: SERVER_VERSION,
12284
+ journalPath: join(dataDir, "update-journal.json")
12285
+ });
12286
+ pluginUpdater = updater;
12287
+ if (state.bridge) setPluginUpdater(state.bridge, updater);
12288
+ sub.effect(() => () => {
12289
+ if (pluginUpdater !== updater) return;
12290
+ pluginUpdater = void 0;
12291
+ if (state.bridge) setPluginUpdater(state.bridge, void 0);
12292
+ }, "deeppilot: optional plugin updater");
12293
+ });
9729
12294
  let pendingUpgrades = 0;
9730
12295
  const authRateLimiter = new AuthRateLimiter();
9731
12296
  const readJSONBody = async (req, maxBytes = 16384) => {
@@ -10057,8 +12622,10 @@ function apply(ctx, options) {
10057
12622
  log("DSH session bridge unavailable: " + String(error), "warn");
10058
12623
  return;
10059
12624
  }
12625
+ setPluginUpdateProbe(proxy, () => pluginUpdater !== void 0);
10060
12626
  const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"), join(dataDir, "schedule-mutations-v1.json"), join(dataDir, "fork-mutations-v1.json"), log);
10061
12627
  bridge.setPushOutlet(pushGateway);
12628
+ setPluginUpdater(bridge, pluginUpdater);
10062
12629
  state.bridge = bridge;
10063
12630
  bridge.start();
10064
12631
  log("data plane active (mux + host streams)");
@@ -10080,7 +12647,6 @@ function apply(ctx, options) {
10080
12647
  await diagnostics.dispose();
10081
12648
  }, "deeppilot: process resources");
10082
12649
  }
10083
- //#endregion
10084
12650
  export { Config, HostBridge, apply, inject, mayReceivePush, name, shouldPrunePushToken, shouldReEnrollRelayToken };
10085
12651
 
10086
12652
  //# sourceMappingURL=index.js.map