@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
package/dist/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
1
2
  import crypto from "node:crypto";
2
3
  import compression from "compression";
3
4
  import express from "express";
@@ -13,12 +14,15 @@ import { AuthService, BROWSER_ADMIN_PRINCIPAL, CONNECTED_APP_PRINCIPAL, principa
13
14
  import { ensureCertificates } from "./cert.js";
14
15
  import { getDefaultModelForProvider, getProviderDefaultModels, isExecutionMode, resolveConfigDir, } from "./config.js";
15
16
  import { ModelCatalogService } from "./models.js";
17
+ import { defaultModelGroupSelector } from "./model-groups.js";
16
18
  import { ProcessManager } from "./process-manager.js";
17
19
  import { SessionLogger } from "./session-logger.js";
20
+ import { coreHarnessAgentDir, warmCoreHarness } from "./harness-engine.js";
18
21
  import { SessionRegistry } from "./session-registry.js";
19
22
  import { SessionCompletionTracker } from "./session-completion.js";
20
23
  import { resolveSystemAiContext } from "./session-ai-context.js";
21
24
  import { StructuredSessionManager } from "./structured-session-manager.js";
25
+ import { SettingsWebAccess, registerSettingsWebAccessRoute } from "./settings-web-access.js";
22
26
  import { DecisionService } from "./decision-service.js";
23
27
  import { registerDecisionRoutes } from "./server-decision-routes.js";
24
28
  import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
@@ -33,8 +37,12 @@ import { registerTaskRoutes } from "./server-task-routes.js";
33
37
  import { getGithubConnectorStatus } from "./github-connector.js";
34
38
  import { registerMissionRoutes } from "./server-mission-routes.js";
35
39
  import { Missions } from "./missions.js";
36
- import { createAiTeamRunner } from "./ai-team-runner.js";
40
+ import { AI_TEAM_CHAT_PREFIX, createAiTeamRunner } from "./ai-team-runner.js";
41
+ import { ConversationService } from "./conversation-service.js";
42
+ import { CONVERSATION_RELAY_PREFIX } from "./conversation-types.js";
43
+ import { forwardConversationRelay, registerConversationRoutes } from "./server-conversation-routes.js";
37
44
  import { registerAiTeamRoutes } from "./server-ai-team-routes.js";
45
+ import { registerTeamDispatchRoutes } from "./server-team-dispatch-routes.js";
38
46
  import { registerSiliconEmployeeRoutes } from "./server-employee-routes.js";
39
47
  import { defaultRoleForCli } from "./default-employee.js";
40
48
  import { UserMemoryService } from "./user-memory.js";
@@ -50,7 +58,7 @@ import { asyncRoute, jsonErrorHandler } from "./express-async.js";
50
58
  import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
51
59
  import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
52
60
  import { computeRelaunch } from "./relaunch.js";
53
- import { startRetentionTimer } from "./retention.js";
61
+ import { createRetentionSweep, startRetentionTimer } from "./retention.js";
54
62
  import { RuntimeConfigState } from "./runtime-config.js";
55
63
  import { safeServiceInstalled } from "./tui/runtime-utils.js";
56
64
  import { checkManagedServiceUpdatePreflight, } from "./update-helper.js";
@@ -73,6 +81,7 @@ import { createUpgradeAwareTerminalHost } from "./render-host.js";
73
81
  import { createUpgradeAwareStructuredHost } from "./render-structured-host.js";
74
82
  import { checkPasswordRateLimit, recordFailedPassword, resetPasswordRateLimit } from "./middleware/rate-limit.js";
75
83
  import { updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
84
+ import { userAuthor } from "./user-profile.js";
76
85
  const SERVER_MODULE_DIR = path.dirname(new URL(import.meta.url).pathname);
77
86
  const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
78
87
  // ── Package info ──
@@ -133,9 +142,12 @@ const DISPLAY_VERSION = BUILD_INFO.version || PKG_VERSION;
133
142
  const SERVER_INSTANCE_ID = crypto.randomUUID();
134
143
  // ── Auth helpers ──
135
144
  const requestPrincipals = new WeakMap();
136
- function buildRequireAuth(useHttps, storage, config, authService) {
145
+ function buildRequireAuth(useHttps, storage, config, authService, settingsAccess) {
137
146
  return function requireAuth(req, res, next) {
138
- const principal = authService.authenticateSession(readSessionCookie(req, useHttps))
147
+ const sessionToken = readSessionCookie(req, useHttps);
148
+ const sessionPrincipal = authService.authenticateSession(sessionToken);
149
+ const principal = (sessionPrincipal && principalHasScope(sessionPrincipal, "session-preferences") &&
150
+ settingsAccess.accepts(sessionToken, req.headers.cookie) ? BROWSER_ADMIN_PRINCIPAL : sessionPrincipal)
139
151
  ?? authenticateBearerAppToken(req, storage, config);
140
152
  if (!principal) {
141
153
  res.status(401).json({ error: "未授权,请先登录。" });
@@ -272,6 +284,8 @@ export function isPortInUseError(error) {
272
284
  && error.code === "EADDRINUSE");
273
285
  }
274
286
  export async function startServer(config, configPath, options = {}) {
287
+ if (config.shell?.trim())
288
+ process.env.SHELL = config.shell.trim();
275
289
  // 关键:在创建 ProcessManager / 任何 spawn 之前先修 PATH。
276
290
  // 服务被注册为 systemd / launchd 时,unit 文件里的 PATH 是安装那一刻烧死的,
277
291
  // 之后用户切 node 版本 / 重装 wand / 把 claude 装到新位置都不会更新 unit,
@@ -299,7 +313,9 @@ export async function startServer(config, configPath, options = {}) {
299
313
  app.set("trust proxy", "loopback, 172.16.0.0/12");
300
314
  const storage = new WandStorage(resolveDatabasePath(configPath));
301
315
  const runtimeConfig = new RuntimeConfigState(config);
316
+ const openRouter = new OpenRouterFreeModelsService(storage, options.openRouterFetch);
302
317
  const authService = new AuthService(storage);
318
+ const settingsAccess = new SettingsWebAccess();
303
319
  const decisions = new DecisionService(config.localDecision);
304
320
  // 默认模型优先读存储(UI 改设置后实时生效),未设置时由 getPreference 回落到 config。
305
321
  const getCurrentDefaultModels = () => ({
@@ -316,11 +332,14 @@ export async function startServer(config, configPath, options = {}) {
316
332
  const currentDefaults = getCurrentDefaultModels();
317
333
  return {
318
334
  storage,
335
+ managedPiModels: () => openRouter.catalog(),
336
+ managedPiModelMembers: () => openRouter.members(),
337
+ modelGroups: () => config.modelGroups ?? [],
319
338
  inheritEnv: config.inheritEnv !== false,
320
339
  apiKey: process.env.ANTHROPIC_API_KEY,
321
340
  baseUrl: process.env.ANTHROPIC_BASE_URL,
322
341
  ...injected,
323
- piEndpointDiscovery: injected.piEndpointDiscovery ?? { enabled: !testMode },
342
+ piEndpointDiscovery: injected.piEndpointDiscovery ?? { enabled: !testMode, agentDir: coreHarnessAgentDir(config.harness) },
324
343
  configuredClaudeModels: [
325
344
  currentDefaults.claude,
326
345
  config.commitCli === "claude" ? (storage.getPreference("pref:commitModel", config.commitModel) ?? "") : undefined,
@@ -351,22 +370,36 @@ export async function startServer(config, configPath, options = {}) {
351
370
  knownSessionIds: knownPtySessionIds,
352
371
  });
353
372
  const terminalHost = ptyHosts.host;
354
- const processes = new ProcessManager(config, storage, configDir, terminalHost);
373
+ let decisionRuntime = null;
374
+ let autoAssignEvaluate;
375
+ const processes = new ProcessManager(config, storage, configDir, terminalHost, { resolveDecisionEvaluate: () => autoAssignEvaluate });
355
376
  const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
356
377
  // Production startup provides a daemon-backed host for structured CLI runs
357
378
  // even when Render owns every PTY. In-process hosts are for test injection.
358
379
  const legacyStructuredHost = ptyHosts.legacyHost
359
380
  ?? (terminalHost instanceof TerminalDaemonClient ? terminalHost : null);
360
381
  const structuredHosts = await createUpgradeAwareStructuredHost(configPath, legacyStructuredHost, config.structured?.processHost);
361
- let decisionRuntime = null;
362
- const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, {}, structuredHosts.host, () => decisionRuntime);
382
+ const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, {}, structuredHosts.host, () => decisionRuntime, openRouter, () => autoAssignEvaluate);
383
+ // core harness 预热:只读本机 Pi 认证与模型目录,不联网。失败不影响启动,
384
+ // 但要在开始接输入前得到确定结论,否则首轮引擎裁决会因缓存未就绪而退回 CLI。
385
+ const coreHarnessStatus = await warmCoreHarness(config.harness);
386
+ if (coreHarnessStatus) {
387
+ console.log(coreHarnessStatus.available
388
+ ? `[wand] core harness 可用(Pi provider ${coreHarnessStatus.providerCount} 个、模型 ${coreHarnessStatus.modelCount} 个,agentDir ${coreHarnessStatus.agentDir});pi 结构化会话将使用进程内引擎。`
389
+ : `[wand] core harness 不可用,pi 结构化会话回退 CLI:${coreHarnessStatus.reason}`);
390
+ }
363
391
  const sessionRegistry = new SessionRegistry(processes, structuredSessions, storage);
364
392
  const sessionCompletions = new SessionCompletionTracker(storage, (id) => sessionRegistry.get(id));
365
393
  const missions = new Missions(storage, structuredSessions, sessionRegistry);
394
+ // 任务保留的立即扫描也晚绑定:设置路由先注册,WebSocket 和会话表稍后才就绪。
395
+ let runTaskRetentionSweep = () => ({
396
+ archivedSessions: 0, purgedSessions: 0, archivedTasks: 0, purgedTasks: 0, purgedTeamRuns: 0,
397
+ });
366
398
  // wsManager 在后面才建:团队运行的变更通知经由这个转发口,接好之前静默丢弃。
367
399
  let notifyAiTeamRun = (_data) => { };
368
400
  // 运行中步骤的 live 文本推送(§4.9),同样走系统通知,接好之前静默丢弃。
369
401
  let notifyAiTeamRunLive = (_update) => { };
402
+ let notifyConversationSession = (_update) => { };
370
403
  // 团队降级的 model-unknown 事前比对只看这份已发现的清单;没刷新过就是空,判定方向是放行。
371
404
  const aiTeamModelIds = (provider) => {
372
405
  const cache = modelCatalog.snapshot();
@@ -381,14 +414,27 @@ export async function startServer(config, configPath, options = {}) {
381
414
  }[provider];
382
415
  return cache[key].map((entry) => entry.id);
383
416
  };
417
+ let conversations = null;
384
418
  const aiTeams = createAiTeamRunner({
385
419
  storage, config, structured: structuredSessions, processes, sessions: sessionRegistry,
386
- notify: (run) => notifyAiTeamRun({ kind: "ai-team-run", runId: run.id, taskId: run.taskId }),
420
+ selfAuthor: () => userAuthor(config.userProfile),
421
+ notify: (run) => {
422
+ conversations?.importRun(run);
423
+ notifyAiTeamRun({ kind: "ai-team-run", runId: run.id, taskId: run.taskId });
424
+ },
387
425
  notifyLive: (update) => notifyAiTeamRunLive(update),
388
426
  models: aiTeamModelIds,
427
+ chatHistoryTurns: id => conversations?.history(id) ?? [],
389
428
  // 成员名单里的模型名:`default` 哨兵换成服务端为该 CLI 配置的默认模型。
390
429
  defaultModelOf: (provider) => getDefaultModelForProvider(config, provider),
391
430
  });
431
+ const conversationService = new ConversationService({ storage, config, structured: structuredSessions, runner: aiTeams,
432
+ notifySession: update => notifyConversationSession(update), deleteSession: id => { sessionRegistry.deleteWithProviderHistory(id); } });
433
+ conversations = conversationService;
434
+ conversationService.migrateExistingChats();
435
+ for (const prefix of [CONVERSATION_RELAY_PREFIX, AI_TEAM_CHAT_PREFIX]) {
436
+ structuredSessions.registerRelay(prefix, (sessionId, input) => forwardConversationRelay(conversationService, sessionId, input));
437
+ }
392
438
  const updateState = new ServerUpdateState();
393
439
  const getUpdateChannel = () => normalizeUpdateChannel(storage.getConfigValue("updateChannel"));
394
440
  let disconnectAuthenticatedSockets = () => { };
@@ -397,7 +443,7 @@ export async function startServer(config, configPath, options = {}) {
397
443
  };
398
444
  const useHttps = config.https === true;
399
445
  const protocol = useHttps ? "https" : "http";
400
- const requireAuth = buildRequireAuth(useHttps, storage, config, authService);
446
+ const requireAuth = buildRequireAuth(useHttps, storage, config, authService, settingsAccess);
401
447
  const requireAdmin = buildRequireScope("admin");
402
448
  const requireSessions = buildRequireScope("sessions");
403
449
  const requireFiles = buildRequireScope("files");
@@ -468,9 +514,9 @@ export async function startServer(config, configPath, options = {}) {
468
514
  res.type("application/javascript").send(chunk.content);
469
515
  });
470
516
  // ── Web UI endpoints ──
471
- app.get("/", (_req, res) => {
517
+ app.get(["/", "/settings"], (req, res) => {
472
518
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
473
- res.type("html").send(renderApp(configPath));
519
+ res.type("html").send(renderApp(configPath, /^\/settings\/?$/.test(req.path) ? "settings" : "console", req.query.client === "app" ? "client" : "browser"));
474
520
  });
475
521
  app.get("/api/structured-chat-avatar/:role", requireAuth, asyncRoute(async (req, res) => {
476
522
  const role = req.params.role === "user" || req.params.role === "assistant"
@@ -597,11 +643,21 @@ export async function startServer(config, configPath, options = {}) {
597
643
  res.json({ ok: true, reauthenticationRequired: true });
598
644
  });
599
645
  // ── Android APK update & download (no auth required) ──
600
- registerPublicUpdateRoutes(app, distributionManager);
646
+ registerPublicUpdateRoutes(app, distributionManager, config.publicOrigin);
601
647
  // Public probe so the unauthenticated browser does not log a 401 on /api/config
602
648
  app.get("/api/session-check", (req, res) => {
603
649
  res.json({ authed: authService.validateSession(readSessionCookie(req, useHttps)) });
604
650
  });
651
+ registerSettingsWebAccessRoute(app, {
652
+ requireAuth, useHttps, access: settingsAccess,
653
+ authenticateSession: (token) => authService.authenticateSession(token),
654
+ });
655
+ // Count-only compatibility probe. Local control (including session IDs and
656
+ // admission barriers) uses the owner-only Unix IPC socket.
657
+ app.get("/api/core-status", (_req, res) => {
658
+ const { hasActiveTurns, activeTurnCount } = structuredSessions.getCoreTurnStatus();
659
+ res.set("Cache-Control", "no-store").json({ hasActiveTurns, activeTurnCount });
660
+ });
605
661
  registerDecisionRoutes(app, { storage, decisions, requireAuth, requireSessions });
606
662
  app.use("/api", requireAuth);
607
663
  // Connected apps receive only the route families used by native clients and
@@ -613,6 +669,7 @@ export async function startServer(config, configPath, options = {}) {
613
669
  "/api/session-list",
614
670
  "/api/session-directories",
615
671
  "/api/structured-sessions",
672
+ "/api/conversations",
616
673
  "/api/commands",
617
674
  "/api/user-memory",
618
675
  "/api/claude-history",
@@ -691,6 +748,8 @@ export async function startServer(config, configPath, options = {}) {
691
748
  ],
692
749
  structuredChatPersona,
693
750
  cardDefaults: config.cardDefaults,
751
+ // 会话署名与头像:客户端只做展示投影,不解释成任何执行身份。
752
+ userProfile: config.userProfile ?? {},
694
753
  // 把语言偏好暴露给前端做 UI 文案 i18n。后端原本只用它给 Claude 拼 system prompt,
695
754
  // 前端没收到 → "SUBAGENT" / "Read" 这些 UI label 一直是英文,跟用户设的中文不匹配。
696
755
  language: config.language ?? "",
@@ -722,7 +781,9 @@ export async function startServer(config, configPath, options = {}) {
722
781
  getDistributionSettings,
723
782
  getGithubConnector: () => getGithubConnectorStatus(storage),
724
783
  modelCatalog,
784
+ openRouter,
725
785
  resolveAppConnectCode: (req) => resolveAppConnectCode(req, config, useHttps, getEffectivePassword(storage, config)),
786
+ sweepTaskRetention: () => runTaskRetentionSweep(),
726
787
  });
727
788
  registerGithubRoutes(app, { storage, requireAdmin, sessions: sessionRegistry });
728
789
  // 任务管理与 Missions 一样只需登录:原生 connected-app 也要能列/建/派发。
@@ -732,12 +793,15 @@ export async function startServer(config, configPath, options = {}) {
732
793
  runner: aiTeams,
733
794
  notifyTeamChanged: (teamId) => notifyAiTeamRun({ kind: "ai-team-definition", teamId }),
734
795
  });
796
+ // 无指派派工:本地决策模型建议名单 → 确认开工(不自动派工,见 AGENTS)。
797
+ registerTeamDispatchRoutes(app, { storage, runner: aiTeams, decisions });
735
798
  registerSiliconEmployeeRoutes(app, {
736
799
  storage,
737
800
  config,
738
801
  notifyEmployeeChanged: (employeeId) => notifyAiTeamRun({ kind: "silicon-employee-definition", employeeId }),
739
802
  });
740
803
  registerAttentionRoutes(app, { sessions: sessionRegistry, runner: aiTeams });
804
+ registerConversationRoutes(app, conversationService);
741
805
  registerAdminUpdateRoutes(app, {
742
806
  storage,
743
807
  config,
@@ -764,7 +828,7 @@ export async function startServer(config, configPath, options = {}) {
764
828
  }
765
829
  registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, sessionRegistry, (cwd) => {
766
830
  recordRecentPath(storage, cwd);
767
- }, (event) => wsManager.emitEvent(event));
831
+ }, (event) => wsManager.emitEvent(event), decisions);
768
832
  registerClaudeHistoryRoutes(app, processes, storage);
769
833
  registerWorkspaceRoutes(app, storage, sessionRegistry, { config });
770
834
  registerMissionRoutes(app, missions);
@@ -837,7 +901,7 @@ export async function startServer(config, configPath, options = {}) {
837
901
  ? "qodercli"
838
902
  : rawCommand;
839
903
  const effectiveModel = provider
840
- ? rawModel || getDefaultModelForProvider(config, provider) || undefined
904
+ ? rawModel || defaultModelGroupSelector(config.modelGroups, provider, getDefaultModelForProvider(config, provider)) || getDefaultModelForProvider(config, provider) || undefined
841
905
  : undefined;
842
906
  const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
843
907
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
@@ -957,6 +1021,7 @@ export async function startServer(config, configPath, options = {}) {
957
1021
  wsManager.emitEvent(event);
958
1022
  });
959
1023
  structuredSessions.setEventEmitter((event) => {
1024
+ conversationService.ingestSessionEvent(event);
960
1025
  sessionCompletions.ingest(event);
961
1026
  missions.ingest(event);
962
1027
  aiTeams.ingest(event);
@@ -970,10 +1035,14 @@ export async function startServer(config, configPath, options = {}) {
970
1035
  type: "notification", sessionId: "__system__", data: { kind: "ai-team-step-live", ...update },
971
1036
  });
972
1037
  };
1038
+ notifyConversationSession = update => {
1039
+ wsManager.emitEvent({ type: "notification", sessionId: "__system__", data: { kind: "conversation-session-preview", ...update } });
1040
+ };
973
1041
  // Re-attach structured CLI runs that kept going inside terminald while the
974
1042
  // previous web process was down; fire-and-forget, failures are logged inside.
975
1043
  void structuredSessions.recoverDetachedRuns();
976
1044
  aiTeams.reconcile();
1045
+ structuredSessions.resumeQueuedPiMessages();
977
1046
  // ── Restart endpoint (needs server + wss in scope) ──
978
1047
  /**
979
1048
  * 统一的关服 + 重启。重启方式由 computeRelaunch 决定:
@@ -981,12 +1050,28 @@ export async function startServer(config, configPath, options = {}) {
981
1050
  * 重写的)ExecStart 拉起,避免再 spawn detached 子进程与 systemd 抢单实例 pidfile;
982
1051
  * - 否则 → spawn 一个 detached 子进程(bin 优先全局安装,确保更新后跑到新版)再退出。
983
1052
  */
1053
+ let restartPending = false;
984
1054
  function relaunchAfterShutdown() {
985
- const plan = computeRelaunch({
986
- serviceInstalled: safeServiceInstalled(),
987
- globalCli: resolveGlobalWandCli(),
988
- });
989
- void close().finally(() => {
1055
+ if (restartPending)
1056
+ return;
1057
+ restartPending = true;
1058
+ const releaseDrain = structuredSessions.beginCoreRestartDrain();
1059
+ void (async () => {
1060
+ if (structuredSessions.getCoreTurnStatus().hasActiveTurns) {
1061
+ process.stdout.write("[wand] 等待原生 Core 回合完成后再重启…\n");
1062
+ }
1063
+ while (structuredSessions.getCoreTurnStatus().hasActiveTurns) {
1064
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1065
+ }
1066
+ // Do not dispose tools/services or arm the force-exit timer while a native
1067
+ // turn is still running. The same barrier covers manual and auto restart.
1068
+ const plan = computeRelaunch({
1069
+ serviceInstalled: safeServiceInstalled(),
1070
+ globalCli: resolveGlobalWandCli(),
1071
+ });
1072
+ const forceExitTimer = setTimeout(() => process.exit(0), 5000);
1073
+ forceExitTimer.unref?.();
1074
+ await close();
990
1075
  if (plan.mode === "spawn") {
991
1076
  spawn(process.execPath, [plan.bin ?? "", ...(plan.args ?? [])], {
992
1077
  detached: true,
@@ -996,10 +1081,11 @@ export async function startServer(config, configPath, options = {}) {
996
1081
  }).unref();
997
1082
  }
998
1083
  process.exit(0);
1084
+ })().catch((error) => {
1085
+ restartPending = false;
1086
+ releaseDrain();
1087
+ wandError("等待 Core 回合后重启失败", getErrorMessage(error));
999
1088
  });
1000
- // Force exit after 5s if graceful shutdown stalls
1001
- const forceExitTimer = setTimeout(() => process.exit(0), 5000);
1002
- forceExitTimer.unref?.();
1003
1089
  }
1004
1090
  app.post("/api/restart", requireAdmin, asyncRoute(async (_req, res) => {
1005
1091
  res.json({ ok: true, message: "服务正在重启..." });
@@ -1019,10 +1105,12 @@ export async function startServer(config, configPath, options = {}) {
1019
1105
  const cleanupFailedListen = () => {
1020
1106
  shuttingDown = true;
1021
1107
  decisions.dispose();
1108
+ openRouter.dispose();
1022
1109
  try {
1023
1110
  processes.dispose();
1024
1111
  }
1025
1112
  catch { /* noop */ }
1113
+ conversationService.dispose();
1026
1114
  try {
1027
1115
  structuredSessions.dispose();
1028
1116
  }
@@ -1069,10 +1157,18 @@ export async function startServer(config, configPath, options = {}) {
1069
1157
  const address = server.address();
1070
1158
  const actualPort = typeof address === "object" && address ? address.port : config.port;
1071
1159
  bindAddr = `${config.host}:${actualPort}`;
1160
+ const decisionStatus = decisions.status();
1072
1161
  if (config.localDecision?.enabled) {
1073
1162
  const localHost = config.host === "::1" ? "[::1]" : config.host === "0.0.0.0" || config.host === "::" ? "127.0.0.1" : config.host;
1074
1163
  decisionRuntime = { url: `${protocol}://${localHost}:${actualPort}`,
1075
- ...(activeSslCertPath ? { caPath: activeSslCertPath } : {}) };
1164
+ ...(activeSslCertPath ? { caPath: activeSslCertPath } : {}),
1165
+ // core 会话直接调进程内决策服务,不再绕出 HTTP + env token。
1166
+ evaluate: (value, caller, signal) => decisions.evaluate(value, caller, signal) };
1167
+ }
1168
+ // 「智能分配」只要真正能跑(启用 + 平台/运行环境就绪)才由本地决策负责;
1169
+ // 否则转给系统员工的一次性文本回退,而不是每次白等一个必然失败的调用。
1170
+ if (config.localDecision?.enabled && decisionStatus.supported && decisionStatus.configured) {
1171
+ autoAssignEvaluate = (value, caller, signal) => decisions.evaluate(value, caller, signal);
1076
1172
  }
1077
1173
  const scheme = useHttps ? "HTTPS" : "HTTP";
1078
1174
  // 主 URL:本机回环;若绑定 0.0.0.0 再补一个对外提示。
@@ -1108,6 +1204,9 @@ export async function startServer(config, configPath, options = {}) {
1108
1204
  data: { kind: "models", revision: result.revision, refreshedAt: result.refreshedAt },
1109
1205
  });
1110
1206
  });
1207
+ openRouter.onChanged(() => modelCatalog.publishManagedPiModels());
1208
+ if (!testMode)
1209
+ openRouter.start();
1111
1210
  let modelCatalogRefreshTimer = null;
1112
1211
  if (!testMode) {
1113
1212
  void modelCatalog.refresh().catch(() => { });
@@ -1117,10 +1216,29 @@ export async function startServer(config, configPath, options = {}) {
1117
1216
  }, MODEL_CATALOG_AUTO_REFRESH_INTERVAL_MS);
1118
1217
  modelCatalogRefreshTimer.unref();
1119
1218
  }
1120
- // 保留期扫描:7 天没活动的会话/任务自动归档,归档 7 天后自动清理。
1219
+ // 会话仍按固定 7 天归档再清理;任务窗口每次扫描读取当前设置。
1220
+ // 保存任务保留设置后走同一条扫描,不等下一小时。
1221
+ const retentionDeps = {
1222
+ storage,
1223
+ sessions: sessionRegistry,
1224
+ taskRetention: () => config.taskRetention,
1225
+ };
1226
+ const sweepRetention = createRetentionSweep(retentionDeps);
1227
+ runTaskRetentionSweep = () => {
1228
+ const result = sweepRetention();
1229
+ try {
1230
+ wsManager.emitEvent({
1231
+ type: "notification",
1232
+ sessionId: "__system__",
1233
+ data: { kind: "task-retention", ...result },
1234
+ });
1235
+ }
1236
+ catch { /* 扫描结果已经返回给保存请求,通知失败不回滚 */ }
1237
+ return result;
1238
+ };
1121
1239
  let retentionTimer = null;
1122
1240
  if (!testMode) {
1123
- retentionTimer = startRetentionTimer({ storage, sessions: sessionRegistry });
1241
+ retentionTimer = startRetentionTimer(retentionDeps, 60 * 60 * 1000, sweepRetention);
1124
1242
  }
1125
1243
  // Express 4 does not forward rejected route promises automatically. Every
1126
1244
  // async route above is wrapped with asyncRoute, and this final middleware
@@ -1194,10 +1312,8 @@ export async function startServer(config, configPath, options = {}) {
1194
1312
  previousInstanceId: SERVER_INSTANCE_ID,
1195
1313
  },
1196
1314
  });
1197
- // Restart after a brief delay
1198
- const restartTimer = setTimeout(() => {
1199
- relaunchAfterShutdown();
1200
- }, 1000);
1315
+ // Manual and automatic restarts share the same live admission barrier.
1316
+ const restartTimer = setTimeout(() => relaunchAfterShutdown(), 1000);
1201
1317
  restartTimer.unref?.();
1202
1318
  }
1203
1319
  catch (error) {
@@ -1269,6 +1385,7 @@ export async function startServer(config, configPath, options = {}) {
1269
1385
  closePromise = (async () => {
1270
1386
  shuttingDown = true;
1271
1387
  decisions.dispose();
1388
+ openRouter.dispose();
1272
1389
  await userMemory.dispose();
1273
1390
  if (updateCheckTimer) {
1274
1391
  clearInterval(updateCheckTimer);
@@ -1318,6 +1435,7 @@ export async function startServer(config, configPath, options = {}) {
1318
1435
  processes.dispose();
1319
1436
  }
1320
1437
  catch { /* best-effort shutdown */ }
1438
+ conversationService.dispose();
1321
1439
  try {
1322
1440
  structuredSessions.dispose();
1323
1441
  }
@@ -1,3 +1,4 @@
1
+ import { type ModelGroup } from "./model-groups.js";
1
2
  import type { SiliconEmployee } from "./ai-team-types.js";
2
3
  import type { AiCliCandidate, SessionProvider, SessionSnapshot, WandConfig } from "./types.js";
3
4
  export interface SessionAiContext {
@@ -9,6 +10,9 @@ export interface SessionAiContext {
9
10
  opsPersona?: string;
10
11
  /** CLI 降级链(按顺序)。为空时只用 provider/model 这一次调用。 */
11
12
  cliCandidates?: AiCliCandidate[];
13
+ /** 系统应用必须经员工 CLI 渠道;候选为空时禁止退回当前会话/默认 provider。 */
14
+ employeeChannelOnly?: boolean;
15
+ modelGroups?: ModelGroup[];
12
16
  }
13
17
  /**
14
18
  * Resolve the provider from every representation used by current and legacy
@@ -17,6 +21,6 @@ export interface SessionAiContext {
17
21
  */
18
22
  export declare function resolveSessionProvider(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command">): SessionProvider;
19
23
  /** Build the provider-specific settings used by session-adjacent AI actions. */
20
- export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultThinkingEffort" | "inheritEnv">): SessionAiContext;
24
+ export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultThinkingEffort" | "inheritEnv" | "modelGroups">): SessionAiContext;
21
25
  /** Build the source order for Wand-owned AI features such as titles. */
22
26
  export declare function resolveSystemAiContext(snapshot: Parameters<typeof resolveSessionAiContext>[0], config: Parameters<typeof resolveSessionAiContext>[1] & Pick<WandConfig, "systemAiCli" | "systemAiModel">, systemEmployee?: SiliconEmployee | null): SessionAiContext;
@@ -1,4 +1,5 @@
1
1
  import { getDefaultModelForProvider } from "./config.js";
2
+ import { resolveModelGroupModels } from "./model-groups.js";
2
3
  import { providerCliInstalled, isSessionProvider } from "./session-provider.js";
3
4
  import { isSystemSiliconEmployee, systemEmployeeCliCandidates } from "./system-employee.js";
4
5
  import { inferProviderFromCommand, inferProviderFromRunner } from "./session-provider.js";
@@ -30,6 +31,7 @@ export function resolveSessionAiContext(snapshot, config) {
30
31
  model: sessionModel ?? defaultModel,
31
32
  thinkingEffort: snapshot.thinkingEffort ?? config.defaultThinkingEffort,
32
33
  inheritEnv: config.inheritEnv,
34
+ ...(config.modelGroups?.length ? { modelGroups: config.modelGroups } : {}),
33
35
  };
34
36
  }
35
37
  /** Build the source order for Wand-owned AI features such as titles. */
@@ -37,13 +39,14 @@ export function resolveSystemAiContext(snapshot, config, systemEmployee) {
37
39
  const sessionContext = resolveSessionAiContext(snapshot, config);
38
40
  // 候选里的「跟随默认模型」在这里就换成具体的 Wand 默认模型:降级到下一个 provider
39
41
  // 时不能拿上一个 provider 的模型,也不能把决定权交给 CLI 自己的默认值。
40
- const chain = systemEmployeeCliCandidates(systemEmployee).map((candidate) => ({
41
- ...candidate,
42
- model: candidate.model ?? normalizeModel(getDefaultModelForProvider(config, candidate.provider)),
43
- }));
42
+ const chain = systemEmployeeCliCandidates(systemEmployee).flatMap((candidate) => resolveModelGroupModels(config.modelGroups, candidate.provider, candidate.model ?? normalizeModel(getDefaultModelForProvider(config, candidate.provider)), {
43
+ preferDefault: !candidate.model || candidate.model === "default",
44
+ })
45
+ .map((model) => ({ ...candidate, model: normalizeModel(model) })));
44
46
  const owned = {
45
47
  ...sessionContext,
46
48
  ...(chain.length ? { cliCandidates: chain } : {}),
49
+ ...(systemEmployee ? { employeeChannelOnly: true } : {}),
47
50
  ...(isSystemSiliconEmployee(systemEmployee) ? { opsPersona: systemEmployee.prompt } : {}),
48
51
  };
49
52
  // 内置员工可用时,Wand 自有调用一律按它的候选链:provider/model 取首个已安装的
@@ -56,6 +59,8 @@ export function resolveSystemAiContext(snapshot, config, systemEmployee) {
56
59
  }
57
60
  if (chain.length)
58
61
  return owned;
62
+ if (systemEmployee)
63
+ return owned;
59
64
  // Older installations without a system AI CLI preference still follow the
60
65
  // current session. A chosen CLI must never inherit another provider's model.
61
66
  if (!isSessionProvider(config.systemAiCli))
@@ -3,6 +3,8 @@ import type { StructuredSessionManager } from "./structured-session-manager.js";
3
3
  import type { WandStorage } from "./storage.js";
4
4
  import type { ExecutionMode, SessionSnapshot } from "./types.js";
5
5
  export type SessionOwner = "structured" | "pty" | "storage";
6
+ export type ProviderHistoryDeleter = Pick<ProcessManager, "deleteClaudeHistoryFiles" | "deleteCodexHistoryFiles" | "deleteOpenCodeHistorySessions" | "deleteQoderHistoryFiles" | "deletePiHistoryFiles" | "deleteGrokHistoryFiles" | "deleteGeminiHistoryFiles">;
7
+ export declare function deleteProviderNativeHistory(deleter: ProviderHistoryDeleter, provider: string, providerSessionId: string, cwd: string): void;
6
8
  type HiddenSessionStore = Pick<WandStorage, "getConfigValue" | "setConfigValue">;
7
9
  /** 读取 `hidden_claude_session_ids`;坏数据 / 空值按空集合处理,不抛错。 */
8
10
  export declare function readHiddenSessionIds(storage: Pick<HiddenSessionStore, "getConfigValue">): Set<string>;
@@ -35,6 +37,8 @@ export declare class SessionRegistry {
35
37
  * 归档 / 取消归档一个会话:只写标记,不杀进程、不删历史;真正的清理留给保留期扫描。
36
38
  * 会话可能只存在于 storage(重启后未加载),因此 storage-only 也要能改。
37
39
  */
40
+ /** 停掉空闲 PTY 壳。保留期只对「进程还在、但没有正在处理的回合」调用。 */
41
+ stop(id: string): SessionSnapshot | null;
38
42
  setArchived(id: string, archived: boolean): SessionSnapshot | null;
39
43
  updateWorktreeState(id: string, status: SessionSnapshot["worktreeMergeStatus"], info: SessionSnapshot["worktreeMergeInfo"]): SessionSnapshot | null;
40
44
  delete(id: string): SessionSnapshot | null;
@@ -1,5 +1,35 @@
1
1
  import { cleanupWorktreeSync } from "./git-worktree.js";
2
2
  import { inferProviderFromCommand } from "./session-provider.js";
3
+ export function deleteProviderNativeHistory(deleter, provider, providerSessionId, cwd) {
4
+ const id = providerSessionId.trim();
5
+ if (!id)
6
+ return;
7
+ switch (provider) {
8
+ case "claude":
9
+ deleter.deleteClaudeHistoryFiles([{ claudeSessionId: id, cwd }]);
10
+ break;
11
+ case "codex":
12
+ deleter.deleteCodexHistoryFiles([id]);
13
+ break;
14
+ case "opencode":
15
+ deleter.deleteOpenCodeHistorySessions([id]);
16
+ break;
17
+ case "qoder":
18
+ deleter.deleteQoderHistoryFiles([id]);
19
+ break;
20
+ case "pi":
21
+ deleter.deletePiHistoryFiles([id]);
22
+ break;
23
+ case "grok":
24
+ deleter.deleteGrokHistoryFiles([id]);
25
+ break;
26
+ case "gemini":
27
+ deleter.deleteGeminiHistoryFiles([id]);
28
+ break;
29
+ default:
30
+ break;
31
+ }
32
+ }
3
33
  /** 读取 `hidden_claude_session_ids`;坏数据 / 空值按空集合处理,不抛错。 */
4
34
  export function readHiddenSessionIds(storage) {
5
35
  const raw = storage.getConfigValue("hidden_claude_session_ids");
@@ -143,6 +173,12 @@ export class SessionRegistry {
143
173
  * 归档 / 取消归档一个会话:只写标记,不杀进程、不删历史;真正的清理留给保留期扫描。
144
174
  * 会话可能只存在于 storage(重启后未加载),因此 storage-only 也要能改。
145
175
  */
176
+ /** 停掉空闲 PTY 壳。保留期只对「进程还在、但没有正在处理的回合」调用。 */
177
+ stop(id) {
178
+ if (this.ownerOf(id) !== "pty")
179
+ return this.get(id);
180
+ return this.processes.stop(id);
181
+ }
146
182
  setArchived(id, archived) {
147
183
  const owner = this.ownerOf(id);
148
184
  if (owner === "structured")
@@ -184,12 +220,18 @@ export class SessionRegistry {
184
220
  catch { /* never block deletion */ }
185
221
  }
186
222
  const owner = this.ownerOf(id);
187
- if (owner === "structured")
223
+ if (owner === "structured") {
188
224
  this.structured.delete(id);
189
- else if (owner === "pty")
225
+ }
226
+ else if (owner === "pty") {
227
+ this.processes.delete(id);
228
+ }
229
+ else if (snapshot.sessionKind === "structured") {
230
+ this.structured.delete(id);
231
+ }
232
+ else {
190
233
  this.processes.delete(id);
191
- else
192
- this.storage.deleteSession(id);
234
+ }
193
235
  return snapshot;
194
236
  }
195
237
  deleteWithProviderHistory(id) {
@@ -201,15 +243,7 @@ export class SessionRegistry {
201
243
  ?? snapshot.structuredState?.provider
202
244
  ?? inferProviderFromCommand(snapshot.command)
203
245
  ?? "claude";
204
- if (provider === "claude") {
205
- this.processes.deleteClaudeHistoryFiles([{ claudeSessionId: providerSessionId, cwd: snapshot.cwd }]);
206
- }
207
- else if (provider === "codex") {
208
- this.processes.deleteCodexHistoryFiles([providerSessionId]);
209
- }
210
- else {
211
- return snapshot;
212
- }
246
+ deleteProviderNativeHistory(this.processes, provider, providerSessionId, snapshot.cwd);
213
247
  addHiddenSessionIds(this.storage, [providerSessionId]);
214
248
  return snapshot;
215
249
  }
@@ -1,4 +1,5 @@
1
1
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
2
+ import { normalizeTurnAnchors } from "./structured-provider-common.js";
2
3
  import { collectSessionTopicBlocklist, shouldAcceptGeneratedSessionTitle, summarizeSessionTitleFromInput, } from "./session-topic.js";
3
4
  export const SESSION_TRANSPORT_OUTPUT_LIMIT = 200_000;
4
5
  function cleanDisplayTitle(value) {
@@ -35,6 +36,10 @@ function sessionBase(snapshot) {
35
36
  providerCliActive: snapshot.providerCliActive,
36
37
  providerCliExitCode: snapshot.providerCliExitCode,
37
38
  ptyBusy: snapshot.ptyBusy === true,
39
+ // 与 ptyBusy 同生命周期的本轮锚点(运行时事实,存储里没有对应列)。和 ptyBusy 一起
40
+ // 读成 null,客户端才不会在一条已经 idle 的会话上算出「已运行 N 分钟」。
41
+ ptyTurnStartedAt: snapshot.ptyBusy === true ? snapshot.ptyTurnStartedAt ?? null : null,
42
+ ptyLastActivityAt: snapshot.ptyBusy === true ? snapshot.ptyLastActivityAt ?? null : null,
38
43
  runner: snapshot.runner,
39
44
  command: snapshot.command,
40
45
  cwd: snapshot.cwd,
@@ -60,7 +65,8 @@ function sessionBase(snapshot) {
60
65
  claudeSessionId: snapshot.claudeSessionId,
61
66
  providerSessionId: snapshot.claudeSessionId,
62
67
  queuedMessages: snapshot.queuedMessages,
63
- structuredState: snapshot.structuredState,
68
+ // inFlight 之外的回合锚点一律读成 null(兜底历史库残值与重启降级窗口)。
69
+ structuredState: normalizeTurnAnchors(snapshot.structuredState),
64
70
  resumedFromSessionId: snapshot.resumedFromSessionId,
65
71
  autoRecovered: snapshot.autoRecovered,
66
72
  autoApprovePermissions: snapshot.autoApprovePermissions,
@@ -74,6 +80,7 @@ function sessionBase(snapshot) {
74
80
  workspaceTaskId: snapshot.workspaceTaskId,
75
81
  selectedModel: snapshot.selectedModel,
76
82
  thinkingEffort: snapshot.thinkingEffort,
83
+ piSettings: snapshot.piSettings,
77
84
  ptyCols: snapshot.ptyCols,
78
85
  ptyRows: snapshot.ptyRows,
79
86
  };