@co0ontty/wand 4.94.0 → 4.95.0

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.
Files changed (197) hide show
  1. package/README.md +4 -1
  2. package/dist/ai-text-runner.d.ts +4 -0
  3. package/dist/ai-text-runner.js +44 -4
  4. package/dist/build-info.json +3 -3
  5. package/dist/cli-api.d.ts +1 -0
  6. package/dist/cli-api.js +7 -2
  7. package/dist/cli.js +47 -0
  8. package/dist/conversation-execution-preference.d.ts +7 -0
  9. package/dist/conversation-execution-preference.js +26 -0
  10. package/dist/conversation-organize-contract.d.ts +70 -0
  11. package/dist/conversation-organize-contract.js +1 -0
  12. package/dist/conversation-organize-service.d.ts +46 -0
  13. package/dist/conversation-organize-service.js +505 -0
  14. package/dist/conversation-organize.d.ts +41 -0
  15. package/dist/conversation-organize.js +97 -0
  16. package/dist/conversation-service.d.ts +2 -1
  17. package/dist/conversation-service.js +10 -4
  18. package/dist/core-turn-tracker.d.ts +5 -1
  19. package/dist/core-turn-tracker.js +17 -1
  20. package/dist/decision-expert-service.d.ts +2 -0
  21. package/dist/decision-expert-service.js +17 -1
  22. package/dist/decision-skill.d.ts +0 -1
  23. package/dist/decision-skill.js +5 -64
  24. package/dist/deployment-lock.js +4 -1
  25. package/dist/employee-im-config.d.ts +17 -0
  26. package/dist/employee-im-config.js +85 -0
  27. package/dist/employee-im-lifecycle.d.ts +35 -0
  28. package/dist/employee-im-lifecycle.js +168 -0
  29. package/dist/employee-im-types.d.ts +80 -0
  30. package/dist/employee-im-types.js +32 -0
  31. package/dist/employee-im.d.ts +35 -0
  32. package/dist/employee-im.js +157 -0
  33. package/dist/employee-knowledge.js +3 -2
  34. package/dist/employee-text.d.ts +1 -0
  35. package/dist/employee-text.js +28 -13
  36. package/dist/git-quick-commit.js +21 -3
  37. package/dist/managed-skill.d.ts +6 -0
  38. package/dist/managed-skill.js +99 -0
  39. package/dist/model-auto-assign.js +1 -1
  40. package/dist/process-manager.js +1 -1
  41. package/dist/prompt-optimizer.js +1 -1
  42. package/dist/scheduled-task-activity.d.ts +16 -0
  43. package/dist/scheduled-task-activity.js +42 -0
  44. package/dist/scheduled-task-agenda.d.ts +20 -0
  45. package/dist/scheduled-task-agenda.js +82 -0
  46. package/dist/scheduled-task-calendar.d.ts +37 -0
  47. package/dist/scheduled-task-calendar.js +131 -0
  48. package/dist/scheduled-task-read-range.d.ts +7 -0
  49. package/dist/scheduled-task-read-range.js +35 -0
  50. package/dist/scheduled-task-service.d.ts +6 -1
  51. package/dist/scheduled-task-service.js +61 -1
  52. package/dist/scheduled-task-state.d.ts +1 -1
  53. package/dist/scheduled-task-types.d.ts +38 -0
  54. package/dist/server-asset-routes.js +3 -2
  55. package/dist/server-conversation-organize-routes.d.ts +4 -0
  56. package/dist/server-conversation-organize-routes.js +75 -0
  57. package/dist/server-conversation-routes.js +2 -1
  58. package/dist/server-employee-im-routes.d.ts +9 -0
  59. package/dist/server-employee-im-routes.js +40 -0
  60. package/dist/server-employee-routes.js +1 -1
  61. package/dist/server-scheduled-task-routes.js +24 -0
  62. package/dist/server-session-lifecycle-routes.js +8 -3
  63. package/dist/server-session-routes.js +18 -0
  64. package/dist/server-session-worktree-routes.js +3 -3
  65. package/dist/server-workspace-routes.d.ts +3 -0
  66. package/dist/server-workspace-routes.js +14 -1
  67. package/dist/server.js +39 -5
  68. package/dist/session-ai-context.d.ts +11 -1
  69. package/dist/session-ai-context.js +21 -2
  70. package/dist/session-metadata-service.js +1 -1
  71. package/dist/session-registry.js +6 -0
  72. package/dist/session-request.js +3 -0
  73. package/dist/session-topic.js +1 -1
  74. package/dist/silicon-employee-draft.js +1 -1
  75. package/dist/speech-polisher-service.d.ts +2 -0
  76. package/dist/speech-polisher-service.js +28 -6
  77. package/dist/storage/conversation-organization-repository.d.ts +13 -0
  78. package/dist/storage/conversation-organization-repository.js +33 -0
  79. package/dist/storage/migrations.js +10 -0
  80. package/dist/storage/secrets-repository.d.ts +7 -0
  81. package/dist/storage/secrets-repository.js +28 -1
  82. package/dist/storage/tasks-repository.js +4 -0
  83. package/dist/storage-schedules.d.ts +8 -1
  84. package/dist/storage-schedules.js +24 -0
  85. package/dist/storage-schema.js +9 -0
  86. package/dist/storage.d.ts +6 -1
  87. package/dist/storage.js +13 -0
  88. package/dist/structured-session-manager.d.ts +1 -0
  89. package/dist/structured-session-manager.js +18 -4
  90. package/dist/system-ai-call.d.ts +49 -0
  91. package/dist/system-ai-call.js +28 -0
  92. package/dist/system-ai-session-recorder.d.ts +30 -0
  93. package/dist/system-ai-session-recorder.js +239 -0
  94. package/dist/system-cli.d.ts +6 -0
  95. package/dist/system-cli.js +93 -0
  96. package/dist/system-skill.d.ts +5 -0
  97. package/dist/system-skill.js +19 -0
  98. package/dist/task-organize-contract.js +2 -2
  99. package/dist/task-title-organize-service.d.ts +1 -1
  100. package/dist/task-title-organize-service.js +9 -5
  101. package/dist/task-title-organize.js +13 -6
  102. package/dist/task-title-service.js +1 -1
  103. package/dist/task-title.js +1 -1
  104. package/dist/user-memory.js +1 -1
  105. package/dist/wand-task-sync.js +2 -1
  106. package/dist/web-ui/content/ai-teams.js +12 -12
  107. package/dist/web-ui/content/module-manifest.json +1 -1
  108. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/library-bridge-DTMQIHVJ.js → 09842858f9d34a4e/library-bridge.js} +2 -2
  109. package/dist/web-ui/content/modules/1021ae40c5761e3f/sha256.js +1 -0
  110. package/dist/web-ui/content/modules/134f6120c0acaf57/host.js +1 -0
  111. package/dist/web-ui/content/modules/1d518ae6ed66fe5a/chunk.js +3 -0
  112. package/dist/web-ui/content/modules/1f93b5bd70381bb6/runtime.js +539 -0
  113. package/dist/web-ui/content/modules/26c68d4decdd18b4/markdown.js +1 -0
  114. package/dist/web-ui/content/modules/2ed8bafd5b4fecc5/workspace-window.js +1 -0
  115. package/dist/web-ui/content/modules/31ffc2f05f87652c/host.js +1 -0
  116. package/dist/web-ui/content/modules/35aaa85b9d0937af/chunk.js +20 -0
  117. package/dist/web-ui/content/modules/3664d0620f124292/chunk.js +3 -0
  118. package/dist/web-ui/content/modules/3cea480770cb184c/chunk.js +1 -0
  119. package/dist/web-ui/content/modules/3d12adf54647c1e2/host-bridge.js +1 -0
  120. package/dist/web-ui/content/modules/459cca2482a9a4ba/host.js +18 -0
  121. package/dist/web-ui/content/modules/474c9647130ef3e9/task-board-host.js +2 -0
  122. package/dist/web-ui/content/modules/4f74d2517f29134c/chunk.js +15 -0
  123. package/dist/web-ui/content/modules/52a218d74002a011/host.js +30 -0
  124. package/dist/web-ui/content/modules/54f2b10659ef8475/employee-card.js +1 -0
  125. package/dist/web-ui/content/modules/703fb1c670f7b199/chunk.js +1 -0
  126. package/dist/web-ui/content/modules/75511da23f051718/host.js +1 -0
  127. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-2IVIZ2YC.js → 799b19da948bec8d/host.js} +3 -2
  128. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-XNIR3NTJ.js → 7ef73b6c20d2504f/host.js} +3 -2
  129. package/dist/web-ui/content/modules/8f8f7a0a8874d29a/host.js +1 -0
  130. package/dist/web-ui/content/modules/90f2cd73cdf3a2cd/chat-render.js +25 -0
  131. package/dist/web-ui/content/modules/9510efce6caaa9ec/chunk.js +1 -0
  132. package/dist/web-ui/content/modules/9a0e65188739957f/chunk.js +67 -0
  133. package/dist/web-ui/content/modules/9eae1033f47e4526/host.js +1 -0
  134. package/dist/web-ui/content/modules/a2c5b246e57fa165/panel.js +1 -0
  135. package/dist/web-ui/content/modules/a50ce28f45bfeb2e/host.js +1 -0
  136. package/dist/web-ui/content/modules/adfc53789d5bedb2/chunk.js +1 -0
  137. package/dist/web-ui/content/modules/bcecb0cc14b7d2bd/chunk.js +1 -0
  138. package/dist/web-ui/content/modules/c4706f47c23f33e4/host.js +1 -0
  139. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-Q524TMSR.js → c7de873095c061ab/host.js} +4 -3
  140. package/dist/web-ui/content/modules/d789f84a3f8765ae/page.js +6 -0
  141. package/dist/web-ui/content/modules/db4e2dbc7aed6353/chunk.js +1 -0
  142. package/dist/web-ui/content/modules/e4fd9906521b4aa3/chunk.js +1 -0
  143. package/dist/web-ui/content/modules/eb95c3ee0d42827a/chunk.js +6 -0
  144. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-PHHNGVNL.js → ec68d93a95c1d5d6/host.js} +2 -2
  145. package/dist/web-ui/content/modules/f33ed89d1ccd3a2d/chunk.js +1 -0
  146. package/dist/web-ui/content/modules/f6502dab121f9d91/chunk.js +1 -0
  147. package/dist/web-ui/content/modules/fac83e61c0a8c8a7/host.js +1 -0
  148. package/dist/web-ui/content/scripts.js +1 -1
  149. package/dist/web-ui/embedded-assets.d.ts +200 -27
  150. package/dist/web-ui/embedded-assets.js +5 -4
  151. package/dist/web-ui/react/workspaces/sidebar-manage.d.ts +5 -0
  152. package/dist/web-ui/react/workspaces/sidebar-manage.js +27 -3
  153. package/dist/web-ui/running-activity.d.ts +22 -9
  154. package/dist/web-ui/running-activity.js +58 -42
  155. package/dist/web-ui/running-current-action.d.ts +12 -0
  156. package/dist/web-ui/running-current-action.js +74 -0
  157. package/dist/web-ui/scripts.d.ts +4 -0
  158. package/dist/web-ui/scripts.js +17 -8
  159. package/package.json +4 -2
  160. package/scripts/recover-web-update.mjs +26 -19
  161. package/scripts/recover-web-update.sh +9 -6
  162. package/scripts/update-node.sh +3 -1
  163. package/skills/wand-system/SKILL.md +69 -0
  164. package/skills/wand-system/references/operations.md +100 -0
  165. package/skills/wand-system/scripts/wand.mjs +34 -0
  166. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chat-render-RY2I2CF3.js +0 -24
  167. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-2F6J2MKX.js +0 -1
  168. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-6C6A2RHG.js +0 -1
  169. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-7AFM245W.js +0 -1
  170. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-ECWYA3U5.js +0 -1
  171. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-MHX2QXIS.js +0 -111
  172. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-RAAPETNH.js +0 -1
  173. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-RXKBHYUR.js +0 -38
  174. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-T35D57FW.js +0 -1
  175. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-U4EDP4TI.js +0 -1
  176. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-WHXQR7JT.js +0 -20
  177. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-YOXXNXOX.js +0 -2
  178. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-YQJ6XYV7.js +0 -1
  179. package/dist/web-ui/content/modules/0051bd3d55f10ab7/employee-card-NV34TZIH.js +0 -1
  180. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-2AQAIUNO.js +0 -1
  181. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-AZXNFCRW.js +0 -1
  182. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-EPEQX22R.js +0 -1
  183. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-FZYIMOLC.js +0 -1
  184. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-G6GL4SNL.js +0 -1
  185. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-PEILC5NU.js +0 -1
  186. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-RO5MB6RB.js +0 -69
  187. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-WOQRK6NG.js +0 -1
  188. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-XRVOIEP6.js +0 -1
  189. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-ZDKOQEMS.js +0 -1
  190. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-bridge-PMQZRG5G.js +0 -1
  191. package/dist/web-ui/content/modules/0051bd3d55f10ab7/page-PY4JUL4W.js +0 -3
  192. package/dist/web-ui/content/modules/0051bd3d55f10ab7/panel-BBQFC55Q.js +0 -1
  193. package/dist/web-ui/content/modules/0051bd3d55f10ab7/runtime.js +0 -974
  194. package/dist/web-ui/content/modules/0051bd3d55f10ab7/sha256-ANXV72UC.js +0 -1
  195. package/dist/web-ui/content/modules/0051bd3d55f10ab7/task-board-host-PXZSWBBG.js +0 -2
  196. package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-agent-picker-DJSECSY6.js +0 -1
  197. package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-window-MNMKSOBG.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { TaskDirectoryGroup } from "./types.js";
2
+ import type { ConversationOrganizationResult } from "../../../conversation-organize-contract.js";
2
3
  export interface SidebarManageSelection {
3
4
  readonly directoryIds?: readonly string[];
4
5
  readonly taskIds: readonly string[];
@@ -20,6 +21,10 @@ export declare function pruneManagedSelection(selection: SidebarManageSelection,
20
21
  */
21
22
  export declare function describeManagedAction(selection: SidebarManageSelection): string;
22
23
  export declare function describeManagedResult(selection: SidebarManageSelection): string;
24
+ export declare function describeConversationOrganizationOutcome(result: ConversationOrganizationResult): {
25
+ label: string;
26
+ reason: string;
27
+ };
23
28
  export declare function describeOrganizeOutcome(result: {
24
29
  movedSessions: readonly unknown[];
25
30
  categorizedTasks: readonly unknown[];
@@ -106,13 +106,37 @@ export function describeManagedResult(selection) {
106
106
  parts.push(`归档 ${selection.sessionIds.length} 个终端`);
107
107
  return parts.join("、") || "处理所选项目";
108
108
  }
109
+ export function describeConversationOrganizationOutcome(result) {
110
+ const changes = [];
111
+ if (result.createdTasks.length)
112
+ changes.push(`新建 ${result.createdTasks.length} 个任务`);
113
+ if (result.updatedTasks.length)
114
+ changes.push(`更新 ${result.updatedTasks.length} 个系统任务标题`);
115
+ if (result.reusedTasks.length)
116
+ changes.push(`复用 ${new Set(result.reusedTasks.map(task => task.taskId)).size} 个任务`);
117
+ if (result.movedSessions.length)
118
+ changes.push(`归入 ${result.movedSessions.length} 个会话`);
119
+ const label = result.unchanged ? "会话内容和归属未变化,没有重复建任务或调用模型"
120
+ : changes.length ? `${result.cancelled || result.error ? "部分完成:" : "已"}${changes.join("、")}`
121
+ : result.cancelled ? "已取消后续会话归类" : result.error ? "会话归类未完成" : result.eligibleSessions ? "会话尚未得到明确工作分组,本次未作调整" : "没有符合范围且有正文的工作会话";
122
+ const details = [`已检查 ${result.analyzedSessions}/${result.eligibleSessions} 个工作会话`,
123
+ result.skippedManualSessions ? `跳过 ${result.skippedManualSessions} 个人工任务中的会话` : "",
124
+ result.skippedSessions ? `排除 ${result.skippedSessions} 个归档/私聊/自动化/系统日志或无正文会话` : "",
125
+ result.unassignedSessions.length ? `${result.unassignedSessions.length} 个会话尚未确定主题` : "",
126
+ result.refused.length ? `${result.refused.length} 项未处理:${result.refused[0]?.reason}` : "", result.error ?? ""].filter(Boolean);
127
+ return { label, reason: details.join(";") };
128
+ }
109
129
  export function describeOrganizeOutcome(result) {
110
130
  const moved = result.movedSessions.length;
111
131
  const categorized = result.categorizedTasks.length;
112
132
  const reason = result.refused[0]?.reason ?? "";
133
+ const details = [
134
+ result.eligibleTasks === undefined ? "" : `已检查 ${result.eligibleTasks} 个任务`,
135
+ result.skippedTasks ? `已跳过 ${result.skippedTasks} 个用户自定标题的任务` : "",
136
+ ].filter(Boolean);
113
137
  if (!moved && !categorized) {
114
- return { label: reason || (result.eligibleTasks === 0 ? "没有符合归类范围的任务" : "所选标题已归类"),
115
- reason: reason || (result.skippedTasks ? `已跳过 ${result.skippedTasks} 个用户自定标题的任务` : "") };
138
+ return { label: reason ? "未调整任何任务或会话" : (result.eligibleTasks === 0 ? "没有符合归类范围的任务" : "未发现可安全归类的项"),
139
+ reason: [...details, reason].filter(Boolean).join(";") };
116
140
  }
117
141
  const parts = [];
118
142
  if (categorized)
@@ -121,7 +145,7 @@ export function describeOrganizeOutcome(result) {
121
145
  parts.push(`归属 ${moved} 个会话`);
122
146
  return {
123
147
  label: `已${parts.join("、")}`,
124
- reason: result.refused.length ? `另有 ${result.refused.length} 项未处理:${reason}` : "",
148
+ reason: [...details, result.refused.length ? `另有 ${result.refused.length} 项未处理:${reason}` : ""].filter(Boolean).join(";"),
125
149
  };
126
150
  }
127
151
  export function standaloneManagedSessionIds(selection, groups) {
@@ -1,9 +1,13 @@
1
+ import type { ConversationTurn } from "../types.js";
1
2
  /**
2
3
  * 「正在执行」的客户端推导:只允许对服务端下发的运行事实与锚点时刻做展示差分。
3
4
  * 锚点(turnStartedAt / lastActivityAt)缺失时一律不显示时长,前端不本地起表伪造运行中。
4
5
  */
5
6
  /** 服务端会话快照里本模块关心的字段(structured 与 PTY 共用同一形状)。 */
6
7
  export interface RunningActivityShape {
8
+ readonly id?: string;
9
+ /** Must be the transcript for this source session, never a team's merged feed. */
10
+ readonly messages?: readonly ConversationTurn[];
7
11
  readonly status?: string;
8
12
  readonly archived?: boolean;
9
13
  readonly permissionBlocked?: boolean;
@@ -16,6 +20,7 @@ export interface RunningActivityShape {
16
20
  readonly ptyRunning?: boolean;
17
21
  readonly structuredState?: {
18
22
  readonly inFlight?: boolean;
23
+ readonly activeRequestId?: string | null;
19
24
  readonly phase?: string;
20
25
  readonly turnStartedAt?: string | null;
21
26
  readonly lastActivityAt?: string | null;
@@ -41,23 +46,31 @@ export declare function isRunningNow(source: RunningActivityShape | RunningRunSh
41
46
  /** 会话侧运行事实:structured 看本轮 inFlight,PTY 看 ptyRunning,两者都不看本地计时。 */
42
47
  export declare function sessionRunning(source: RunningActivityShape | null | undefined): boolean;
43
48
  /**
44
- * 分阶段:等待审批/输入 > 已接收排队 > 正在执行 > 结束。
45
- * 非运行中一律 idle,调用方必须据此立即收敛状态条,不留残留。
49
+ * 分阶段:终态/归档 > 等待审批/输入 > 正在执行 > 已接收排队 > 结束。
50
+ * 队列是下一项工作,不能掩盖本轮执行或待答。终态保留在完整投影中,
46
51
  * `runMode` 给团队 run:那里的 status 就是运行事实本身。会话侧不行——裸 shell 的
47
52
  * status 恒为 running,必须走 sessionRunning(structured inFlight / PTY ptyRunning)。
48
53
  */
49
54
  export declare function computeRunningPhase(source: RunningActivityShape | null | undefined, runMode?: boolean): RunningPhase;
50
55
  /** 距最近一次活动已静默多久;锚点缺失或不在运行中时为 0。 */
51
- export declare function silenceDurationMs(source: RunningActivityShape | RunningRunShape | null | undefined, nowMs: number): number;
56
+ export declare function silenceDurationMs(source: RunningActivityShape | RunningRunShape | null | undefined, nowMs: number, runMode?: boolean): number;
52
57
  /** 静默超阈值时的提示文案;不满足条件返回 null。 */
53
- export declare function silenceNotice(source: RunningActivityShape | RunningRunShape | null | undefined, nowMs: number, thresholdMs?: number): string | null;
58
+ export declare function silenceNotice(source: RunningActivityShape | RunningRunShape | null | undefined, nowMs: number, thresholdMs?: number, runMode?: boolean): string | null;
54
59
  /** 与 formatElapsedShort 不同:面向「已 N 分钟」这类自然语言读数。 */
55
60
  export declare function formatMinutes(ms: number): string;
56
61
  export declare const PHASE_LABELS: Readonly<Record<RunningPhase, string>>;
57
- /**
58
- * 状态条的完整文案。运行中恒有文字(阶段 + 已运行时长),静默超阈值时追加无新消息提示,
59
- * 这样「还在跑」和「已经停了」在界面上一眼可分。
60
- */
62
+ /** Distinct display stages; computeRunningPhase remains compatible with avatar consumers. */
63
+ export type RunningStatusStage = RunningPhase | "tool" | "output" | "thinking" | "background" | "failed" | "stopped";
64
+ export interface RunningStatusProjection {
65
+ readonly phase: RunningPhase;
66
+ readonly stage: RunningStatusStage;
67
+ readonly label: string;
68
+ readonly action: string | null;
69
+ readonly toolId?: string;
70
+ readonly active: boolean;
71
+ }
72
+ export declare function projectRunningStatus(source: RunningActivityShape | null | undefined, runMode?: boolean): RunningStatusProjection;
73
+ /** One authoritative label, with server-anchored elapsed/silence reads. */
61
74
  export declare function runningStatusText(source: RunningActivityShape | null | undefined, nowMs: number, runMode?: boolean): string;
62
- /** 团队侧文案:run 级锚点,语义与会话侧一致。 */
75
+ /** 团队只投影 run 自身,不借用任何成员会话的工具或输出。 */
63
76
  export declare function runStatusText(source: RunningRunShape | null | undefined, nowMs: number): string;
@@ -1,9 +1,8 @@
1
- /**
2
- * 「正在执行」的客户端推导:只允许对服务端下发的运行事实与锚点时刻做展示差分。
3
- * 锚点(turnStartedAt / lastActivityAt)缺失时一律不显示时长,前端不本地起表伪造运行中。
4
- */
1
+ import { currentRunningAction } from "./running-current-action.js";
5
2
  /** 静默起显阈值:超过后从「正在执行」升级为「仍在运行 · 已 N 分钟无新消息」。 */
6
3
  export const SILENCE_NOTICE_MS = 60_000;
4
+ const TERMINAL_STATUSES = new Set(["failed", "stopped", "exited", "done"]);
5
+ const WAITING_STATUSES = new Set(["waiting-input", "waiting_input", "waiting_user", "awaiting_approval"]);
7
6
  const RUNNING_STATUSES = new Set([
8
7
  "running", "thinking", "waiting-input", "waiting_input",
9
8
  // 团队 run 的等待相位仍是「这一轮没结束」,收敛只发生在 done/failed/stopped。
@@ -36,6 +35,8 @@ export function isRunningNow(source) {
36
35
  if (source.archived)
37
36
  return false;
38
37
  const activity = source;
38
+ if (activity.archived || TERMINAL_STATUSES.has(activity.status ?? ""))
39
+ return false;
39
40
  if (activity.permissionBlocked || activity.pendingEscalation)
40
41
  return true;
41
42
  if (activity.structuredState?.inFlight)
@@ -45,31 +46,34 @@ export function isRunningNow(source) {
45
46
  }
46
47
  /** 会话侧运行事实:structured 看本轮 inFlight,PTY 看 ptyRunning,两者都不看本地计时。 */
47
48
  export function sessionRunning(source) {
48
- if (!source || source.archived)
49
+ if (!source || source.archived || TERMINAL_STATUSES.has(source.status ?? ""))
49
50
  return false;
50
51
  return Boolean(source.structuredState?.inFlight) || source.ptyRunning === true;
51
52
  }
52
53
  /**
53
- * 分阶段:等待审批/输入 > 已接收排队 > 正在执行 > 结束。
54
- * 非运行中一律 idle,调用方必须据此立即收敛状态条,不留残留。
54
+ * 分阶段:终态/归档 > 等待审批/输入 > 正在执行 > 已接收排队 > 结束。
55
+ * 队列是下一项工作,不能掩盖本轮执行或待答。终态保留在完整投影中,
55
56
  * `runMode` 给团队 run:那里的 status 就是运行事实本身。会话侧不行——裸 shell 的
56
57
  * status 恒为 running,必须走 sessionRunning(structured inFlight / PTY ptyRunning)。
57
58
  */
58
59
  export function computeRunningPhase(source, runMode = false) {
59
- if (!source)
60
+ if (!source || source.archived || TERMINAL_STATUSES.has(source.status ?? ""))
60
61
  return "idle";
61
- // 排队未空 = 服务端还欠这一轮回复,属于「已接收」;不能被会话级 idle 吞掉。
62
- if ((source.queuedMessages?.length ?? 0) > 0)
63
- return "received";
64
- if (!(runMode ? isRunningNow(source) : sessionRunning(source)))
65
- return "idle";
66
- if (source.permissionBlocked || source.pendingEscalation)
62
+ if (source.permissionBlocked || source.pendingEscalation || WAITING_STATUSES.has(source.status ?? ""))
67
63
  return "waiting";
68
- return "executing";
64
+ if (runMode ? isRunningNow(source) : sessionRunning(source)) {
65
+ if (!runMode && currentRunningAction(source)?.kind === "waiting")
66
+ return "waiting";
67
+ return "executing";
68
+ }
69
+ if ((source.queuedMessages?.length ?? 0) > 0 || (runMode && source.status === "starting"))
70
+ return "received";
71
+ return "idle";
69
72
  }
70
73
  /** 距最近一次活动已静默多久;锚点缺失或不在运行中时为 0。 */
71
- export function silenceDurationMs(source, nowMs) {
72
- if (!isRunningNow(source) && !sessionRunning(source))
74
+ export function silenceDurationMs(source, nowMs, runMode = false) {
75
+ // Session status=running alone is only a live shell, not an active turn.
76
+ if (!(runMode ? isRunningNow(source) : sessionRunning(source)))
73
77
  return 0;
74
78
  const last = lastActivityAtMs(source);
75
79
  if (last === null)
@@ -77,8 +81,8 @@ export function silenceDurationMs(source, nowMs) {
77
81
  return Math.max(0, nowMs - last);
78
82
  }
79
83
  /** 静默超阈值时的提示文案;不满足条件返回 null。 */
80
- export function silenceNotice(source, nowMs, thresholdMs = SILENCE_NOTICE_MS) {
81
- const silence = silenceDurationMs(source, nowMs);
84
+ export function silenceNotice(source, nowMs, thresholdMs = SILENCE_NOTICE_MS, runMode = false) {
85
+ const silence = silenceDurationMs(source, nowMs, runMode);
82
86
  if (silence < thresholdMs)
83
87
  return null;
84
88
  return `仍在运行 · 已 ${formatMinutes(silence)}无新消息`;
@@ -101,34 +105,46 @@ export const PHASE_LABELS = {
101
105
  waiting: "等待你的输入",
102
106
  idle: "",
103
107
  };
104
- /**
105
- * 状态条的完整文案。运行中恒有文字(阶段 + 已运行时长),静默超阈值时追加无新消息提示,
106
- * 这样「还在跑」和「已经停了」在界面上一眼可分。
107
- */
108
- export function runningStatusText(source, nowMs, runMode = false) {
108
+ export function projectRunningStatus(source, runMode = false) {
109
109
  const phase = computeRunningPhase(source, runMode);
110
- if (phase === "idle")
110
+ const base = { phase, stage: phase, label: PHASE_LABELS[phase], action: null, active: phase !== "idle" };
111
+ if (!source || source.archived)
112
+ return base;
113
+ if (source.status === "failed" || source.status === "stopped") {
114
+ return { ...base, stage: source.status, label: source.status === "failed" ? "执行失败" : "已停止", active: false };
115
+ }
116
+ if (phase === "received")
117
+ return { ...base, label: "已接收 · 排队中" };
118
+ if (phase === "waiting")
119
+ return { ...base, label: source.permissionBlocked || source.pendingEscalation || source.status === "awaiting_approval" ? "等待你确认操作" : PHASE_LABELS.waiting };
120
+ if (phase !== "executing" || runMode)
121
+ return base;
122
+ if (source.structuredState?.phase === "background")
123
+ return { ...base, stage: "background", label: "等待后台任务" };
124
+ const action = currentRunningAction(source);
125
+ return action ? { ...base, stage: action.kind, label: action.kind === "tool" ? "正在使用工具" : action.label, action: action.kind === "tool" ? action.label : null, toolId: action.toolId } : base;
126
+ }
127
+ /** One authoritative label, with server-anchored elapsed/silence reads. */
128
+ export function runningStatusText(source, nowMs, runMode = false) {
129
+ const projection = projectRunningStatus(source, runMode);
130
+ if (!projection.label)
111
131
  return "";
112
- const parts = [PHASE_LABELS[phase]];
113
- const started = turnStartedAtMs(source);
132
+ const parts = [projection.label];
133
+ if (projection.action)
134
+ parts.push(projection.action);
135
+ if (!projection.active || projection.phase === "received")
136
+ return parts.join(" · ");
137
+ const started = runMode
138
+ ? parseTime(source?.turnStartedAt) ?? parseTime(source?.startedAt)
139
+ : turnStartedAtMs(source);
114
140
  if (started !== null)
115
141
  parts.push(`已运行 ${formatMinutes(Math.max(0, nowMs - started))}`);
116
- const silence = silenceNotice(source, nowMs);
117
- if (silence)
118
- parts.push(silence);
142
+ const silence = silenceDurationMs(source, nowMs, runMode);
143
+ if (silence >= SILENCE_NOTICE_MS)
144
+ parts.push(`已 ${formatMinutes(silence)}无新消息`);
119
145
  return parts.join(" · ");
120
146
  }
121
- /** 团队侧文案:run 级锚点,语义与会话侧一致。 */
147
+ /** 团队只投影 run 自身,不借用任何成员会话的工具或输出。 */
122
148
  export function runStatusText(source, nowMs) {
123
- if (!isRunningNow(source))
124
- return "";
125
- const waiting = source?.status === "awaiting_approval" || source?.status === "waiting_user";
126
- const parts = [waiting ? PHASE_LABELS.waiting : PHASE_LABELS.executing];
127
- const started = parseTime(source?.turnStartedAt) ?? parseTime(source?.startedAt);
128
- if (started !== null)
129
- parts.push(`已运行 ${formatMinutes(Math.max(0, nowMs - started))}`);
130
- const silence = silenceNotice(source, nowMs);
131
- if (silence)
132
- parts.push(silence);
133
- return parts.join(" · ");
149
+ return runningStatusText(source, nowMs, true);
134
150
  }
@@ -0,0 +1,12 @@
1
+ import type { RunningActivityShape } from "./running-activity.js";
2
+ export interface CurrentRunningAction {
3
+ readonly kind: "tool" | "output" | "thinking" | "waiting";
4
+ readonly label: string;
5
+ readonly toolId?: string;
6
+ }
7
+ /**
8
+ * Only inspect one source session's current request. Missing request/time evidence
9
+ * falls back to generic execution; an old unfinished tool is not current activity.
10
+ * Text contents and tool arguments never participate in phase classification.
11
+ */
12
+ export declare function currentRunningAction(source: RunningActivityShape): CurrentRunningAction | null;
@@ -0,0 +1,74 @@
1
+ /** A tool name is an identifier, never a command/input/description excerpt. */
2
+ function toolLabel(tool) {
3
+ const name = /^[\p{L}\p{N}_.:-]{1,64}$/u.test(tool.name) ? tool.name : "工具调用";
4
+ switch (tool.activity?.kind) {
5
+ case "read_file": return `读取文件 · ${name}`;
6
+ case "edit_file": return `修改文件 · ${name}`;
7
+ case "run_command": return `运行命令 · ${name}`;
8
+ default: return name;
9
+ }
10
+ }
11
+ /**
12
+ * Only inspect one source session's current request. Missing request/time evidence
13
+ * falls back to generic execution; an old unfinished tool is not current activity.
14
+ * Text contents and tool arguments never participate in phase classification.
15
+ */
16
+ export function currentRunningAction(source) {
17
+ if (source.archived || ["failed", "stopped", "done", "exited"].includes(source.status ?? "")
18
+ || !(source.structuredState?.inFlight || source.ptyRunning))
19
+ return null;
20
+ const messages = source.messages ?? [];
21
+ const request = source.structuredState?.activeRequestId;
22
+ const started = Date.parse(source.structuredState?.turnStartedAt ?? source.turnStartedAt ?? "");
23
+ let start = messages.length - 1;
24
+ while (start >= 0 && !(messages[start].role === "user"
25
+ && messages[start].content.some(block => block.type === "text" && !block.__subagent && block.text.trim())))
26
+ start--;
27
+ const user = messages[start];
28
+ if (request && user?.requestId && user.requestId !== request)
29
+ return null;
30
+ const sameRequest = Boolean(request && user?.requestId === request);
31
+ const isCurrent = (turn) => {
32
+ if (turn.author?.sessionId && turn.author.sessionId !== source.id)
33
+ return false;
34
+ if (request && turn.requestId)
35
+ return turn.requestId === request;
36
+ if (sameRequest)
37
+ return true;
38
+ const created = Date.parse(turn.createdAt ?? "");
39
+ return Number.isFinite(started) && Number.isFinite(created) && created >= started;
40
+ };
41
+ const pending = new Map();
42
+ const completed = new Set();
43
+ let latest = null;
44
+ for (let index = start + 1; index < messages.length; index++) {
45
+ const turn = messages[index];
46
+ if (!isCurrent(turn))
47
+ continue;
48
+ for (const block of turn.content) {
49
+ if (block.__subagent)
50
+ continue;
51
+ if (block.type === "tool_result") {
52
+ completed.add(block.tool_use_id);
53
+ latest = null;
54
+ }
55
+ if (turn.role !== "assistant")
56
+ continue;
57
+ // completedAt ends the message stream, not the invoked tool. Only its
58
+ // paired result retires a tool or a question awaiting the user.
59
+ if (block.type === "tool_use") {
60
+ pending.set(block.id, block);
61
+ latest = null;
62
+ }
63
+ else if (!turn.completedAt && block.type === "text" && block.text.trim())
64
+ latest = { kind: "output", label: "正在输出" };
65
+ else if (!turn.completedAt && block.type === "thinking")
66
+ latest = { kind: "thinking", label: "正在思考" };
67
+ }
68
+ }
69
+ const tool = [...pending.values()].reverse().find(block => !completed.has(block.id));
70
+ if (tool?.semantic?.kind === "question_request" || tool?.name === "AskUserQuestion") {
71
+ return { kind: "waiting", toolId: tool.id, label: "等待你的输入" };
72
+ }
73
+ return tool ? { kind: "tool", toolId: tool.id, label: toolLabel(tool) } : latest;
74
+ }
@@ -17,6 +17,10 @@ export declare function getPlushAvatarChunk(requestedHash?: string): {
17
17
  /** Blocking, content-versioned device preference bootstrap. */
18
18
  export declare function getThemePreloadAsset(): VersionedWebAsset;
19
19
  export interface BrowserModuleManifest {
20
+ buildId?: string;
21
+ imports?: Record<string, string>;
22
+ dependencies?: Record<string, readonly string[]>;
23
+ integrity?: Record<string, string>;
20
24
  entry: string;
21
25
  eager: readonly string[];
22
26
  files: readonly string[];
@@ -24,32 +24,33 @@ function readScriptCache() {
24
24
  // Retain the last disk read, or fall back to the embedded copy.
25
25
  }
26
26
  }
27
- function injectRuntimeValues(source, configPath, chunkHash, plushHash, entry) {
27
+ function injectRuntimeValues(source, configPath, chunkHash, plushHash, manifest) {
28
28
  return source
29
29
  // This is a JS string literal, not HTML text. JSON escaping also handles
30
30
  // quotes, backslashes and newlines in nonstandard configuration paths.
31
31
  .replace('"${wandConfigPath}"', JSON.stringify(configPath))
32
32
  .replace("${aiTeamsChunkSrc}", `/assets/ai-teams.js?v=${chunkHash}`)
33
33
  .replace("${plushAvatarChunkSrc}", `/assets/plush-avatar.js?v=${plushHash}`)
34
- .replace("${browserModuleSrc}", `/assets/${entry}`);
34
+ .replace("${browserModuleSrc}", `/assets/${manifest.entry}`)
35
+ .replace('"${browserModuleImports}"', JSON.stringify(manifest.imports ?? {}));
35
36
  }
36
37
  export function getScriptAsset(configPath, requestedHash) {
37
38
  readScriptCache();
38
39
  const teamHash = getAiTeamsChunk().hash;
39
40
  const plushHash = getPlushAvatarChunk().hash;
40
- const entry = getBrowserModuleManifest().entry;
41
- const chunkHash = `${teamHash}:${plushHash}:${entry}`;
41
+ const manifest = getBrowserModuleManifest();
42
+ const chunkHash = `${teamHash}:${plushHash}:${manifest.buildId ?? manifest.entry}`;
42
43
  if (!_scriptAsset || _scriptAsset.source !== _scriptCache
43
44
  || _scriptAsset.configPath !== configPath || _scriptAsset.chunkHash !== chunkHash) {
44
45
  _scriptAsset = {
45
- ...versionWebAsset(injectRuntimeValues(_scriptCache, configPath, teamHash, plushHash, entry)),
46
+ ...versionWebAsset(injectRuntimeValues(_scriptCache, configPath, teamHash, plushHash, manifest)),
46
47
  source: _scriptCache, configPath, chunkHash,
47
48
  };
48
49
  }
49
50
  if (requestedHash && requestedHash !== _scriptAsset.hash) {
50
51
  if (!_embeddedScriptAsset || _embeddedScriptAsset.configPath !== configPath) {
51
52
  _embeddedScriptAsset = {
52
- ...versionWebAsset(injectRuntimeValues(EMBEDDED_WEB_ASSETS.scriptsJs, configPath, embeddedAiTeamsChunk.hash, embeddedPlushAvatarChunk.hash, EMBEDDED_WEB_ASSETS.browserModuleManifest.entry)),
53
+ ...versionWebAsset(injectRuntimeValues(EMBEDDED_WEB_ASSETS.scriptsJs, configPath, embeddedAiTeamsChunk.hash, embeddedPlushAvatarChunk.hash, EMBEDDED_WEB_ASSETS.browserModuleManifest)),
53
54
  configPath,
54
55
  };
55
56
  }
@@ -123,10 +124,18 @@ export function getBrowserModuleManifest() {
123
124
  if (key !== browserManifestKey) {
124
125
  const next = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
125
126
  if (!next.files.every(file => /^modules\/[a-f0-9]{16}\/[A-Za-z0-9_-]+\.js$/.test(file))
126
- || !next.files.includes(next.entry) || !next.eager.every(file => next.files.includes(file))) {
127
+ || !next.files.includes(next.entry) || !next.eager.every(file => next.files.includes(file))
128
+ || !Object.values(next.imports ?? {}).every(file => next.files.includes(file))
129
+ || !Object.entries(next.dependencies ?? {}).every(([file, deps]) => next.files.includes(file) && deps.every(dep => next.files.includes(dep)))) {
127
130
  throw new Error("Invalid browser module manifest");
128
131
  }
129
- const files = new Map(next.files.map(file => [file, fs.readFileSync(path.join(__dirname, "content", file), "utf8")]));
132
+ const files = new Map(next.files.map(file => {
133
+ const content = fs.readFileSync(path.join(__dirname, "content", file), "utf8");
134
+ if (next.integrity && createHash("sha256").update(content).digest("hex") !== next.integrity[file]) {
135
+ throw new Error("Browser module integrity mismatch");
136
+ }
137
+ return [file, content];
138
+ }));
130
139
  browserBuilds.unshift(files);
131
140
  browserBuilds.splice(2);
132
141
  browserManifest = next;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "4.94.0",
3
+ "version": "4.95.0",
4
4
  "description": "A web console for Claude Code, Codex, OpenCode, and other local CLI tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "skills/wand-decision",
17
+ "skills/wand-system",
17
18
  "browser-extension",
18
19
  "scripts/install-render-binary.js",
19
20
  "scripts/recover-web-update.sh",
@@ -40,6 +41,7 @@
40
41
  "check:body": "node scripts/check-node-version.js && node scripts/bundle-tailwind.js && node scripts/bundle-xterm.js && node scripts/bundle-qrcode.js && node scripts/bundle-browser.js && npm run generate:web-assets && tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.browser.json && tsc --noEmit -p tsconfig.react.json",
41
42
  "test": "node scripts/check-node-version.js && node scripts/run-tests.js",
42
43
  "check:bundle-budget": "node scripts/check-node-version.js && node scripts/check-bundle-budget.js",
44
+ "budget:accept": "node scripts/check-node-version.js && node scripts/check-bundle-budget.js --accept-baseline",
43
45
  "sync:brand-assets": "node scripts/sync-brand-assets.js",
44
46
  "check:brand-assets": "node scripts/sync-brand-assets.js --check",
45
47
  "audit:remnants": "node scripts/audit-legacy-remnants.js",
@@ -57,7 +59,7 @@
57
59
  ],
58
60
  "license": "MIT",
59
61
  "engines": {
60
- "node": ">=24.21.0"
62
+ "node": ">=22.19.0"
61
63
  },
62
64
  "dependencies": {
63
65
  "@anthropic-ai/sdk": "^0.81.0",
@@ -1,5 +1,4 @@
1
1
  import { accessSync, constants, chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
2
- import { randomUUID } from "node:crypto";
3
2
  import { spawn, spawnSync } from "node:child_process";
4
3
  import path from "node:path";
5
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -57,23 +56,28 @@ export function chooseInstallPrefix(defaultPrefix, home, configPath) {
57
56
  return writable ? defaultPrefix : path.join(home, ".wand/runtime/npm");
58
57
  }
59
58
 
60
- function acquireRecoveryLock(root) {
61
- const lockPath = path.join(root, ".wand-update-lock");
62
- mkdirSync(root, { recursive: true });
63
- const token = randomUUID();
64
- try { mkdirSync(lockPath, { mode: 0o700 }); }
65
- catch (error) {
66
- if (error.code === "EEXIST") throw new Error("另一个更新仍持有安装锁,旧安装未修改。");
67
- throw error;
59
+ /** Same protocol as Web updates/start.sh, including a live parent's lease.
60
+ * Acquire before draining: a competing install must not close task admission.
61
+ * The module is copied into the independent recovery runtime, never loaded
62
+ * from the installation that this transaction is about to replace.
63
+ */
64
+ export async function withRecoveryLock(root, guardPath, action) {
65
+ const directory = path.dirname(guardPath);
66
+ const lockModule = path.join(directory, existsSync(path.join(directory, "deployment-lock.js"))
67
+ ? "deployment-lock.js" : "deployment-lock.ts");
68
+ const { acquireDeploymentLock, releaseDeploymentLock } = await import(pathToFileURL(lockModule).href);
69
+ const previous = process.env.WAND_DEPLOYMENT_LOCKS;
70
+ const inherited = previous ? JSON.parse(previous) : [];
71
+ if (!Array.isArray(inherited)) throw new Error("无效的部署锁回执。");
72
+ const lock = acquireDeploymentLock(path.join(root, ".wand-update-lock"), { inherited });
73
+ try {
74
+ process.env.WAND_DEPLOYMENT_LOCKS = JSON.stringify([...inherited, lock]);
75
+ return await action();
76
+ } finally {
77
+ if (previous === undefined) delete process.env.WAND_DEPLOYMENT_LOCKS;
78
+ else process.env.WAND_DEPLOYMENT_LOCKS = previous;
79
+ releaseDeploymentLock(lock);
68
80
  }
69
- try { writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify({ pid: process.pid, token, createdAt: Date.now() }), { mode: 0o600 }); }
70
- catch (error) { rmSync(lockPath, { recursive: true, force: true }); throw error; }
71
- return () => {
72
- try {
73
- const owner = JSON.parse(readFileSync(path.join(lockPath, "owner.json"), "utf8"));
74
- if (owner.pid === process.pid && owner.token === token) rmSync(lockPath, { recursive: true, force: true });
75
- } catch { /* Never remove a changed or unknown lock. */ }
76
- };
77
81
  }
78
82
 
79
83
  function copyEntry(source, destination) {
@@ -109,6 +113,10 @@ function stopManagedService(platform, scope, serviceFile) {
109
113
  }
110
114
 
111
115
  export async function recoverPreparedInstallation(options) {
116
+ return withRecoveryLock(options.root, options.guardPath, () => applyPreparedInstallation(options));
117
+ }
118
+
119
+ async function applyPreparedInstallation(options) {
112
120
  const { prepared, root, bin, configPath, scope, platform, healthPath, guardPath } = options;
113
121
  const health = await import(pathToFileURL(healthPath).href);
114
122
  const guard = await import(pathToFileURL(guardPath).href);
@@ -118,7 +126,6 @@ export async function recoverPreparedInstallation(options) {
118
126
  try { originalPid = JSON.parse(readFileSync(path.join(configDir, "wand.pid"), "utf8")).pid; }
119
127
  catch (error) { if (error.code !== "ENOENT") throw error; }
120
128
  const wasRunning = alive(originalPid);
121
- const unlock = acquireRecoveryLock(root);
122
129
  let stageRoot, backupRoot, backup, oldBin, previousService;
123
130
  let movedOld = false, promoted = false, registered = false;
124
131
  const serviceFile = scope === "standalone" ? null : platform === "Darwin"
@@ -244,7 +251,7 @@ export async function recoverPreparedInstallation(options) {
244
251
  [...(privileged ? ["systemctl"] : []), ...(scope === "user" ? ["--user"] : []), "start", "wand.service"], { stdio: "inherit" });
245
252
  } else if (wasRunning && !alive(originalPid) && savedBin) await startStandalone();
246
253
  throw error;
247
- } finally { unlock(); }
254
+ }
248
255
  }
249
256
 
250
257
  let main = false;
@@ -13,7 +13,7 @@ copy_or_fetch_script update-node.sh
13
13
  copy_or_fetch_script update-target.js
14
14
  copy_or_fetch_script recover-web-update.mjs
15
15
  . "$RECOVERY_DIR/update-node.sh"
16
- wand_ensure_update_node 24.21.0 || { echo "需要 Node.js >=24.21.0;旧服务与安装未修改。" >&2; exit 1; }
16
+ wand_ensure_update_node "$WAND_MINIMUM_NODE_VERSION" || { echo "需要 Node.js >=$WAND_MINIMUM_NODE_VERSION;旧服务与安装未修改。" >&2; exit 1; }
17
17
 
18
18
  PLATFORM="$(uname -s)"
19
19
  SYSTEM=0
@@ -83,8 +83,9 @@ GLOBAL_BIN="$INSTALL_PREFIX/bin/wand"
83
83
  echo "[wand-recovery] preparing $VERSION ($CHANNEL subscription); current service stays online"
84
84
  npm install -g --engine-strict --prefix "$RECOVERY_DIR/prepared" "@co0ontty/wand@$VERSION"
85
85
  PREPARED="$RECOVERY_DIR/prepared/lib/node_modules/@co0ontty/wand"
86
- for name in core-status-cli update-health update-legacy; do
87
- if [ -f "$SCRIPT_DIR/../dist/$name.js" ]; then
86
+ for name in core-status-cli update-health update-legacy deployment-lock; do
87
+ # A checkout may contain an older dist; use the matching source protocol there.
88
+ if [ ! -f "$SCRIPT_DIR/../src/$name.ts" ] && [ -f "$SCRIPT_DIR/../dist/$name.js" ]; then
88
89
  cp "$SCRIPT_DIR/../dist/$name.js" "$RECOVERY_DIR/$name.js"
89
90
  case "$name" in core-status-cli) GUARD_PATH="$RECOVERY_DIR/$name.js" ;; update-health) HEALTH_PATH="$RECOVERY_DIR/$name.js" ;; update-legacy) LEGACY_PATH="$RECOVERY_DIR/$name.js" ;; esac
90
91
  else
@@ -114,10 +115,12 @@ else
114
115
  fi
115
116
  # No sudo around the transaction: package, DB and WAL stay owned by the caller.
116
117
  # Only system-service registration prompts for its existing required privilege.
117
- "$NODE_BIN" --input-type=module - "$GUARD_PATH" "$LEGACY_PATH" "$CONFIG_PATH" "$ALLOW_STOPPED" "$NODE_BIN" "$RECOVERY_DIR/recover-web-update.mjs" apply "$PREPARED" "$GLOBAL_ROOT" "$GLOBAL_BIN" "$CONFIG_PATH" "$SCOPE" "$PLATFORM" "$HEALTH_PATH" "$GUARD_PATH" <<'NODE'
118
+ "$NODE_BIN" --input-type=module - "$GUARD_PATH" "$LEGACY_PATH" "$CONFIG_PATH" "$ALLOW_STOPPED" "$GLOBAL_ROOT" "$NODE_BIN" "$RECOVERY_DIR/recover-web-update.mjs" apply "$PREPARED" "$GLOBAL_ROOT" "$GLOBAL_BIN" "$CONFIG_PATH" "$SCOPE" "$PLATFORM" "$HEALTH_PATH" "$GUARD_PATH" <<'NODE'
118
119
  import { pathToFileURL } from "node:url";
119
- const [guardPath, legacyPath, configPath, stopped, ...command] = process.argv.slice(2);
120
+ const [guardPath, legacyPath, configPath, stopped, root, ...command] = process.argv.slice(2);
120
121
  const guard = await import(pathToFileURL(guardPath).href);
121
122
  const { waitForCompatibleUpdate } = await import(pathToFileURL(legacyPath).href);
122
- process.exitCode = await waitForCompatibleUpdate(configPath, 300, command, stopped === "1", guard);
123
+ const { withRecoveryLock } = await import(pathToFileURL(command[1]).href);
124
+ process.exitCode = await withRecoveryLock(root, guardPath,
125
+ () => waitForCompatibleUpdate(configPath, 300, command, stopped === "1", guard));
123
126
  NODE
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Sourced by install/recovery. Changes only this process's PATH; never removes an old Node runtime.
3
+ # Keep in sync with package.json engines; .nvmrc is only the CI/build pin.
4
+ WAND_MINIMUM_NODE_VERSION="22.19.0"
3
5
  wand_node_satisfies() {
4
6
  local executable="$1"
5
7
  local required="$2"
@@ -25,7 +27,7 @@ wand_use_existing_node() {
25
27
  # nvm installs and Homebrew's versioned prefixes are user-selected runtimes.
26
28
  # Do not run arbitrary executables discovered in the project or rewrite nvm aliases.
27
29
  for candidate in "${NVM_DIR:-$HOME/.nvm}"/versions/node/v*/bin/node \
28
- /opt/homebrew/opt/node@24/bin/node /usr/local/opt/node@24/bin/node \
30
+ /opt/homebrew/opt/node@{22,24,26}/bin/node /usr/local/opt/node@{22,24,26}/bin/node \
29
31
  /opt/homebrew/bin/node /usr/local/bin/node; do
30
32
  [ -x "$candidate" ] || continue
31
33
  wand_node_satisfies "$candidate" "$required" || continue