@co0ontty/wand 4.83.1 → 4.84.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 (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
@@ -5,7 +5,11 @@ import { asyncRoute } from "./express-async.js";
5
5
  import { bodyObject, sendRouteError, text } from "./server-request.js";
6
6
  import { parseTaskAgent } from "./server-task-routes.js";
7
7
  import { resolveSystemAiContext } from "./session-ai-context.js";
8
- import { generateSiliconEmployeeDraft } from "./silicon-employee-draft.js";
8
+ import { employeeCliAvailable } from "./silicon-employee-dispatch.js";
9
+ import { EMPLOYEE_CREATION_DEFAULTS_PREF, resolveSiliconEmployeeDefaults, } from "./silicon-employee-defaults.js";
10
+ import { availableEmployeeProviders, generateSiliconEmployeeDraft, SiliconEmployeeDraftError, validateStructuredEmployeeDraft, } from "./silicon-employee-draft.js";
11
+ import { SESSION_PROVIDERS } from "./session-provider.js";
12
+ import { DEFAULT_WAND_TASK_AGENT_KIND } from "./task-types.js";
9
13
  import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "./employee-knowledge-types.js";
10
14
  /** 员工起草没有会话上下文,按「默认 provider」解析,与任务标题 / 提示词优化一致。 */
11
15
  function employeeDraftAiOptions(storage, config) {
@@ -21,10 +25,22 @@ function employeeDraftAiOptions(storage, config) {
21
25
  thinkingEffort: config.defaultThinkingEffort,
22
26
  }, config, storage.getSystemSiliconEmployee());
23
27
  }
24
- function sendEmployeeError(res, error) {
28
+ function sendEmployeeError(res, error, context) {
25
29
  const message = getErrorMessage(error, "硅基员工操作失败。");
26
30
  const status = /不存在/.test(message) ? 404 : 400;
27
- sendRouteError(res, error, "硅基员工操作失败。", status);
31
+ const code = error instanceof SiliconEmployeeDraftError ? error.code : undefined;
32
+ const field = error instanceof SiliconEmployeeDraftError ? error.field : undefined;
33
+ if (!code && !context?.expectation) {
34
+ sendRouteError(res, error, "硅基员工操作失败。", status);
35
+ return;
36
+ }
37
+ // 结构化可恢复错误:带 code/field,并回显原口语输入,客户端可继续编辑重试。
38
+ res.status(status).json({
39
+ error: message,
40
+ ...(code ? { code } : {}),
41
+ ...(field ? { field } : {}),
42
+ ...(context?.expectation ? { expectation: context.expectation } : {}),
43
+ });
28
44
  }
29
45
  /** 内置员工是 Wand 自己的执行者,不能被归档或删除。 */
30
46
  function rejectSystemEmployeeMutation(existing, action) {
@@ -82,7 +98,7 @@ export function parseSystemEmployeeAgents(value, existing) {
82
98
  throw new Error("该员工是内置的,身份不可修改。");
83
99
  }
84
100
  if (!Array.isArray(body.agents))
85
- throw new Error("请提供执行候选数组。");
101
+ throw new Error("请提供执行候选数组(CLI / SDK)。");
86
102
  if (body.agents.length === 0)
87
103
  throw new Error("至少需要一个执行候选。");
88
104
  if (body.agents.length > AI_TEAM_MAX_CANDIDATES) {
@@ -91,7 +107,7 @@ export function parseSystemEmployeeAgents(value, existing) {
91
107
  const agents = body.agents.map((rawAgent) => {
92
108
  const parsed = parseTaskAgent(rawAgent ?? {});
93
109
  if (!parsed)
94
- throw new Error("请选择有效的 CLI 工具。");
110
+ throw new Error("请选择有效的 CLI 工具或 SDK 执行引擎。");
95
111
  if (parsed.kind !== "structured")
96
112
  throw new Error("硅基员工只支持结构化会话。");
97
113
  return parsed;
@@ -120,7 +136,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
120
136
  agents = body.agents.map((rawAgent) => {
121
137
  const parsed = parseTaskAgent(rawAgent ?? {});
122
138
  if (!parsed)
123
- throw new Error("请选择有效的 CLI 工具。");
139
+ throw new Error("请选择有效的 CLI 工具或 SDK 执行引擎。");
124
140
  if (parsed.kind !== "structured")
125
141
  throw new Error("硅基员工只支持结构化会话。");
126
142
  return parsed;
@@ -132,7 +148,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
132
148
  else if (body.agent !== undefined && body.agent !== null) {
133
149
  const singleAgent = parseTaskAgent(body.agent);
134
150
  if (!singleAgent)
135
- throw new Error("请选择有效的 CLI 工具。");
151
+ throw new Error("请选择有效的 CLI 工具或 SDK 执行引擎。");
136
152
  if (singleAgent.kind !== "structured")
137
153
  throw new Error("硅基员工只支持结构化会话。");
138
154
  agents = [singleAgent];
@@ -162,11 +178,31 @@ export function parseSiliconEmployeeInput(value, existing, now) {
162
178
  }
163
179
  export function registerSiliconEmployeeRoutes(app, deps) {
164
180
  const { storage, notifyEmployeeChanged, config } = deps;
181
+ const isProviderAvailable = deps.isProviderAvailable ?? employeeCliAvailable;
182
+ /** 已保存的员工创建配置优先;没有就用服务端 defaultProvider;再没有就要求用户选择。 */
183
+ const savedEmployeeAgent = () => {
184
+ const raw = storage.getPreference(EMPLOYEE_CREATION_DEFAULTS_PREF, null);
185
+ if (!raw)
186
+ return null;
187
+ try {
188
+ const agent = parseTaskAgent(raw, undefined, DEFAULT_WAND_TASK_AGENT_KIND);
189
+ return agent?.kind === "structured" ? agent : null;
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ };
195
+ const resolveDefaults = () => resolveSiliconEmployeeDefaults({ savedAgent: savedEmployeeAgent(), config });
196
+ const allowedProviders = () => {
197
+ const available = availableEmployeeProviders(resolveDefaults().provider, isProviderAvailable);
198
+ return available.length ? available : [...SESSION_PROVIDERS];
199
+ };
165
200
  const generateDraft = deps.generateDraft ?? ((request) => generateSiliconEmployeeDraft(request.expectation, employeeDraftAiOptions(storage, config), {
166
201
  cwd: config?.defaultCwd || process.cwd(),
167
202
  language: config?.language ?? "",
168
203
  existingNames: request.existingNames,
169
- preferredProvider: config?.defaultProvider ?? "claude",
204
+ preferredProvider: resolveDefaults().provider,
205
+ isProviderAvailable,
170
206
  }));
171
207
  const notifyEmployee = (employeeId) => {
172
208
  try {
@@ -186,17 +222,55 @@ export function registerSiliconEmployeeRoutes(app, deps) {
186
222
  sendEmployeeError(res, error);
187
223
  }
188
224
  });
225
+ /** 两端唯一的默认执行配置读取点;未配置时如实返回需要用户选择。 */
226
+ app.get("/api/silicon-employees/draft/defaults", (_req, res) => {
227
+ res.json(resolveDefaults());
228
+ });
229
+ /** 显式保存员工创建默认配置(只落偏好,不创建员工实体)。 */
230
+ app.put("/api/silicon-employees/draft/defaults", (req, res) => {
231
+ try {
232
+ const agent = parseTaskAgent(req.body ?? {}, undefined, DEFAULT_WAND_TASK_AGENT_KIND);
233
+ if (!agent)
234
+ throw new Error("请选择有效的执行候选。");
235
+ if (agent.kind !== "structured")
236
+ throw new Error("硅基员工只支持结构化会话。");
237
+ storage.setPreference(EMPLOYEE_CREATION_DEFAULTS_PREF, {
238
+ provider: agent.provider,
239
+ model: agent.model,
240
+ thinkingEffort: agent.thinkingEffort,
241
+ mode: agent.mode,
242
+ kind: agent.kind,
243
+ ...(agent.engine ? { engine: agent.engine } : {}),
244
+ });
245
+ res.json(resolveDefaults());
246
+ }
247
+ catch (error) {
248
+ sendEmployeeError(res, error);
249
+ }
250
+ });
189
251
  app.post("/api/silicon-employees/draft", asyncRoute(async (req, res) => {
252
+ let expectation = "";
190
253
  try {
191
254
  const body = bodyObject(req.body);
192
- const expectation = text(body.expectation);
255
+ expectation = text(body.expectation);
193
256
  const existingNames = storage.listSiliconEmployees({ includeArchived: true })
194
257
  .map((employee) => employee.name);
258
+ // HR 已给完整结构化草稿:走同一校验,不再调用模型。
259
+ if (body.draft !== undefined && body.draft !== null) {
260
+ const draft = validateStructuredEmployeeDraft(body.draft, {
261
+ allowedProviders: allowedProviders(), existingNames,
262
+ });
263
+ res.json({ draft, usage: { generateCalls: 0, path: "validated" } });
264
+ return;
265
+ }
266
+ if (!expectation) {
267
+ throw new SiliconEmployeeDraftError("先说说你对这位员工的期望吧。", "EMPTY_EXPECTATION", "expectation");
268
+ }
195
269
  const draft = await generateDraft({ expectation, existingNames });
196
- res.json({ draft });
270
+ res.json({ draft, usage: { generateCalls: 1, path: "generated" } });
197
271
  }
198
272
  catch (error) {
199
- sendEmployeeError(res, error);
273
+ sendEmployeeError(res, error, { expectation });
200
274
  }
201
275
  }));
202
276
  app.post("/api/silicon-employees", (req, res) => {
@@ -0,0 +1,13 @@
1
+ import type { Express } from "express";
2
+ import type { DecisionService } from "./decision-service.js";
3
+ import type { StructuredSessionManager } from "./structured-session-manager.js";
4
+ import type { SessionRegistry } from "./session-registry.js";
5
+ import type { WandConfig } from "./types.js";
6
+ export type PiRecommendationRuntime = Pick<DecisionService, "evaluate" | "status">;
7
+ /** Uses the parent's login + sessions-scope middleware; inference-only tokens cannot enter this route. */
8
+ export declare function registerPiRecommendationRoute(app: Express, deps: {
9
+ structured: StructuredSessionManager;
10
+ sessions: SessionRegistry;
11
+ config: WandConfig;
12
+ decisions?: PiRecommendationRuntime;
13
+ }): void;
@@ -0,0 +1,67 @@
1
+ import { DecisionError } from "./decision-types.js";
2
+ import { discoverPiResources, resolvePiResourceSelection } from "./pi-resource-catalog.js";
3
+ import { parsePiRecommendationRequest, recommendPiResources } from "./pi-resource-recommendation.js";
4
+ /** Uses the parent's login + sessions-scope middleware; inference-only tokens cannot enter this route. */
5
+ export function registerPiRecommendationRoute(app, deps) {
6
+ const active = new Set();
7
+ app.post("/api/sessions/:id/pi-settings/recommend", async (req, res) => {
8
+ const id = req.params.id;
9
+ const abort = new AbortController();
10
+ const disconnected = () => { if (!res.writableEnded)
11
+ abort.abort(); };
12
+ let timer;
13
+ let owns = false;
14
+ res.set("Cache-Control", "no-store");
15
+ res.once("close", disconnected);
16
+ try {
17
+ const owner = deps.sessions.ownerOf(id);
18
+ if (!owner)
19
+ throw new DecisionError("NOT_FOUND", "未找到该会话。", 404);
20
+ if (owner !== "structured" || deps.structured.get(id)?.provider !== "pi") {
21
+ throw new DecisionError("UNSUPPORTED", "推荐仅适用于 Pi 结构化会话。");
22
+ }
23
+ const { settings, resolution } = deps.structured.getPiSettings(id);
24
+ if (deps.structured.get(id)?.archived || resolution.engine !== "cli") {
25
+ throw new DecisionError("UNSUPPORTED", "请使用未归档的 Pi CLI 会话进行资源推荐。");
26
+ }
27
+ const status = deps.decisions?.status();
28
+ if (!deps.decisions || !status?.enabled || !status.supported || !status.configured || settings.localDecision === false) {
29
+ throw new DecisionError("UNAVAILABLE", "本地决策未启用或不可用,原选择未改变。", 503);
30
+ }
31
+ const input = parsePiRecommendationRequest(req.body);
32
+ if (active.has(id))
33
+ throw new DecisionError("BUSY", "本会话正在推荐,请等待当前请求完成。", 409);
34
+ active.add(id);
35
+ owns = true;
36
+ timer = setTimeout(() => abort.abort(), 15_000);
37
+ const inventory = await discoverPiResources(deps.config, deps.structured.get(id).cwd);
38
+ resolvePiResourceSelection(inventory, input.candidates);
39
+ const result = await recommendPiResources({ ...input, catalog: inventory.catalog,
40
+ evaluate: (value, caller, signal) => deps.decisions.evaluate(value, caller, signal),
41
+ caller: `pi-recommend:${id}`, signal: abort.signal });
42
+ const latest = deps.structured.get(id);
43
+ if (!latest || latest.archived || latest.provider !== "pi" || latest.piSettings?.localDecision === false
44
+ || deps.structured.getPiSettings(id).resolution.engine !== "cli") {
45
+ throw new DecisionError("CANCELLED", "会话已变化,请重新打开设置面板。", 409);
46
+ }
47
+ if (abort.signal.aborted)
48
+ throw new DecisionError("TIMEOUT", "推荐已取消或超时,原选择未改变。", 504);
49
+ if (!res.destroyed)
50
+ res.json(result);
51
+ }
52
+ catch (error) {
53
+ if (res.destroyed)
54
+ return;
55
+ const failure = error instanceof DecisionError ? error
56
+ : new DecisionError("RECOMMEND_FAILED", "推荐失败,请重新打开面板核对资源;原选择未改变。", 400);
57
+ res.status(failure.status).json({ error: failure.message, code: failure.code });
58
+ }
59
+ finally {
60
+ if (timer)
61
+ clearTimeout(timer);
62
+ res.off("close", disconnected);
63
+ if (owns)
64
+ active.delete(id);
65
+ }
66
+ });
67
+ }
@@ -65,7 +65,7 @@ const STRUCTURED_RESUME_SPECS = [
65
65
  path: "/api/pi-sessions/:nativeId/resume",
66
66
  param: "nativeId",
67
67
  spec: {
68
- label: "Pi",
68
+ label: "one 的 Agent",
69
69
  provider: "pi",
70
70
  runner: "pi-cli-json",
71
71
  nativeIdKey: "claudeSessionId",
@@ -4,7 +4,8 @@ import { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import { WandStorage } from "./storage.js";
5
5
  import { ExecutionMode, ProcessEvent, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
6
6
  import { toSessionListItemDTO } from "./session-transport.js";
7
- import { SessionRegistry } from "./session-registry.js";
7
+ import { type ProviderHistoryDeleter, SessionRegistry } from "./session-registry.js";
8
+ import { type PiRecommendationRuntime } from "./server-pi-recommendation-routes.js";
8
9
  import { type SessionDirectoryNode } from "./session-directory-tree.js";
9
10
  export declare function parseExecutionMode(value: unknown, fallback: ExecutionMode): ExecutionMode;
10
11
  export declare function parseSessionCreationOrigin(body: {
@@ -14,7 +15,7 @@ export declare function parseSessionCreationOrigin(body: {
14
15
  sessionSource: SessionSource;
15
16
  automationId?: string;
16
17
  };
17
- type SessionDeletionProcesses = Pick<ProcessManager, "get" | "delete" | "deleteClaudeHistoryFiles" | "deleteCodexHistoryFiles" | "deleteOpenCodeHistorySessions" | "deleteQoderHistoryFiles">;
18
+ type SessionDeletionProcesses = Pick<ProcessManager, "get" | "delete"> & ProviderHistoryDeleter;
18
19
  type SessionDeletionStructured = Pick<StructuredSessionManager, "get" | "delete">;
19
20
  /**
20
21
  * A Wand-managed session and its provider-native history represent the same
@@ -45,6 +46,6 @@ export interface SessionDirectoryTreeResponse {
45
46
  }
46
47
  export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
47
48
  export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
48
- export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void, onCompletionViewed?: (event: ProcessEvent) => void): void;
49
+ export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void, onCompletionViewed?: (event: ProcessEvent) => void, decisions?: PiRecommendationRuntime): void;
49
50
  export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
50
51
  export {};
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { PtyInputDeliveryError, SessionInputError } from "./process-manager.js";
3
3
  import { getDefaultModelForProvider, isExecutionMode } from "./config.js";
4
4
  import { defaultRoleForCli } from "./default-employee.js";
5
+ import { defaultModelGroupSelector } from "./model-groups.js";
6
+ import { inspectPiExecution } from "./pi-execution.js";
5
7
  import { alignedBlockStart, blockWindowMessagesForTransport, compactToolMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, visibleBlockCount, windowMessagesForTransport } from "./message-truncator.js";
6
8
  import { toSessionDetailDTO, toSessionListItemDTO } from "./session-transport.js";
7
9
  import { checkSessionWorktreeMergeabilityAsync, cleanupSessionWorktreeAsync, getWorktreeMergeErrorCode, mergeSessionWorktreeAsync, WorktreeMergeError, } from "./git-worktree.js";
@@ -18,9 +20,13 @@ import { parseBoundedInteger } from "./request-limits.js";
18
20
  import { asyncRoute } from "./express-async.js";
19
21
  import { sendRouteError } from "./server-request.js";
20
22
  import { registerStructuredResumeRoutes } from "./server-resume-routes.js";
21
- import { addHiddenSessionIds, removeHiddenSessionIds } from "./session-registry.js";
23
+ import { addHiddenSessionIds, deleteProviderNativeHistory, removeHiddenSessionIds } from "./session-registry.js";
22
24
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
23
25
  import { asRecord, isStructuredImagePart } from "./structured-content.js";
26
+ import { installedPiExtensions } from "./pi-extension-resources.js";
27
+ import { registerPiRecommendationRoute } from "./server-pi-recommendation-routes.js";
28
+ import { discoverPiResources, resolvePiResourceSelection } from "./pi-resource-catalog.js";
29
+ import { effectivePiSessionSettings, patchPiSessionSettings } from "./pi-session-settings.js";
24
30
  import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
25
31
  import { projectCwdForSession, renameWorkspaceDirectory, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
26
32
  export function parseExecutionMode(value, fallback) {
@@ -138,24 +144,7 @@ export function deleteSessionWithProviderHistory(processes, structured, storage,
138
144
  ?? snapshot.structuredState?.provider
139
145
  ?? inferProviderFromCommand(snapshot.command)
140
146
  ?? "claude";
141
- if (provider === "claude") {
142
- processes.deleteClaudeHistoryFiles([{
143
- claudeSessionId: providerSessionId,
144
- cwd: snapshot.cwd,
145
- }]);
146
- }
147
- else if (provider === "codex") {
148
- processes.deleteCodexHistoryFiles([providerSessionId]);
149
- }
150
- else if (provider === "opencode") {
151
- processes.deleteOpenCodeHistorySessions([providerSessionId]);
152
- }
153
- else if (provider === "qoder") {
154
- processes.deleteQoderHistoryFiles([providerSessionId]);
155
- }
156
- else {
157
- return;
158
- }
147
+ deleteProviderNativeHistory(processes, provider, providerSessionId, snapshot.cwd);
159
148
  // History deletion is intentionally best-effort at the filesystem layer.
160
149
  // Keep a tombstone as a fallback for permission errors or process tail writes.
161
150
  addHiddenSessionIds(storage, [providerSessionId]);
@@ -320,6 +309,33 @@ async function startResumedPtySession(processes, storage, existingSession, sessi
320
309
  workspaceTaskId: existingSession.workspaceTaskId,
321
310
  });
322
311
  }
312
+ /** 取消归档后,用原来的 Wand 会话 ID 把终端拉起来;有 provider session id 就续接那次对话。 */
313
+ async function restoreArchivedSession(sessions, processes, storage, sessionId, defaultMode) {
314
+ const existing = sessions.get(sessionId) ?? storage.getSession(sessionId);
315
+ if (!existing)
316
+ return null;
317
+ const cleared = sessions.setArchived(sessionId, false);
318
+ if (!cleared)
319
+ return null;
320
+ if ((existing.sessionKind ?? "pty") !== "pty" || cleared.status === "running")
321
+ return cleared;
322
+ const command = existing.command.trim();
323
+ if (!command)
324
+ return cleared;
325
+ const provider = existing.provider ?? inferProviderFromCommand(command) ?? undefined;
326
+ if (existing.claudeSessionId && provider && isPtyProviderCommand(provider, command)) {
327
+ return startResumedPtySession(processes, storage, { ...existing, ...cleared }, sessionId, defaultMode, {});
328
+ }
329
+ return processes.start(command, existing.cwd, parseExecutionMode(existing.mode, defaultMode), undefined, {
330
+ reuseId: sessionId,
331
+ provider,
332
+ model: existing.selectedModel ?? undefined,
333
+ thinkingEffort: existing.thinkingEffort ?? undefined,
334
+ systemPrompt: existing.systemPrompt ?? undefined,
335
+ workspaceId: existing.workspaceId,
336
+ workspaceTaskId: existing.workspaceTaskId,
337
+ });
338
+ }
323
339
  function getAutoResumeInitialInput(snapshot, input, view, shortcutKey) {
324
340
  if (view === "terminal") {
325
341
  const isCodexPty = Boolean(snapshot?.provider === "codex" || /^codex\b/.test(snapshot?.command.trim() ?? ""));
@@ -349,7 +365,8 @@ function canMergeSession(snapshot) {
349
365
  function isMergeActionAllowed(snapshot) {
350
366
  return snapshot.status !== "running";
351
367
  }
352
- export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated, onCompletionViewed) {
368
+ export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated, onCompletionViewed, decisions) {
369
+ registerPiRecommendationRoute(app, { structured, sessions, config, decisions });
353
370
  const wantsCompactTools = (req) => req.get("X-Wand-Tool-Projection") === "compact" || req.query.compactTools === "1";
354
371
  const sessionResponseDTO = (snapshot, req) => {
355
372
  const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
@@ -462,11 +479,15 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
462
479
  throw new Error("硅基员工不存在或已归档。");
463
480
  const selectedEmployeeCandidate = employee ? selectEmployeeCandidate(employee) : null;
464
481
  const employeeAgent = selectedEmployeeCandidate?.agent;
465
- if (!employee && body.provider && !isSessionProvider(body.provider)) {
482
+ if (body.provider && !isSessionProvider(body.provider)) {
466
483
  res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok、Qoder、Pi 或 Gemini provider。" });
467
484
  return;
468
485
  }
469
- const provider = employeeAgent?.provider
486
+ const overrideCli = body.overrideCli === true;
487
+ const rawProvider = typeof body.provider === "string" ? body.provider.trim() : "";
488
+ const requestedProvider = overrideCli && isSessionProvider(rawProvider) ? rawProvider : null;
489
+ const provider = requestedProvider
490
+ ?? employeeAgent?.provider
470
491
  ?? (isSessionProvider(body.provider) ? body.provider : config.defaultProvider ?? "claude");
471
492
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
472
493
  const origin = parseSessionCreationOrigin(body);
@@ -477,15 +498,19 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
477
498
  const cwd = resolveSessionCwd(body.cwd, config.defaultCwd);
478
499
  const snapshot = structured.createSession({
479
500
  cwd,
480
- mode: employeeAgent?.mode ?? parseExecutionMode(body.mode, defaultMode),
501
+ mode: (requestedProvider && body.mode ? parseExecutionMode(body.mode, defaultMode) : null)
502
+ ?? employeeAgent?.mode
503
+ ?? parseExecutionMode(body.mode, defaultMode),
481
504
  provider,
482
505
  // Omit runner to let StructuredSessionManager apply the configured
483
506
  // Claude default; explicit values are validated against the provider.
484
- runner: employee ? undefined : body.runner,
507
+ runner: employee && !requestedProvider ? undefined : body.runner,
485
508
  worktreeEnabled: body.worktreeEnabled === true,
486
- model: employeeAgent
487
- ? employeeAgent.model === "default" ? getDefaultModelForProvider(config, provider) || undefined : employeeAgent.model
488
- : rawModel || getDefaultModelForProvider(config, provider) || undefined,
509
+ model: rawModel && rawModel !== "default"
510
+ ? rawModel
511
+ : (employeeAgent && !requestedProvider
512
+ ? employeeAgent.model === "default" ? defaultModelGroupSelector(config.modelGroups, provider, getDefaultModelForProvider(config, provider)) || getDefaultModelForProvider(config, provider) || undefined : employeeAgent.model
513
+ : rawModel || defaultModelGroupSelector(config.modelGroups, provider, getDefaultModelForProvider(config, provider)) || getDefaultModelForProvider(config, provider) || undefined),
489
514
  thinkingEffort: employeeAgent?.thinkingEffort ?? (typeof body.thinkingEffort === "string"
490
515
  ? body.thinkingEffort
491
516
  : config.defaultThinkingEffort),
@@ -497,7 +522,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
497
522
  employeeName: role?.name,
498
523
  employeeAvatar: role?.avatar,
499
524
  employeeCandidates: employee?.agents,
500
- employeeCandidateIndex: selectedEmployeeCandidate?.index,
525
+ employeeCandidateIndex: requestedProvider ? undefined : selectedEmployeeCandidate?.index,
501
526
  ...origin,
502
527
  });
503
528
  onSessionCreated?.(snapshot.cwd);
@@ -553,6 +578,105 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
553
578
  sendRouteError(res, error, "切换工具失败。");
554
579
  }
555
580
  });
581
+ const piSettingsResponse = async (id) => {
582
+ const { settings, resolution } = structured.getPiSettings(id);
583
+ const snapshot = structured.get(id);
584
+ const installed = await installedPiExtensions(config, snapshot.cwd);
585
+ const sdk = resolution.engine === "core";
586
+ const archived = snapshot.archived;
587
+ const modifiable = !archived;
588
+ const goalAvailable = installed.some((item) => item.goal);
589
+ const localDecisionAvailable = config.localDecision?.enabled === true;
590
+ const inventory = await discoverPiResources(config, snapshot.cwd);
591
+ const resourceCatalog = { ...inventory.catalog, supported: modifiable && !sdk && inventory.catalog.supported,
592
+ reason: archived ? "请先恢复已归档的会话。" : sdk ? "当前 SDK 候选尚不支持 Skills / MCP 会话选择。" : inventory.catalog.reason };
593
+ const controls = {
594
+ // 基础工具与 CodeMode 在两种引擎下都能表达(CLI 通过 --tools 白名单)。
595
+ tools: modifiable,
596
+ codemode: modifiable,
597
+ codemodeOnly: modifiable && sdk,
598
+ codemodeOverride: modifiable && !sdk,
599
+ globalTools: modifiable,
600
+ goalMode: modifiable && goalAvailable,
601
+ localDecision: modifiable && localDecisionAvailable,
602
+ autoCompaction: modifiable && sdk,
603
+ };
604
+ const status = decisions?.status();
605
+ const recommendationAvailable = resourceCatalog.supported && settings.localDecision !== false
606
+ && status?.enabled === true && status.supported && status.configured;
607
+ const recommendationReason = !resourceCatalog.supported ? resourceCatalog.reason
608
+ : settings.localDecision === false ? "本会话已关闭本地决策。"
609
+ : !recommendationAvailable ? "本地决策未启用或运行环境不可用。" : "";
610
+ return { recommendationAvailable, recommendationReason,
611
+ autoResourcesAvailable: recommendationAvailable, autoResourcesReason: recommendationReason,
612
+ autoCodemodeAvailable: recommendationAvailable, skillLocksAvailable: resourceCatalog.supported,
613
+ settings: effectivePiSessionSettings(settings, resolution.engine), engine: resolution.engine,
614
+ available: sdk && modifiable, toolsAvailable: !sdk && modifiable,
615
+ reason: archived ? "请先恢复已归档的会话。" : resolution.reason,
616
+ goalAvailable, globalExtensions: [...new Set(installed.filter((item) => !item.goal).map((item) => item.name))],
617
+ localDecisionAvailable, controls, resourceCatalog };
618
+ };
619
+ const requirePiSession = (id) => {
620
+ const owner = sessions.ownerOf(id);
621
+ if (!owner)
622
+ throw Object.assign(new Error("未找到该会话。"), { status: 404 });
623
+ if (owner !== "structured" || structured.get(id)?.provider !== "pi") {
624
+ throw new Error("这些设置仅适用于 Pi 结构化会话,不会改变 PTY 或其他工具。");
625
+ }
626
+ };
627
+ app.get("/api/sessions/:id/pi-settings", asyncRoute(async (req, res) => {
628
+ try {
629
+ requirePiSession(req.params.id);
630
+ res.set("Cache-Control", "no-store");
631
+ res.json(await piSettingsResponse(req.params.id));
632
+ }
633
+ catch (error) {
634
+ sendRouteError(res, error, "读取 Pi 设置失败。", error.status ?? 400);
635
+ }
636
+ }));
637
+ app.patch("/api/sessions/:id/pi-settings", asyncRoute(async (req, res) => {
638
+ try {
639
+ const id = req.params.id;
640
+ requirePiSession(id);
641
+ const { settings } = structured.getPiSettings(id);
642
+ const next = patchPiSessionSettings(settings, req.body);
643
+ if (req.body.autoResources === true) {
644
+ const capabilities = await piSettingsResponse(id);
645
+ const runtime = decisions?.status();
646
+ if (!capabilities.resourceCatalog?.supported || next.localDecision === false
647
+ || !runtime?.enabled || !runtime.supported || !runtime.configured) {
648
+ throw new Error(next.localDecision === false ? "本会话已关闭本地决策。"
649
+ : capabilities.autoResourcesReason || "自动选择暂不可用。");
650
+ }
651
+ }
652
+ if (req.body.resources !== undefined || req.body.lockedSkills !== undefined) {
653
+ const inventory = await discoverPiResources(config, structured.get(id).cwd);
654
+ // Removed resources may be retained only when already selected, so users can deselect stale
655
+ // items one at a time. Any NEW ID must resolve; the runner always revalidates the full list.
656
+ if (req.body.resources !== undefined)
657
+ resolvePiResourceSelection(inventory, {
658
+ skills: next.resources.skills.filter((id) => !settings.resources?.skills.includes(id)),
659
+ mcpServers: next.resources.mcpServers.filter((id) => !settings.resources?.mcpServers.includes(id)),
660
+ });
661
+ if (req.body.lockedSkills !== undefined)
662
+ resolvePiResourceSelection(inventory, {
663
+ skills: next.lockedSkills.filter((id) => !settings.lockedSkills?.includes(id)), mcpServers: [],
664
+ });
665
+ }
666
+ if (next.goalMode && !settings.goalMode) {
667
+ const installed = await installedPiExtensions(config, structured.get(id).cwd);
668
+ if (!installed.some((item) => item.goal))
669
+ throw new Error("目标模式不可用:请先在 Pi 中安装并启用 pi-goal。");
670
+ }
671
+ structured.setPiSettings(id, req.body);
672
+ // 回复「实际生效」的设置(CLI 的 CodeMode 只归一到 off/on),不把存库原值当生效值展示。
673
+ const saved = structured.getPiSettings(id);
674
+ res.json({ settings: effectivePiSessionSettings(saved.settings, saved.resolution.engine) });
675
+ }
676
+ catch (error) {
677
+ sendRouteError(res, error, "保存 Pi 设置失败。", error.status ?? 400);
678
+ }
679
+ }));
556
680
  app.post("/api/sessions/:id/model", (req, res) => {
557
681
  const body = req.body;
558
682
  const rawModel = typeof body?.model === "string" ? body.model.trim() : null;
@@ -741,6 +865,20 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
741
865
  }
742
866
  });
743
867
  // ── Tool content lazy-load endpoint ──
868
+ app.get("/api/sessions/:id/pi-execution/:toolUseId", asyncRoute(async (req, res) => {
869
+ const snapshot = sessions.get(req.params.id);
870
+ if (!snapshot) {
871
+ res.status(404).json({ error: "未找到该会话。" });
872
+ return;
873
+ }
874
+ const execution = await inspectPiExecution(snapshot.messages ?? [], req.params.toolUseId);
875
+ if (!execution) {
876
+ res.status(404).json({ error: "未找到该 Pi 执行调用。" });
877
+ return;
878
+ }
879
+ res.setHeader("Cache-Control", "no-store");
880
+ res.json(execution);
881
+ }));
744
882
  app.get("/api/sessions/:id/tool-content/:toolUseId", (req, res) => {
745
883
  const snapshot = sessions.get(req.params.id);
746
884
  if (!snapshot) {
@@ -933,7 +1071,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
933
1071
  try {
934
1072
  // A commit is already durable, even when push failed. Archiving must not turn that
935
1073
  // successful commit into a retryable HTTP error (which would duplicate the commit).
936
- archivedTaskIds = archiveCommitTasks(storage, snapshot, context.entryIds);
1074
+ const openedTaskId = typeof body.workspaceTaskId === "string" ? body.workspaceTaskId.trim() : "";
1075
+ archivedTaskIds = archiveCommitTasks(storage, snapshot, context.entryIds, {
1076
+ workspaceTaskId: openedTaskId || null,
1077
+ });
937
1078
  }
938
1079
  catch (error) {
939
1080
  archiveError = getErrorMessage(error, "归档关联任务失败。");
@@ -1379,7 +1520,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1379
1520
  try {
1380
1521
  if (structured.get(sessionId)) {
1381
1522
  const completion = structured.sendMessage(sessionId, input);
1382
- if (body.respondImmediately === true) {
1523
+ if (body.respondImmediately === true || (structured.get(sessionId)?.provider === "pi"
1524
+ && structured.get(sessionId)?.piSettings?.autoResources === true)) {
1383
1525
  // sendMessage updates the canonical snapshot synchronously before it
1384
1526
  // starts awaiting the runner. Native clients should not hold this
1385
1527
  // request open for an entire model turn (which can exceed their HTTP
@@ -1537,7 +1679,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1537
1679
  sendRouteError(res, error, "无法停止会话。");
1538
1680
  }
1539
1681
  });
1540
- // 归档 / 取消归档会话:只写标记,不杀终端、不删历史;保留期扫描再按 archivedAt 清理。
1682
+ // 归档会停止会话,但保留 Wand 会话 ID 和 provider session id。恢复时用这个 ID 续上。
1541
1683
  app.post("/api/sessions/:id/archive", (req, res) => {
1542
1684
  try {
1543
1685
  const updated = sessions.setArchived(req.params.id, true);
@@ -1551,9 +1693,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1551
1693
  sendRouteError(res, error, "无法归档会话。");
1552
1694
  }
1553
1695
  });
1554
- app.post("/api/sessions/:id/unarchive", (req, res) => {
1696
+ app.post("/api/sessions/:id/unarchive", asyncRoute(async (req, res) => {
1555
1697
  try {
1556
- const updated = sessions.setArchived(req.params.id, false);
1698
+ const updated = await restoreArchivedSession(sessions, processes, storage, req.params.id, defaultMode);
1557
1699
  if (!updated) {
1558
1700
  res.status(404).json({ error: "未找到该会话。" });
1559
1701
  return;
@@ -1563,9 +1705,9 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1563
1705
  catch (error) {
1564
1706
  sendRouteError(res, error, "无法恢复会话。");
1565
1707
  }
1566
- });
1708
+ }));
1567
1709
  // 批量归档 / 恢复:与 /api/sessions/batch-delete 对称,返回失败项让客户端保留选择。
1568
- app.post("/api/sessions/batch-archive", (req, res) => {
1710
+ app.post("/api/sessions/batch-archive", asyncRoute(async (req, res) => {
1569
1711
  const body = req.body;
1570
1712
  const ids = Array.isArray(body?.sessionIds)
1571
1713
  ? body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
@@ -1579,7 +1721,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1579
1721
  let changed = 0;
1580
1722
  for (const id of ids) {
1581
1723
  try {
1582
- if (sessions.setArchived(id, archived))
1724
+ const updated = archived
1725
+ ? sessions.setArchived(id, true)
1726
+ : await restoreArchivedSession(sessions, processes, storage, id, defaultMode);
1727
+ if (updated)
1583
1728
  changed += 1;
1584
1729
  else
1585
1730
  failed.push(id);
@@ -1589,7 +1734,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1589
1734
  }
1590
1735
  }
1591
1736
  res.json({ ok: true, archived: changed, ...(failed.length > 0 ? { failed } : {}) });
1592
- });
1737
+ }));
1593
1738
  app.delete("/api/sessions/:id", (req, res) => {
1594
1739
  try {
1595
1740
  sessions.deleteWithProviderHistory(req.params.id);