@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
@@ -1,3 +1,4 @@
1
+ import type { DecisionResult } from "./decision-types.js";
1
2
  import { EventEmitter } from "node:events";
2
3
  import { WandStorage } from "./storage.js";
3
4
  import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
@@ -39,6 +40,8 @@ export interface ProcessManagerOptions {
39
40
  ptyForegroundSampleMs?: number;
40
41
  /** Overrides the foreground probe itself, so tests can drive it deterministically. */
41
42
  samplePtyForegrounds?: typeof samplePtyForegrounds;
43
+ /** 本地决策入口;只有带首条提示词创建终端会话时用来结算「智能分配」。 */
44
+ resolveDecisionEvaluate?: () => ((value: unknown, caller: string, signal?: AbortSignal) => Promise<DecisionResult>) | undefined;
42
45
  }
43
46
  export declare class ProcessManager extends EventEmitter {
44
47
  private readonly config;
@@ -52,6 +55,7 @@ export declare class ProcessManager extends EventEmitter {
52
55
  private readonly ptyForegroundSampleMs;
53
56
  /** Foreground probe; injectable so tests can drive it without a real process table. */
54
57
  private readonly probePtyForegrounds;
58
+ private readonly resolveDecisionEvaluate;
55
59
  /** 24h archive scan timer */
56
60
  private archiveTimer;
57
61
  /** 前台进程组采样定时器(见 samplePtyForegrounds) */
@@ -68,6 +72,12 @@ export declare class ProcessManager extends EventEmitter {
68
72
  private orphanRecoveredCount;
69
73
  private readonly topicCoordinator;
70
74
  private readonly nativeTitles;
75
+ /**
76
+ * 静默期心跳:ptyBusy 且连续 TURN_QUIET_HEARTBEAT_MS 没有任何输出时,用现有的 status
77
+ * 事件重发一次 ptyBusy 事实与本轮锚点,证明「还在跑」。timer 只在回合在飞期间存在,
78
+ * clearPtyTurn / cleanupRecord / dispose 立即撤销。
79
+ */
80
+ private readonly ptyHeartbeat;
71
81
  private disposed;
72
82
  private readonly terminalHost;
73
83
  constructor(config: WandConfig, storage: WandStorage, configDir?: string, terminalHost?: TerminalHost, options?: ProcessManagerOptions);
@@ -100,6 +110,12 @@ export declare class ProcessManager extends EventEmitter {
100
110
  /** The quiet window elapsed: the CLI is back at its prompt, so the turn is done. */
101
111
  private closePtyTurn;
102
112
  private clearPtyTurn;
113
+ /**
114
+ * 静默窗口到期:确认这一轮仍在飞才补发,payload 与一次普通 ptyBusy status 事件同形
115
+ * (不带新字段、不用新事件类型)。停在权限裁决上的回合不广播「仍在运行」——那一屏
116
+ * 已有 pendingEscalation 这个显式指示,再说在跑是错的(保留窗口,等输出恢复)。
117
+ */
118
+ private publishPtyQuietHeartbeat;
103
119
  private bindTerminalProcess;
104
120
  private handleTerminalResync;
105
121
  private handleTerminalExit;
@@ -144,6 +160,9 @@ export declare class ProcessManager extends EventEmitter {
144
160
  deleteOpenCodeHistorySessions(sessionIds: string[]): number;
145
161
  listQoderHistorySessions(): QoderHistorySession[];
146
162
  deleteQoderHistoryFiles(sessionIds: string[]): number;
163
+ deletePiHistoryFiles(sessionIds: string[]): number;
164
+ deleteGrokHistoryFiles(sessionIds: string[]): number;
165
+ deleteGeminiHistoryFiles(sessionIds: string[]): number;
147
166
  private captureCodexSessionId;
148
167
  private captureOpenCodeSessionId;
149
168
  private captureClaudeSessionId;
@@ -158,7 +177,8 @@ export declare class ProcessManager extends EventEmitter {
158
177
  */
159
178
  setSessionModel(id: string, model: string | null): SessionSnapshot;
160
179
  /**
161
- * 归档 / 取消归档一个 PTY 会话:只写标记,不杀进程;保留期清理再按 archivedAt 删除。
180
+ * 归档会先停掉还在跑的终端,但保留会话 ID 和 provider session id,恢复时用它们续上。
181
+ * 取消归档只清标记;重新拉起由恢复入口负责。
162
182
  */
163
183
  setSessionArchived(id: string, archived: boolean): SessionSnapshot;
164
184
  /**
@@ -1,3 +1,7 @@
1
+ import { isOpenRouterFreeSelector } from "./openrouter-free-models.js";
2
+ import { isAutoAssignSelector, defaultModelGroupSelector, resolveModelGroupModels } from "./model-groups.js";
3
+ import { resolveAutoAssign } from "./model-auto-assign.js";
4
+ import { getDefaultModelForProvider } from "./config.js";
1
5
  import { randomUUID } from "node:crypto";
2
6
  import { EventEmitter } from "node:events";
3
7
  import { existsSync, unlinkSync, rmSync, readFileSync, readdirSync, statSync } from "node:fs";
@@ -9,7 +13,7 @@ import { SessionLogger } from "./session-logger.js";
9
13
  import { ClaudePtyBridge } from "./claude-pty-bridge.js";
10
14
  import { truncateMessagesForTransport } from "./message-truncator.js";
11
15
  import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
12
- import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
16
+ import { buildChildEnv, isRunningAsRoot, systemEnvValue } from "./env-utils.js";
13
17
  import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
14
18
  import { prepareSessionWorktree } from "./git-worktree.js";
15
19
  import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
@@ -17,11 +21,12 @@ import { commandWithSystemPrompt, composeSystemFallback, normalizeThinkingEffort
17
21
  import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionNativeTitleTracker, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
18
22
  import { readNativeSessionTitle } from "./native-session-title.js";
19
23
  import { getErrorMessage } from "./error-utils.js";
24
+ import { TurnQuietHeartbeat } from "./turn-heartbeat.js";
20
25
  import { recordIterationPrompt } from "./iteration-log.js";
21
26
  import { describePtySpawnFailure } from "./ensure-node-pty-helper.js";
22
27
  import { resolveSessionProvider, resolveSystemAiContext } from "./session-ai-context.js";
23
28
  import { resolveSessionCwd } from "./session-cwd.js";
24
- import { inferProviderFromCommand } from "./session-provider.js";
29
+ import { inferProviderFromCommand, providerCliCommand } from "./session-provider.js";
25
30
  import { RETENTION_IDLE_MS } from "./retention.js";
26
31
  import { PtyTerminalState } from "./pty-terminal-state.js";
27
32
  import { isPtySubmitInput } from "./pty-turn-activity.js";
@@ -482,7 +487,7 @@ function restoreTerminalState(state) {
482
487
  return terminal;
483
488
  }
484
489
  const MAX_SESSIONS = 200;
485
- // 与会话/任务的统一保留期一致:7 天没活动才自动归档(随后由保留期扫描在 7 天后清理)。
490
+ // 会话空闲 7 天后在这里自动归档,再由保留期扫描于 7 天后清理。任务窗口在设置里单独配置。
486
491
  const ARCHIVE_AFTER_MS = RETENTION_IDLE_MS;
487
492
  const CONFIRM_WINDOW_SIZE = 800;
488
493
  /**
@@ -558,6 +563,7 @@ export class ProcessManager extends EventEmitter {
558
563
  ptyForegroundSampleMs;
559
564
  /** Foreground probe; injectable so tests can drive it without a real process table. */
560
565
  probePtyForegrounds;
566
+ resolveDecisionEvaluate;
561
567
  /** 24h archive scan timer */
562
568
  archiveTimer = null;
563
569
  /** 前台进程组采样定时器(见 samplePtyForegrounds) */
@@ -574,6 +580,12 @@ export class ProcessManager extends EventEmitter {
574
580
  orphanRecoveredCount = 0;
575
581
  topicCoordinator = new SessionTopicCoordinator();
576
582
  nativeTitles = new SessionNativeTitleTracker();
583
+ /**
584
+ * 静默期心跳:ptyBusy 且连续 TURN_QUIET_HEARTBEAT_MS 没有任何输出时,用现有的 status
585
+ * 事件重发一次 ptyBusy 事实与本轮锚点,证明「还在跑」。timer 只在回合在飞期间存在,
586
+ * clearPtyTurn / cleanupRecord / dispose 立即撤销。
587
+ */
588
+ ptyHeartbeat = new TurnQuietHeartbeat((sessionId) => this.publishPtyQuietHeartbeat(sessionId));
577
589
  disposed = false;
578
590
  terminalHost;
579
591
  constructor(config, storage, configDir, terminalHost, options = {}) {
@@ -583,6 +595,7 @@ export class ProcessManager extends EventEmitter {
583
595
  this.ptyTurnIdleMs = options.ptyTurnIdleMs ?? PTY_TURN_IDLE_MS;
584
596
  this.ptyForegroundSampleMs = options.ptyForegroundSampleMs ?? PTY_FOREGROUND_SAMPLE_MS;
585
597
  this.probePtyForegrounds = options.samplePtyForegrounds ?? samplePtyForegrounds;
598
+ this.resolveDecisionEvaluate = options.resolveDecisionEvaluate ?? (() => undefined);
586
599
  this.terminalHost = terminalHost ?? new InProcessTerminalHost();
587
600
  this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
588
601
  let startupCodexHistory = null;
@@ -802,7 +815,8 @@ export class ProcessManager extends EventEmitter {
802
815
  initializeClaudeBridge(record, initialOutput) {
803
816
  if (record.provider !== "claude" || !record.providerCliActive)
804
817
  return;
805
- record.ptyBusy = false;
818
+ // 新 bridge 接管 ptyBusy 与本轮锚点:从干净的一轮起步,锚点由 bridge 的翻转写入。
819
+ this.clearPtyTurn(record);
806
820
  record.ptyBridge?.removeAllListeners();
807
821
  record.ptyBridge = new ClaudePtyBridge({
808
822
  sessionId: record.id,
@@ -839,7 +853,16 @@ export class ProcessManager extends EventEmitter {
839
853
  return;
840
854
  if (record.ptyBusy !== true) {
841
855
  record.ptyBusy = true;
842
- this.emitEvent({ type: "status", sessionId: record.id, data: { ptyBusy: true } });
856
+ // 本轮起点:两个锚点同时落下,客户端据此算「这一轮跑了多久 / 多久没动静」。
857
+ const at = new Date().toISOString();
858
+ record.ptyTurnStartedAt = at;
859
+ record.ptyLastActivityAt = at;
860
+ this.ptyHeartbeat.observe(record.id);
861
+ this.emitEvent({
862
+ type: "status",
863
+ sessionId: record.id,
864
+ data: { ptyBusy: true, ptyTurnStartedAt: at, ptyLastActivityAt: at },
865
+ });
843
866
  }
844
867
  this.refreshPtyTurn(record);
845
868
  }
@@ -847,6 +870,10 @@ export class ProcessManager extends EventEmitter {
847
870
  refreshPtyTurn(record) {
848
871
  if (record.ptyBridge || !this.canTrackPtyTurn(record))
849
872
  return;
873
+ // 输出续期只动 lastActivityAt;turnStartedAt 留在本轮开始处,否则「已运行多久」
874
+ // 会被每次 chunk 重置。静默窗口跟着往后推。
875
+ record.ptyLastActivityAt = new Date().toISOString();
876
+ this.ptyHeartbeat.observe(record.id);
850
877
  if (record.ptyTurnTimer)
851
878
  clearTimeout(record.ptyTurnTimer);
852
879
  const timer = setTimeout(() => {
@@ -864,13 +891,47 @@ export class ProcessManager extends EventEmitter {
864
891
  if (record.ptyBusy !== true)
865
892
  return;
866
893
  record.ptyBusy = false;
867
- this.emitEvent({ type: "status", sessionId: record.id, data: { ptyBusy: false } });
894
+ this.emitEvent({
895
+ type: "status",
896
+ sessionId: record.id,
897
+ // 锚点与 ptyBusy 一起收敛成 null,不残留「还在跑」的读数。
898
+ data: { ptyBusy: false, ptyTurnStartedAt: null, ptyLastActivityAt: null },
899
+ });
868
900
  }
869
901
  clearPtyTurn(record) {
870
902
  if (record.ptyTurnTimer) {
871
903
  clearTimeout(record.ptyTurnTimer);
872
904
  record.ptyTurnTimer = null;
873
905
  }
906
+ // ptyBusy 清 false 的一切路径(退回提示符、CLI 退出、finishProviderCli、stop、
907
+ // cleanupRecord)都经过这里:撤掉静默心跳,锚点一并回 null。
908
+ this.ptyHeartbeat.cancel(record.id);
909
+ record.ptyTurnStartedAt = null;
910
+ record.ptyLastActivityAt = null;
911
+ }
912
+ /**
913
+ * 静默窗口到期:确认这一轮仍在飞才补发,payload 与一次普通 ptyBusy status 事件同形
914
+ * (不带新字段、不用新事件类型)。停在权限裁决上的回合不广播「仍在运行」——那一屏
915
+ * 已有 pendingEscalation 这个显式指示,再说在跑是错的(保留窗口,等输出恢复)。
916
+ */
917
+ publishPtyQuietHeartbeat(id) {
918
+ if (this.disposed)
919
+ return false;
920
+ const record = this.sessions.get(id);
921
+ if (!record || record.ptyBusy !== true)
922
+ return false;
923
+ if (record.pendingEscalation || this.isPermissionBlocked(record))
924
+ return true;
925
+ this.emitEvent({
926
+ type: "status",
927
+ sessionId: id,
928
+ data: {
929
+ ptyBusy: true,
930
+ ptyTurnStartedAt: record.ptyTurnStartedAt ?? null,
931
+ ptyLastActivityAt: record.ptyLastActivityAt ?? null,
932
+ },
933
+ });
934
+ return true;
874
935
  }
875
936
  bindTerminalProcess(record, child, onVisibleChunk) {
876
937
  child.onResync?.((state) => this.handleTerminalResync(record, child, state));
@@ -1039,6 +1100,8 @@ export class ProcessManager extends EventEmitter {
1039
1100
  if (this.disposed)
1040
1101
  return;
1041
1102
  this.disposed = true;
1103
+ // 静默心跳不跨服务关闭存活;每个会话的 timer 也在 cleanupRecord 里单独撤销。
1104
+ this.ptyHeartbeat.dispose();
1042
1105
  if (this.archiveTimer) {
1043
1106
  clearInterval(this.archiveTimer);
1044
1107
  this.archiveTimer = null;
@@ -1131,8 +1194,52 @@ export class ProcessManager extends EventEmitter {
1131
1194
  async start(command, cwd, mode, initialInput, opts) {
1132
1195
  if (this.disposed)
1133
1196
  throw new Error("ProcessManager has been disposed.");
1197
+ if (isOpenRouterFreeSelector(opts?.model))
1198
+ throw new Error("免费分组仅支持 one 的 Agent 结构化对话。");
1134
1199
  if (!opts?.interactiveShell)
1135
1200
  this.assertCommandAllowed(command);
1201
+ const groupProvider = opts?.interactiveShell ? undefined : opts?.provider ?? inferProviderFromCommand(command);
1202
+ const configured = groupProvider ? getDefaultModelForProvider(this.config, groupProvider) : "";
1203
+ const requested = opts?.model?.trim();
1204
+ let selector = requested && requested !== "default" ? requested : configured;
1205
+ /** 「智能分配」在终端里结算后的落地选择器;不让会话停在哨兵上说不清实际用了什么。 */
1206
+ let autoAssignedSelector;
1207
+ // 终端在创建时就带着首条提示词(任务派发 / API)时,可以按提示词结算「智能分配」;
1208
+ // 交互式终端没有提示词,只能回落默认分组,不能靠猜。
1209
+ if (groupProvider && isAutoAssignSelector(selector)) {
1210
+ const prompt = initialInput?.trim() ?? "";
1211
+ // 没有提示词时的落地值:默认分组(没有分组时才是该工具配置的默认模型)。
1212
+ const fallbackGroup = defaultModelGroupSelector(this.config.modelGroups, groupProvider, undefined) ?? "";
1213
+ const fallback = fallbackGroup || (isAutoAssignSelector(configured) ? "" : configured);
1214
+ if (prompt) {
1215
+ const assigned = await resolveAutoAssign({
1216
+ provider: groupProvider,
1217
+ prompt,
1218
+ groups: this.config.modelGroups,
1219
+ evaluate: this.resolveDecisionEvaluate(),
1220
+ ai: resolveSystemAiContext({
1221
+ provider: groupProvider, runner: undefined, command: providerCliCommand(groupProvider),
1222
+ structuredState: undefined, selectedModel: null, thinkingEffort: opts?.thinkingEffort,
1223
+ }, this.config, this.storage.getSystemSiliconEmployee()),
1224
+ cwd: resolveSessionCwd(cwd, this.config.defaultCwd),
1225
+ language: this.config.language,
1226
+ });
1227
+ selector = assigned.selector || fallback;
1228
+ autoAssignedSelector = assigned.selector || fallback || undefined;
1229
+ if (assigned.group)
1230
+ console.info(`[AutoAssign] 终端会话使用分组「${assigned.group.name}」(${assigned.strategy})`);
1231
+ }
1232
+ else {
1233
+ console.info("[AutoAssign] 终端会话没有首条提示词,使用默认分组");
1234
+ selector = fallback;
1235
+ autoAssignedSelector = fallback || undefined;
1236
+ }
1237
+ }
1238
+ const preferredModels = groupProvider ? resolveModelGroupModels(this.config.modelGroups, groupProvider, selector, {
1239
+ preferDefault: !requested || requested === "default",
1240
+ }) : [requested ?? ""];
1241
+ if (preferredModels.some(isOpenRouterFreeSelector))
1242
+ throw new Error("免费模型分组仅支持 one 的 Agent 结构化对话。");
1136
1243
  const baseCwd = resolveSessionCwd(cwd, this.config.defaultCwd);
1137
1244
  const id = opts?.reuseId || randomUUID();
1138
1245
  // When a session is being resumed under the same id, capture its prior
@@ -1178,9 +1285,11 @@ export class ProcessManager extends EventEmitter {
1178
1285
  : opts?.provider ?? inferProviderFromCommand(command);
1179
1286
  const effectiveMode = provider === "codex" ? "full-access" : mode;
1180
1287
  const isClaudeProvider = provider === "claude";
1181
- const selectedModel = opts?.model?.trim() || undefined;
1288
+ const selectedModel = autoAssignedSelector ?? (opts?.model?.trim() || undefined);
1289
+ // Resolve before touching a resume record/worktree; invalid groups must not disturb prior work.
1290
+ // PTY has no reliable pre-input acceptance fact: use the preferred member, never replay automatically.
1182
1291
  const initialThinkingEffort = normalizeThinkingEffort(opts?.thinkingEffort);
1183
- let processedCommand = this.processCommandForMode(command, effectiveMode, provider, selectedModel, initialThinkingEffort);
1292
+ let processedCommand = this.processCommandForMode(command, effectiveMode, provider, preferredModels[0], initialThinkingEffort);
1184
1293
  // 会话级系统提示:provider 有系统提示开关就走开关(PTY 与结构化行为一致),没有就并进首条输入。
1185
1294
  const systemPrompt = (opts?.systemPrompt ?? inheritedRole?.systemPrompt)?.trim() || undefined;
1186
1295
  const initialInputText = initialInput && systemPrompt && !systemPromptFlag(provider)
@@ -1300,8 +1409,8 @@ export class ProcessManager extends EventEmitter {
1300
1409
  WAND_AUTO_CONFIRM: record.autoApprovePermissions ? "1" : "0",
1301
1410
  WAND_AUTO_EDIT: effectiveMode === "auto-edit" ? "1" : "0",
1302
1411
  SHELL: this.config.shell,
1303
- LANG: process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || "C.UTF-8",
1304
- LC_CTYPE: process.env.LC_CTYPE || process.env.LANG || process.env.LC_ALL || "C.UTF-8",
1412
+ LANG: systemEnvValue("LANG") || systemEnvValue("LC_ALL") || systemEnvValue("LC_CTYPE") || "C.UTF-8",
1413
+ LC_CTYPE: systemEnvValue("LC_CTYPE") || systemEnvValue("LANG") || systemEnvValue("LC_ALL") || "C.UTF-8",
1305
1414
  }),
1306
1415
  name: "xterm-256color",
1307
1416
  // 使用 record 上由前端协商好的真实尺寸,避免"先 120 列、几百毫秒后再 resize"
@@ -1525,6 +1634,15 @@ export class ProcessManager extends EventEmitter {
1525
1634
  deleteQoderHistoryFiles(sessionIds) {
1526
1635
  return this.providerHistory.deleteQoderHistoryFiles(sessionIds);
1527
1636
  }
1637
+ deletePiHistoryFiles(sessionIds) {
1638
+ return this.providerHistory.deletePiHistoryFiles(sessionIds);
1639
+ }
1640
+ deleteGrokHistoryFiles(sessionIds) {
1641
+ return this.providerHistory.deleteGrokHistoryFiles(sessionIds);
1642
+ }
1643
+ deleteGeminiHistoryFiles(sessionIds) {
1644
+ return this.providerHistory.deleteGeminiHistoryFiles(sessionIds);
1645
+ }
1528
1646
  captureCodexSessionId(record, options) {
1529
1647
  if (record.provider !== "codex" || record.claudeSessionId) {
1530
1648
  return false;
@@ -1630,9 +1748,16 @@ export class ProcessManager extends EventEmitter {
1630
1748
  setSessionModel(id, model) {
1631
1749
  const record = this.mustGet(id);
1632
1750
  const normalized = model?.trim() || null;
1751
+ const configured = record.provider ? getDefaultModelForProvider(this.config, record.provider) : "";
1752
+ const selector = normalized && normalized !== "default" ? normalized : configured;
1753
+ const members = resolveModelGroupModels(this.config.modelGroups, record.provider ?? "claude", selector, {
1754
+ preferDefault: !normalized || normalized === "default",
1755
+ });
1756
+ if (members.some(isOpenRouterFreeSelector))
1757
+ throw new Error("免费分组仅支持 one 的 Agent 结构化对话。");
1633
1758
  record.selectedModel = normalized;
1634
1759
  if (record.providerCliActive && record.provider === "claude" && record.status === "running" && record.ptyProcess) {
1635
- const value = normalized && normalized !== "default" ? normalized : "default";
1760
+ const value = members[0] && members[0] !== "default" ? members[0] : "default";
1636
1761
  record.ptyProcess.write(`/model ${value}\r`);
1637
1762
  }
1638
1763
  this.persist(record);
@@ -1640,14 +1765,22 @@ export class ProcessManager extends EventEmitter {
1640
1765
  return this.snapshot(record);
1641
1766
  }
1642
1767
  /**
1643
- * 归档 / 取消归档一个 PTY 会话:只写标记,不杀进程;保留期清理再按 archivedAt 删除。
1768
+ * 归档会先停掉还在跑的终端,但保留会话 ID 和 provider session id,恢复时用它们续上。
1769
+ * 取消归档只清标记;重新拉起由恢复入口负责。
1644
1770
  */
1645
1771
  setSessionArchived(id, archived) {
1772
+ if (archived && this.mustGet(id).status === "running") {
1773
+ this.stop(id);
1774
+ }
1646
1775
  const record = this.mustGet(id);
1647
1776
  record.archived = archived;
1648
- record.archivedAt = archived ? new Date().toISOString() : null;
1777
+ record.archivedAt = archived ? (record.archivedAt ?? new Date().toISOString()) : null;
1649
1778
  this.persist(record);
1650
- this.emitEvent({ type: "status", sessionId: id, data: { archived, archivedAt: record.archivedAt } });
1779
+ this.emitEvent({
1780
+ type: "status",
1781
+ sessionId: id,
1782
+ data: { archived, archivedAt: record.archivedAt, claudeSessionId: record.claudeSessionId },
1783
+ });
1651
1784
  return this.snapshot(record);
1652
1785
  }
1653
1786
  /**
@@ -1967,6 +2100,8 @@ export class ProcessManager extends EventEmitter {
1967
2100
  delete(id) {
1968
2101
  const record = this.mustGet(id);
1969
2102
  // Always clear pending timers
2103
+ // 会话删除后迟到的回合回调要失效,静默心跳也不能再给不存在的会话发事件。
2104
+ this.ptyHeartbeat.cancel(id);
1970
2105
  if (record.claudeTaskDiscoveryTimer) {
1971
2106
  clearTimeout(record.claudeTaskDiscoveryTimer);
1972
2107
  record.claudeTaskDiscoveryTimer = null;
@@ -2080,6 +2215,9 @@ export class ProcessManager extends EventEmitter {
2080
2215
  providerCliActive: record.providerCliActive,
2081
2216
  providerCliExitCode: record.providerCliExitCode,
2082
2217
  ptyBusy: record.ptyBridge ? record.ptyBridge.isResponding() : record.ptyBusy === true,
2218
+ // 与 ptyBusy 同生命周期的本轮锚点。运行时事实:存储没有对应列,重启后为 null。
2219
+ ptyTurnStartedAt: record.ptyTurnStartedAt ?? null,
2220
+ ptyLastActivityAt: record.ptyLastActivityAt ?? null,
2083
2221
  runner: "pty",
2084
2222
  command: record.command,
2085
2223
  cwd: record.cwd,
@@ -2494,12 +2632,27 @@ export class ProcessManager extends EventEmitter {
2494
2632
  const responding = bridgeData?.isResponding === true;
2495
2633
  if (record.ptyBusy !== responding) {
2496
2634
  record.ptyBusy = responding;
2635
+ // Claude 的 bridge 精确知道回合边界:翻转为 busy 时落下本轮锚点并挂静默
2636
+ // 心跳;回复结束就一起清回 null。
2637
+ const at = responding ? new Date().toISOString() : null;
2638
+ record.ptyTurnStartedAt = at;
2639
+ record.ptyLastActivityAt = at;
2640
+ if (responding)
2641
+ this.ptyHeartbeat.observe(record.id);
2642
+ else
2643
+ this.ptyHeartbeat.cancel(record.id);
2497
2644
  this.emitEvent({
2498
2645
  type: "status",
2499
2646
  sessionId: event.sessionId,
2500
- data: { ptyBusy: responding },
2647
+ data: { ptyBusy: responding, ptyTurnStartedAt: at, ptyLastActivityAt: at },
2501
2648
  });
2502
2649
  }
2650
+ else if (responding) {
2651
+ // 回合仍在飞:每个 chat 事件都是活动证据,只续 lastActivityAt 与静默窗口,
2652
+ // 不额外广播(流式期间每 chunk 一条 status 会把 WS 刷爆)。
2653
+ record.ptyLastActivityAt = new Date().toISOString();
2654
+ this.ptyHeartbeat.observe(record.id);
2655
+ }
2503
2656
  const data = {
2504
2657
  permissionBlocked: this.isPermissionBlocked(record),
2505
2658
  };
@@ -35,7 +35,7 @@ export const PROVIDER_LABELS = {
35
35
  opencode: "OpenCode",
36
36
  grok: "Grok",
37
37
  qoder: "Qoder",
38
- pi: "Pi",
38
+ pi: "one 的 Agent",
39
39
  gemini: "Gemini",
40
40
  };
41
41
  /**
@@ -127,6 +127,12 @@ export function parseHomebrewInstall(executable) {
127
127
  function isUpdateSupported(id, version) {
128
128
  return !(id === "opencode" && version !== null && /^0\.0\./.test(version));
129
129
  }
130
+ function isCodexOptionalDependencyError(error) {
131
+ return /Missing optional dependency\s+@openai\/codex-[\w-]+/.test(error ?? "");
132
+ }
133
+ function npmBin(options) {
134
+ return options.env?.WAND_NPM_BIN || process.env.WAND_NPM_BIN || (process.platform === "win32" ? "npm.cmd" : "npm");
135
+ }
130
136
  export function providerCliUpdateAvailable(currentVersion, latestVersion) {
131
137
  if (!currentVersion || !latestVersion)
132
138
  return false;
@@ -160,9 +166,8 @@ async function readLatestVersion(spec, options, status) {
160
166
  }
161
167
  if (!spec.npmPackage)
162
168
  return { version: null, error: "该 CLI 没有配置版本来源。" };
163
- const npm = options.env?.WAND_NPM_BIN || process.env.WAND_NPM_BIN || (process.platform === "win32" ? "npm.cmd" : "npm");
164
169
  try {
165
- const result = await runCommand(npm, ["view", `${spec.npmPackage}@latest`, "version"], options.registryTimeoutMs ?? REGISTRY_TIMEOUT_MS, options);
170
+ const result = await runCommand(npmBin(options), ["view", `${spec.npmPackage}@latest`, "version"], options.registryTimeoutMs ?? REGISTRY_TIMEOUT_MS, options);
166
171
  const version = parseProviderCliVersion(result.stdout);
167
172
  return version ? { version } : { version: null, error: "npm registry 未返回版本。" };
168
173
  }
@@ -250,6 +255,7 @@ export async function checkProviderCliUpdates(options = {}) {
250
255
  const installKind = resolveInstallKind(installed.executable, spec.id, installed.version);
251
256
  const latest = await readLatestVersion(spec, options, { executable: installed.executable, installKind });
252
257
  const updateSupported = isUpdateSupported(spec.id, installed.version);
258
+ const repairableCodexInstall = spec.id === "codex" && isCodexOptionalDependencyError(installed.error);
253
259
  const errors = [installed.error, latest.error].filter(Boolean);
254
260
  if (!updateSupported) {
255
261
  errors.push("检测到已归档的 OpenCode 0.0.x;请先卸载旧包并安装 opencode-ai@latest。");
@@ -262,7 +268,7 @@ export async function checkProviderCliUpdates(options = {}) {
262
268
  installed: installed.executable !== null,
263
269
  currentVersion: installed.version,
264
270
  latestVersion: latest.version,
265
- updateAvailable: providerCliUpdateAvailable(installed.version, latest.version),
271
+ updateAvailable: repairableCodexInstall || providerCliUpdateAvailable(installed.version, latest.version),
266
272
  updateSupported,
267
273
  installKind,
268
274
  ...(errors.length ? { error: errors.join(";") } : {}),
@@ -335,10 +341,25 @@ export async function updateProviderClis(statuses, ids, options = {}) {
335
341
  }
336
342
  options.onLog?.(`[CLI Update] ${spec.label}: ${status.currentVersion} -> ${status.latestVersion}`);
337
343
  try {
338
- // 没有自更新子命令的 CLI(Gemini)走它的 npm 发布渠道。
339
- const output = spec.updateArgs
340
- ? await runCommand(executable, spec.updateArgs, options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options)
341
- : await runCommand(options.env?.WAND_NPM_BIN || process.env.WAND_NPM_BIN || (process.platform === "win32" ? "npm.cmd" : "npm"), ["install", "-g", `${spec.npmPackage}@latest`], options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
344
+ const repairCodexInstall = spec.id === "codex" && isCodexOptionalDependencyError(status.error);
345
+ // Codex 的 Node shim 依赖平台 optional package;缺失时任何 `codex ...` 子命令都会先崩溃,只能用 npm 重装修复。
346
+ let output;
347
+ if (repairCodexInstall) {
348
+ output = await runCommand(npmBin(options), ["install", "-g", `${spec.npmPackage}@latest`, "--include=optional"], options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
349
+ }
350
+ else if (spec.updateArgs) {
351
+ output = await runCommand(executable, spec.updateArgs, options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
352
+ }
353
+ else {
354
+ // 没有自更新子命令的 CLI(Gemini)走它的 npm 发布渠道。
355
+ output = await runCommand(npmBin(options), ["install", "-g", `${spec.npmPackage}@latest`], options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
356
+ }
357
+ if (spec.id === "codex") {
358
+ const verified = await readInstalledVersion(spec, options);
359
+ if (!verified.version) {
360
+ throw new Error(`Codex 更新后仍不可用:${verified.error ?? "无法读取版本。"}`);
361
+ }
362
+ }
342
363
  const combined = trimOutput([output.stdout, output.stderr].filter(Boolean).join("\n"));
343
364
  results.push({
344
365
  id: spec.id,
@@ -347,7 +368,7 @@ export async function updateProviderClis(statuses, ids, options = {}) {
347
368
  skipped: false,
348
369
  fromVersion: status.currentVersion,
349
370
  toVersion: status.latestVersion,
350
- message: `${spec.label} 更新命令执行完成。`,
371
+ message: repairCodexInstall ? `${spec.label} optional dependency 已修复。` : `${spec.label} 更新命令执行完成。`,
351
372
  ...(combined ? { output: combined } : {}),
352
373
  });
353
374
  }
@@ -48,6 +48,9 @@ export interface ProviderHistoryScannerOptions {
48
48
  openCodeDatabasePath?: string;
49
49
  /** Qoder and Qoder CN use separate config roots; callers may override both for tests. */
50
50
  qoderProjectsDirs?: string[];
51
+ piSessionsDir?: string;
52
+ grokSessionsDir?: string;
53
+ geminiHome?: string;
51
54
  }
52
55
  /**
53
56
  * Incremental provider-history index. Directory entries are refreshed on each
@@ -59,6 +62,9 @@ export declare class ProviderHistoryScanner {
59
62
  private readonly codexSessionsDir;
60
63
  private readonly openCodeDatabasePath;
61
64
  private readonly qoderProjectsDirs;
65
+ private readonly piSessionsDir;
66
+ private readonly grokSessionsDir;
67
+ private readonly geminiHome;
62
68
  private readonly claudeIndex;
63
69
  private readonly codexIndex;
64
70
  private readonly qoderIndex;
@@ -88,4 +94,7 @@ export declare class ProviderHistoryScanner {
88
94
  deleteCodexHistoryFiles(threadIds: string[]): number;
89
95
  deleteOpenCodeHistorySessions(sessionIds: string[]): number;
90
96
  deleteQoderHistoryFiles(sessionIds: string[]): number;
97
+ deletePiHistoryFiles(sessionIds: string[]): number;
98
+ deleteGrokHistoryFiles(sessionIds: string[]): number;
99
+ deleteGeminiHistoryFiles(sessionIds: string[]): number;
91
100
  }