@co0ontty/wand 4.89.5 → 4.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +5 -3
  2. package/dist/agent-dispatch.js +4 -9
  3. package/dist/ai-team-employee-binding.js +10 -9
  4. package/dist/ai-team-runner.d.ts +2 -1
  5. package/dist/ai-team-runner.js +38 -2
  6. package/dist/ai-team-types.js +3 -2
  7. package/dist/ai-text-runner.d.ts +61 -0
  8. package/dist/ai-text-runner.js +653 -0
  9. package/dist/avatar-input.d.ts +2 -0
  10. package/dist/avatar-input.js +19 -0
  11. package/dist/bounded-process-output.d.ts +5 -0
  12. package/dist/bounded-process-output.js +9 -0
  13. package/dist/build-info.json +3 -3
  14. package/dist/command-error.d.ts +6 -0
  15. package/dist/command-error.js +8 -0
  16. package/dist/config.js +11 -42
  17. package/dist/conversation-service.d.ts +12 -2
  18. package/dist/conversation-service.js +104 -25
  19. package/dist/conversation-session-preview.d.ts +2 -0
  20. package/dist/conversation-session-preview.js +4 -0
  21. package/dist/decision-client.js +22 -6
  22. package/dist/decision-expert-employee.js +2 -2
  23. package/dist/decision-expert-identity.d.ts +1 -1
  24. package/dist/decision-expert-identity.js +1 -1
  25. package/dist/decision-expert-service.d.ts +1 -0
  26. package/dist/decision-expert-service.js +82 -76
  27. package/dist/decision-hardware.js +4 -3
  28. package/dist/decision-types.d.ts +4 -0
  29. package/dist/decision-types.js +4 -0
  30. package/dist/deployment-lock.d.ts +13 -0
  31. package/dist/deployment-lock.js +189 -0
  32. package/dist/employee-text.js +1 -1
  33. package/dist/fixed-employee-avatar.d.ts +7 -0
  34. package/dist/fixed-employee-avatar.js +13 -0
  35. package/dist/git-quick-commit.d.ts +5 -40
  36. package/dist/git-quick-commit.js +8 -636
  37. package/dist/hr-employee-identity.d.ts +5 -0
  38. package/dist/hr-employee-identity.js +5 -0
  39. package/dist/hr-employee.d.ts +5 -0
  40. package/dist/hr-employee.js +21 -0
  41. package/dist/middleware/path-safety.js +1 -1
  42. package/dist/milestone-scope.js +1 -1
  43. package/dist/model-auto-assign.d.ts +1 -1
  44. package/dist/model-auto-assign.js +1 -1
  45. package/dist/models.d.ts +4 -0
  46. package/dist/models.js +21 -1
  47. package/dist/npm-update-utils.js +5 -44
  48. package/dist/pi-auto-resources.js +6 -3
  49. package/dist/plush-avatar.d.ts +10 -1
  50. package/dist/plush-avatar.js +13 -1
  51. package/dist/plush-cat-appearance.d.ts +97 -0
  52. package/dist/plush-cat-appearance.js +118 -0
  53. package/dist/plush-cat-whiskers.d.ts +6 -0
  54. package/dist/plush-cat-whiskers.js +42 -0
  55. package/dist/process-manager.d.ts +22 -21
  56. package/dist/process-manager.js +49 -83
  57. package/dist/prompt-optimizer.d.ts +1 -1
  58. package/dist/prompt-optimizer.js +1 -1
  59. package/dist/render-host.js +1 -19
  60. package/dist/scheduled-task-service.d.ts +41 -0
  61. package/dist/scheduled-task-service.js +294 -0
  62. package/dist/scheduled-task-state.d.ts +11 -0
  63. package/dist/scheduled-task-state.js +65 -0
  64. package/dist/scheduled-task-time.d.ts +4 -0
  65. package/dist/scheduled-task-time.js +82 -0
  66. package/dist/scheduled-task-types.d.ts +71 -0
  67. package/dist/scheduled-task-types.js +1 -0
  68. package/dist/server-ai-team-routes.js +3 -17
  69. package/dist/server-employee-routes.d.ts +3 -0
  70. package/dist/server-employee-routes.js +20 -38
  71. package/dist/server-file-routes.js +44 -1
  72. package/dist/server-pi-recommendation-routes.js +13 -4
  73. package/dist/server-request.d.ts +2 -0
  74. package/dist/server-request.js +9 -0
  75. package/dist/server-resume-routes.js +1 -1
  76. package/dist/server-scheduled-task-routes.d.ts +4 -0
  77. package/dist/server-scheduled-task-routes.js +34 -0
  78. package/dist/server-session-routes.d.ts +5 -13
  79. package/dist/server-session-routes.js +35 -70
  80. package/dist/server-task-routes.d.ts +6 -21
  81. package/dist/server-task-routes.js +25 -120
  82. package/dist/server-team-dispatch-routes.js +12 -2
  83. package/dist/server-workspace-routes.d.ts +1 -1
  84. package/dist/server-workspace-routes.js +33 -78
  85. package/dist/server.js +55 -20
  86. package/dist/session-model.d.ts +15 -0
  87. package/dist/session-model.js +23 -0
  88. package/dist/session-registry.d.ts +5 -1
  89. package/dist/session-registry.js +20 -11
  90. package/dist/session-request.d.ts +9 -0
  91. package/dist/session-request.js +27 -0
  92. package/dist/session-topic.d.ts +1 -1
  93. package/dist/session-topic.js +1 -1
  94. package/dist/session-transport.d.ts +3 -1
  95. package/dist/session-transport.js +11 -5
  96. package/dist/silicon-employee-draft.d.ts +2 -2
  97. package/dist/silicon-employee-draft.js +3 -3
  98. package/dist/socket-probe.d.ts +2 -0
  99. package/dist/socket-probe.js +20 -0
  100. package/dist/storage-schedules.d.ts +20 -0
  101. package/dist/storage-schedules.js +78 -0
  102. package/dist/storage-schema.d.ts +3 -0
  103. package/dist/storage-schema.js +702 -0
  104. package/dist/storage.d.ts +42 -8
  105. package/dist/storage.js +182 -715
  106. package/dist/structured-claude-adapter.js +12 -0
  107. package/dist/structured-claude-protocol.d.ts +4 -0
  108. package/dist/structured-claude-protocol.js +18 -4
  109. package/dist/structured-codex-adapter.js +11 -0
  110. package/dist/structured-exec-pump.js +79 -38
  111. package/dist/structured-failure.d.ts +21 -0
  112. package/dist/structured-failure.js +14 -0
  113. package/dist/structured-gemini-adapter.js +12 -2
  114. package/dist/structured-grok-adapter.js +12 -2
  115. package/dist/structured-opencode-adapter.js +8 -1
  116. package/dist/structured-qoder-adapter.js +14 -0
  117. package/dist/structured-session-manager.d.ts +13 -18
  118. package/dist/structured-session-manager.js +147 -517
  119. package/dist/system-employee.d.ts +1 -1
  120. package/dist/system-employee.js +1 -1
  121. package/dist/task-archive-policy.js +1 -1
  122. package/dist/task-read-model.d.ts +27 -0
  123. package/dist/task-read-model.js +47 -0
  124. package/dist/task-retention.d.ts +1 -1
  125. package/dist/task-retention.js +1 -1
  126. package/dist/task-title-service.d.ts +30 -0
  127. package/dist/task-title-service.js +122 -0
  128. package/dist/task-title.d.ts +1 -1
  129. package/dist/task-title.js +1 -1
  130. package/dist/terminal-daemon-client.js +1 -18
  131. package/dist/user-memory.js +1 -1
  132. package/dist/web-ui/asset-runtime.d.ts +12 -0
  133. package/dist/web-ui/asset-runtime.js +6 -0
  134. package/dist/web-ui/composer-avatar-activity.d.ts +6 -0
  135. package/dist/web-ui/composer-avatar-activity.js +29 -0
  136. package/dist/web-ui/content/ai-teams.js +13 -13
  137. package/dist/web-ui/content/module-manifest.json +1 -0
  138. package/dist/web-ui/content/modules/5478be410b112154/chat-render-PY6DIXNZ.js +24 -0
  139. package/dist/web-ui/content/modules/5478be410b112154/chunk-2YZLCGYN.js +1 -0
  140. package/dist/web-ui/content/modules/5478be410b112154/chunk-6CFHW3FA.js +111 -0
  141. package/dist/web-ui/content/modules/5478be410b112154/chunk-6XQEBIEX.js +1 -0
  142. package/dist/web-ui/content/modules/5478be410b112154/chunk-DDSO7OX7.js +1 -0
  143. package/dist/web-ui/content/modules/5478be410b112154/chunk-G3CXNMA2.js +20 -0
  144. package/dist/web-ui/content/modules/5478be410b112154/chunk-LGVAPHRS.js +36 -0
  145. package/dist/web-ui/content/modules/5478be410b112154/chunk-NURCNMRU.js +1 -0
  146. package/dist/web-ui/content/modules/5478be410b112154/chunk-PCMFGZKX.js +1 -0
  147. package/dist/web-ui/content/modules/5478be410b112154/chunk-QRNA4D7E.js +1 -0
  148. package/dist/web-ui/content/modules/5478be410b112154/chunk-TMRPHNJO.js +1 -0
  149. package/dist/web-ui/content/modules/5478be410b112154/chunk-WC7RK7LP.js +1 -0
  150. package/dist/web-ui/content/modules/5478be410b112154/chunk-YOXXNXOX.js +2 -0
  151. package/dist/web-ui/content/modules/5478be410b112154/chunk-YX475LXO.js +1 -0
  152. package/dist/web-ui/content/modules/5478be410b112154/employee-card-MDWKU4V3.js +1 -0
  153. package/dist/web-ui/content/modules/5478be410b112154/host-56WPSON2.js +1 -0
  154. package/dist/web-ui/content/modules/5478be410b112154/host-5QOHKHNC.js +1 -0
  155. package/dist/web-ui/content/modules/5478be410b112154/host-AWIJ5L2K.js +1 -0
  156. package/dist/web-ui/content/modules/5478be410b112154/host-BOZLGIT5.js +69 -0
  157. package/dist/web-ui/content/modules/5478be410b112154/host-EZV22Q7Y.js +4 -0
  158. package/dist/web-ui/content/modules/5478be410b112154/host-IW43Z5ZF.js +1 -0
  159. package/dist/web-ui/content/modules/5478be410b112154/host-JT7NIUQL.js +1 -0
  160. package/dist/web-ui/content/modules/5478be410b112154/host-JWEIDB55.js +3 -0
  161. package/dist/web-ui/content/modules/5478be410b112154/host-OU5TRYH6.js +2 -0
  162. package/dist/web-ui/content/modules/5478be410b112154/host-PRSTCPVE.js +5 -0
  163. package/dist/web-ui/content/modules/5478be410b112154/host-TWPJI3HE.js +1 -0
  164. package/dist/web-ui/content/modules/5478be410b112154/host-WZGJSNML.js +1 -0
  165. package/dist/web-ui/content/modules/5478be410b112154/host-X6KQVFD6.js +1 -0
  166. package/dist/web-ui/content/modules/5478be410b112154/host-YKCQFMQR.js +1 -0
  167. package/dist/web-ui/content/modules/5478be410b112154/host-bridge-2LZX6UUZ.js +1 -0
  168. package/dist/web-ui/content/modules/5478be410b112154/library-bridge-MQFJGTGY.js +4 -0
  169. package/dist/web-ui/content/modules/5478be410b112154/page-T3VUCU2W.js +3 -0
  170. package/dist/web-ui/content/modules/5478be410b112154/panel-HJBGBBPY.js +1 -0
  171. package/dist/web-ui/content/modules/5478be410b112154/runtime.js +920 -0
  172. package/dist/web-ui/content/modules/5478be410b112154/sha256-ANXV72UC.js +1 -0
  173. package/dist/web-ui/content/modules/5478be410b112154/task-board-host-AVMDIHIV.js +2 -0
  174. package/dist/web-ui/content/modules/5478be410b112154/workspace-agent-picker-KI7QVSK3.js +1 -0
  175. package/dist/web-ui/content/modules/5478be410b112154/workspace-window-L542FGP2.js +1 -0
  176. package/dist/web-ui/content/plush-avatar.js +177 -177
  177. package/dist/web-ui/content/scripts.js +1 -994
  178. package/dist/web-ui/content/styles.css +1 -1
  179. package/dist/web-ui/content/tailwind.css +1 -1
  180. package/dist/web-ui/embedded-assets.d.ts +32 -0
  181. package/dist/web-ui/embedded-assets.js +10 -8
  182. package/dist/web-ui/markdown.d.ts +3 -2
  183. package/dist/web-ui/markdown.js +61 -48
  184. package/dist/web-ui/message-file-target.d.ts +13 -0
  185. package/dist/web-ui/message-file-target.js +85 -0
  186. package/dist/web-ui/provider-identity.d.ts +1 -1
  187. package/dist/web-ui/provider-identity.js +3 -4
  188. package/dist/web-ui/react/local-preview/controller.d.ts +2 -0
  189. package/dist/web-ui/react/local-preview/controller.js +12 -0
  190. package/dist/web-ui/scripts.d.ts +10 -0
  191. package/dist/web-ui/scripts.js +42 -5
  192. package/package.json +6 -3
  193. package/browser-extension/icons/wand-128.png +0 -0
  194. package/browser-extension/icons/wand-16.png +0 -0
  195. package/browser-extension/icons/wand-32.png +0 -0
  196. package/browser-extension/icons/wand-48.png +0 -0
  197. package/dist/web-ui/content/licenses/codex-LICENSE.txt +0 -21
@@ -1,4 +1,4 @@
1
- import { defaultModelGroupSelector, isAutoAssignSelector, resolveModelGroupModels } from "./model-groups.js";
1
+ import { isAutoAssignSelector, resolveModelGroupModels } from "./model-groups.js";
2
2
  import { resolveAutoAssign } from "./model-auto-assign.js";
3
3
  import { withModelGroups } from "./model-group-runner.js";
4
4
  import { classifyStructuredFailure, hasStructuredExecutionProgress } from "./structured-failure.js";
@@ -9,7 +9,8 @@ import { truncateMessagesForTransport } from "./message-truncator.js";
9
9
  import { stampNewToolUseTimes } from "./tool-use-timestamps.js";
10
10
  import { buildChildEnv } from "./env-utils.js";
11
11
  import { getErrorMessage } from "./error-utils.js";
12
- import { getDefaultModelForProvider } from "./config.js";
12
+ import { compactTransportAvatar } from "./fixed-employee-avatar.js";
13
+ import { fallbackProviderModel } from "./session-model.js";
13
14
  import { isLocalDecisionModel } from "./decision-expert-identity.js";
14
15
  import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
15
16
  import { recordIterationPrompt } from "./iteration-log.js";
@@ -22,6 +23,7 @@ import { withDecisionAccess } from "./decision-runner.js";
22
23
  import { signalNameFromNumber } from "./signal-utils.js";
23
24
  import { provisionalSessionTopic, SessionNativeTitleTracker, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
24
25
  import { resolveSessionCwd } from "./session-cwd.js";
26
+ import { isScheduledConversationSession } from "./conversation-session-preview.js";
25
27
  import { isSessionProvider, providerCliCommand } from "./session-provider.js";
26
28
  import { resolveSessionProvider, resolveSystemAiContext } from "./session-ai-context.js";
27
29
  import { readNativeSessionTitle } from "./native-session-title.js";
@@ -36,10 +38,9 @@ import { QoderRunner } from "./structured-qoder-adapter.js";
36
38
  import { PiRunner, applyPiEvent, isMissingPiSession } from "./structured-pi-adapter.js";
37
39
  import { GeminiRunner, applyGeminiEvent, isMissingGeminiSession } from "./structured-gemini-adapter.js";
38
40
  import { structuredRunId, } from "./structured-exec-host.js";
39
- import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, } from "./structured-provider-common.js";
41
+ import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, normalizeTurnAnchors, resolveStructuredRunner, } from "./structured-provider-common.js";
40
42
  import { TurnQuietHeartbeat } from "./turn-heartbeat.js";
41
43
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
42
- import { RETENTION_IDLE_MS } from "./retention.js";
43
44
  class PersistedStructuredRunnerError extends Error {
44
45
  constructor(message) {
45
46
  super(message);
@@ -167,6 +168,12 @@ function recoveredCommandLabel(runner) {
167
168
  }
168
169
  }
169
170
  const STREAM_EMIT_DEBOUNCE_MS = 16;
171
+ /**
172
+ * 员工候选链的启动看门狗:CLI 进程起来后这么久没有任何协议事件,就认定这一候选本轮没有开始
173
+ * 执行(无输出、无用量、无工具调用),按未接受输入处理并沿候选链降级,而不是让会话无限停在
174
+ * 「运行中」。取 3 分钟:慢模型的首个事件与真正卡死之间留出足够余量。
175
+ */
176
+ const EMPLOYEE_CANDIDATE_STARTUP_TIMEOUT_MS = 180_000;
170
177
  /** Min interval between full saveSession() calls for an in-progress streaming turn.
171
178
  * saveSession serializes the entire messages array, so doing it on every NDJSON
172
179
  * event is N². close-path always calls saveSession unconditionally to take the
@@ -177,7 +184,6 @@ const STREAM_EMIT_DEBOUNCE_MS = 16;
177
184
  // second on the event loop.
178
185
  const STREAM_SAVE_THROTTLE_MS = 1_000;
179
186
  const DETACHED_RECOVERY_RETRY_MS = 500;
180
- const ARCHIVE_AFTER_MS = RETENTION_IDLE_MS;
181
187
  /**
182
188
  * 找出最后一条 assistant turn 中尚未配对 tool_result 的 AskUserQuestion tool_use。
183
189
  * 用来识别"刚被 SIGTERM 中断、正在等用户提交答案"的状态。
@@ -233,6 +239,26 @@ function withSummary(snapshot) {
233
239
  function shouldAutoApproveForMode(mode) {
234
240
  return mode === "full-access" || mode === "managed" || mode === "auto-edit";
235
241
  }
242
+ /** Quiet heartbeat is a status ping, not another copy of the transcript or uploaded avatar. */
243
+ export function structuredQuietStatusData(session) {
244
+ return {
245
+ sessionKind: "structured",
246
+ status: session.status,
247
+ exitCode: session.exitCode,
248
+ structuredState: normalizeTurnAnchors(session.structuredState),
249
+ provider: session.provider,
250
+ runner: session.runner,
251
+ selectedModel: session.selectedModel,
252
+ thinkingEffort: session.thinkingEffort,
253
+ employeeId: session.employeeId,
254
+ employeeName: session.employeeName,
255
+ employeeAvatar: compactTransportAvatar(session.employeeAvatar),
256
+ employeeCandidateIndex: session.employeeCandidateIndex,
257
+ title: session.title,
258
+ description: session.description,
259
+ summary: session.description ?? session.summary,
260
+ };
261
+ }
236
262
  function buildStructuredOutputPayload(snapshot) {
237
263
  return {
238
264
  wandProtocolVersion: WAND_PROTOCOL_VERSION,
@@ -247,7 +273,7 @@ function buildStructuredOutputPayload(snapshot) {
247
273
  thinkingEffort: snapshot.thinkingEffort,
248
274
  employeeId: snapshot.employeeId,
249
275
  employeeName: snapshot.employeeName,
250
- employeeAvatar: snapshot.employeeAvatar,
276
+ employeeAvatar: compactTransportAvatar(snapshot.employeeAvatar),
251
277
  employeeCandidateIndex: snapshot.employeeCandidateIndex,
252
278
  title: snapshot.title,
253
279
  description: snapshot.description,
@@ -467,7 +493,6 @@ export class StructuredSessionManager {
467
493
  seenIdempotencyKeys = new Map();
468
494
  relayHandlers = new Map();
469
495
  emitEvent = null;
470
- archiveTimer = null;
471
496
  /**
472
497
  * 静默期心跳:回合在飞且连续 TURN_QUIET_HEARTBEAT_MS 没有任何事件时,用现有的
473
498
  * status 事件重发一次权威快照(含最新 turn 锚点)。timer 只在 inFlight 期间存在。
@@ -476,6 +501,8 @@ export class StructuredSessionManager {
476
501
  topicCoordinator = new SessionTopicCoordinator();
477
502
  nativeTitles = new SessionNativeTitleTracker();
478
503
  streamEmitTimers = new Set();
504
+ /** 员工候选链的「本轮还没开始执行」看门狗,按会话一个;首个协议事件或回合收敛即撤销。 */
505
+ candidateStartupWatchdogs = new Map();
479
506
  claudeCliRunner;
480
507
  codexRunner;
481
508
  openCodeRunner;
@@ -578,30 +605,6 @@ export class StructuredSessionManager {
578
605
  if (!recoverableDetachedRun)
579
606
  this.storage.saveSession(restored);
580
607
  }
581
- this.archiveExpiredSessions();
582
- this.archiveTimer = setInterval(() => {
583
- try {
584
- this.archiveExpiredSessions();
585
- }
586
- catch (err) {
587
- console.error(`[StructuredSessionManager] archive scan failed: ${String(err)}`);
588
- }
589
- }, 60 * 1000);
590
- this.archiveTimer.unref?.();
591
- }
592
- archiveExpiredSessions() {
593
- const now = Date.now();
594
- for (const session of this.sessions.values()) {
595
- if (session.archived || session.status === "running")
596
- continue;
597
- const referenceTime = session.endedAt ?? session.startedAt;
598
- const endedAtMs = Date.parse(referenceTime);
599
- if (!Number.isFinite(endedAtMs) || now - endedAtMs < ARCHIVE_AFTER_MS)
600
- continue;
601
- session.archived = true;
602
- session.archivedAt = new Date(now).toISOString();
603
- this.storage.updateSessionRuntimeMetadata(session);
604
- }
605
608
  }
606
609
  setEventEmitter(emitEvent) {
607
610
  if (this.disposed)
@@ -653,10 +656,6 @@ export class StructuredSessionManager {
653
656
  this.unacceptedTeamStarts.clear();
654
657
  // 静默心跳不跨服务关闭存活(会话级 timer 不允许泄漏)。
655
658
  this.turnHeartbeat.dispose();
656
- if (this.archiveTimer) {
657
- clearInterval(this.archiveTimer);
658
- this.archiveTimer = null;
659
- }
660
659
  if (this.detachedRecoveryRetryTimer) {
661
660
  clearTimeout(this.detachedRecoveryRetryTimer);
662
661
  this.detachedRecoveryRetryTimer = null;
@@ -714,6 +713,9 @@ export class StructuredSessionManager {
714
713
  for (const timer of this.streamCheckpointTimers.values())
715
714
  clearTimeout(timer);
716
715
  this.streamCheckpointTimers.clear();
716
+ for (const timer of this.candidateStartupWatchdogs.values())
717
+ clearTimeout(timer);
718
+ this.candidateStartupWatchdogs.clear();
717
719
  this.streamCheckpointDirty.clear();
718
720
  this.lastStreamSaveAt.clear();
719
721
  this.topicCoordinator.clear();
@@ -1229,6 +1231,13 @@ export class StructuredSessionManager {
1229
1231
  const s = this.sessions.get(id);
1230
1232
  return s ? withSummary(s) : null;
1231
1233
  }
1234
+ getSlim(id) {
1235
+ const session = this.get(id);
1236
+ if (!session)
1237
+ return null;
1238
+ const { messages: _messages, output: _output, ...metadata } = session;
1239
+ return { ...metadata, output: "" };
1240
+ }
1232
1241
  setSessionWorkspace(id, membership) {
1233
1242
  const updated = { ...this.requireSession(id), ...membership };
1234
1243
  this.sessions.set(id, updated);
@@ -1296,6 +1305,10 @@ export class StructuredSessionManager {
1296
1305
  return;
1297
1306
  // 迭代提示词记录:和会话标题共用同一套「有没有信息量」判断,用户不用多做一步。
1298
1307
  recordIterationPrompt(this.storage, session, input, "session");
1308
+ // The plan names this execution. Preserve that name and avoid a second model
1309
+ // call merely to rename background work before it reaches its chosen employee.
1310
+ if (isScheduledConversationSession(session))
1311
+ return;
1299
1312
  const blockedTitles = sessionTopicBlocklistForSnapshot(session, this.storage);
1300
1313
  // CLI 自己起了名字就归它,模型标题与输入首行都不再覆盖;取不到原生标题才走系统硅基员工。
1301
1314
  const hasNativeTitle = this.nativeTitles.has(id);
@@ -1490,43 +1503,50 @@ export class StructuredSessionManager {
1490
1503
  && !session.structuredState?.activeRequestId && !session.structuredState?.inFlight ? fact.failure : null;
1491
1504
  }
1492
1505
  employeeCandidateModel(agent) {
1493
- return agent.model === "default"
1494
- ? defaultModelGroupSelector(this.config.modelGroups, agent.provider, getDefaultModelForProvider(this.config, agent.provider))
1495
- || getDefaultModelForProvider(this.config, agent.provider) || null : agent.model;
1506
+ return agent.model === "default" ? fallbackProviderModel(this.config, agent.provider) ?? null : agent.model;
1496
1507
  }
1497
- /** Only an unstarted employee conversation may replay its explicitly rejected first input. */
1508
+ /**
1509
+ * A turn the provider rejected before it accepted any input may replay on the next candidate.
1510
+ * Same-tool switches carry the conversation as it stood before this turn — history plus the native
1511
+ * session it already had — so they also cover an already-started chat. A cross-tool switch cannot
1512
+ * hand that context over, so it stays limited to conversations that have not produced a turn yet.
1513
+ */
1498
1514
  retryEmployeeCandidate(id, original, prompt, error) {
1499
1515
  // Team runs own candidate scheduling; keep the snapshot without a second retry loop.
1500
1516
  if (original.automationId?.startsWith("ai-team:") || !(error instanceof UnacceptedStructuredInputError))
1501
1517
  return null;
1518
+ if (!original.employeeId || !error.failure.retryable)
1519
+ return null;
1502
1520
  const candidates = original.employeeCandidates ?? [];
1503
1521
  const index = original.employeeCandidateIndex ?? 0;
1522
+ const selected = candidates[index];
1504
1523
  let nextIndex = index + 1;
1505
1524
  // LAYA has no chat adapter; keep original candidate indices while skipping bounded-only models.
1506
1525
  while (nextIndex < candidates.length && isLocalDecisionModel(candidates[nextIndex].model))
1507
1526
  nextIndex += 1;
1508
- if (!original.employeeId || (original.messages?.length ?? 0) !== 0 || original.claudeSessionId
1509
- || !error.failure.retryable || nextIndex >= candidates.length)
1527
+ if (nextIndex >= candidates.length)
1510
1528
  return null;
1511
- const selected = candidates[index];
1512
1529
  // Explicit tool/model changes, including changes during the pending request, remain authoritative.
1513
1530
  if (!selected || original.provider !== selected.provider
1514
1531
  || (original.structuredState?.engine && (original.structuredState.engine === "core" ? "sdk" : "cli") !== (selected.engine ?? "cli"))
1515
1532
  || (original.selectedModel && original.selectedModel !== this.employeeCandidateModel(selected)))
1516
1533
  return null;
1534
+ const next = candidates[nextIndex];
1535
+ const sameTool = next.provider === selected.provider && (next.engine ?? "cli") === (selected.engine ?? "cli");
1536
+ if (!sameTool && ((original.messages?.length ?? 0) !== 0 || original.claudeSessionId))
1537
+ return null;
1517
1538
  const current = this.sessions.get(id);
1518
1539
  if (!current || current.status !== "running" || current.provider !== original.provider
1519
1540
  || current.selectedModel !== original.selectedModel || current.mode !== original.mode
1520
1541
  || current.piSettings !== original.piSettings)
1521
1542
  return null;
1522
- const next = candidates[nextIndex];
1523
1543
  const runner = resolveStructuredRunner(next.provider, undefined);
1524
1544
  const model = this.employeeCandidateModel(next);
1525
- const sameTool = next.provider === original.provider && (next.engine ?? "cli") === (selected.engine ?? "cli");
1526
1545
  const piSettings = next.provider !== "pi" ? undefined : sameTool ? current.piSettings
1527
1546
  : next.engine === "sdk" ? defaultPiSessionSettings(this.config.harness?.compaction?.enabled ?? true)
1528
1547
  : { ...(this.storage.getPiSessionDefaults() ?? defaultPiCliSessionSettings()),
1529
1548
  autoCompaction: this.config.harness?.compaction?.enabled ?? true };
1549
+ const carried = current.structuredState;
1530
1550
  const retry = {
1531
1551
  ...current,
1532
1552
  provider: next.provider,
@@ -1538,21 +1558,25 @@ export class StructuredSessionManager {
1538
1558
  thinkingEffort: next.thinkingEffort,
1539
1559
  employeeCandidateIndex: nextIndex,
1540
1560
  piSettings,
1541
- claudeSessionId: null,
1542
- harnessContext: undefined,
1543
- harnessExtensionState: undefined,
1544
- messages: [],
1545
- output: "",
1561
+ // Replay resumes the state this turn started from, not what the rejected attempt produced:
1562
+ // its native session may already hold the input we are about to send again.
1563
+ claudeSessionId: sameTool ? original.claudeSessionId : null,
1564
+ harnessContext: sameTool ? original.harnessContext : undefined,
1565
+ harnessExtensionState: sameTool ? original.harnessExtensionState : undefined,
1566
+ messages: sameTool ? original.messages ?? [] : [],
1567
+ output: sameTool ? original.output ?? "" : "",
1546
1568
  status: "idle",
1547
1569
  exitCode: null,
1548
1570
  endedAt: null,
1549
1571
  structuredState: {
1550
1572
  ...defaultStructuredState(next.provider, runner),
1573
+ ...(sameTool ? { engine: carried?.engine, engineReason: carried?.engineReason,
1574
+ contextUsage: carried?.contextUsage, compactions: carried?.compactions } : {}),
1551
1575
  model: model ?? undefined,
1552
1576
  },
1553
1577
  };
1554
1578
  this.logger?.appendStructuredSpawn(id, { kind: "candidate-fallback", failureKind: error.failure.kind,
1555
- fromProvider: original.provider, toProvider: next.provider, candidateIndex: nextIndex });
1579
+ fromProvider: original.provider, toProvider: next.provider, candidateIndex: nextIndex, sameTool });
1556
1580
  this.sessions.set(id, retry);
1557
1581
  this.saveAuthoritativeSession(retry);
1558
1582
  // sendMessage publishes the new running snapshot synchronously; do not expose a transient idle slot.
@@ -1803,16 +1827,24 @@ export class StructuredSessionManager {
1803
1827
  throw new Error(`会话 runner ${String(runner)} 与 provider ${provider} 不匹配。`);
1804
1828
  }
1805
1829
  if (provider === "codex") {
1806
- await this.runCodexStreaming(id, updated, prompt, requestId);
1830
+ await this.runStructuredStreaming(id, updated, prompt, requestId, {
1831
+ runner: this.codexRunner, provider: "codex", commandLabel: "codex exec", logKind: "codex-exec",
1832
+ });
1807
1833
  }
1808
1834
  else if (provider === "opencode") {
1809
- await this.runOpenCodeStreaming(id, updated, prompt, requestId);
1835
+ await this.runStructuredStreaming(id, updated, prompt, requestId, {
1836
+ runner: this.openCodeRunner, provider: "opencode", commandLabel: "opencode run", logKind: "opencode-run",
1837
+ installHint: "请安装 opencode-ai,或重跑 `wand service:install` 刷新服务 PATH",
1838
+ });
1810
1839
  }
1811
1840
  else if (provider === "grok") {
1812
- await this.runGrokStreaming(id, updated, prompt, requestId);
1841
+ await this.runStructuredStreaming(id, updated, prompt, requestId, {
1842
+ runner: this.grokRunner, provider: "grok", commandLabel: "grok", logKind: "grok-headless",
1843
+ installHint: "请安装 Grok Build CLI,或重跑 `wand service:install` 刷新服务 PATH",
1844
+ });
1813
1845
  }
1814
1846
  else if (provider === "qoder") {
1815
- await this.runClaudeStreaming(id, updated, prompt, requestId, {
1847
+ await this.runStructuredStreaming(id, updated, prompt, requestId, {
1816
1848
  runner: this.qoderRunner,
1817
1849
  provider: "qoder",
1818
1850
  commandLabel: "qodercli -p",
@@ -1841,7 +1873,7 @@ export class StructuredSessionManager {
1841
1873
  };
1842
1874
  this.sessions.set(id, enginePatched);
1843
1875
  if (resolution.engine === "core") {
1844
- await this.runClaudeStreaming(id, enginePatched, prompt, requestId, {
1876
+ await this.runStructuredStreaming(id, enginePatched, prompt, requestId, {
1845
1877
  runner: this.coreRunner,
1846
1878
  provider: "pi",
1847
1879
  commandLabel: "Wand Agent(SDK)",
@@ -1849,7 +1881,7 @@ export class StructuredSessionManager {
1849
1881
  });
1850
1882
  }
1851
1883
  else {
1852
- await this.runClaudeStreaming(id, enginePatched, prompt, requestId, {
1884
+ await this.runStructuredStreaming(id, enginePatched, prompt, requestId, {
1853
1885
  runner: this.piRunner,
1854
1886
  provider: "pi",
1855
1887
  commandLabel: "pi --mode json --print",
@@ -1859,7 +1891,7 @@ export class StructuredSessionManager {
1859
1891
  }
1860
1892
  }
1861
1893
  else if (provider === "gemini") {
1862
- await this.runClaudeStreaming(id, updated, prompt, requestId, {
1894
+ await this.runStructuredStreaming(id, updated, prompt, requestId, {
1863
1895
  runner: this.geminiRunner,
1864
1896
  provider: "gemini",
1865
1897
  commandLabel: "gemini -p --output-format stream-json",
@@ -1868,7 +1900,7 @@ export class StructuredSessionManager {
1868
1900
  });
1869
1901
  }
1870
1902
  else {
1871
- await this.runClaudeStreaming(id, updated, cliClaudePrompt, requestId);
1903
+ await this.runStructuredStreaming(id, updated, cliClaudePrompt, requestId);
1872
1904
  }
1873
1905
  const finished = this.requireSession(id);
1874
1906
  return finished;
@@ -2143,7 +2175,7 @@ export class StructuredSessionManager {
2143
2175
  const candidate = candidateIndex >= 0 ? candidates[candidateIndex] : undefined;
2144
2176
  const runner = defaultStructuredRunner(provider);
2145
2177
  const model = candidate && candidate.model !== "default" ? candidate.model
2146
- : defaultModelGroupSelector(this.config.modelGroups, provider, getDefaultModelForProvider(this.config, provider)) || getDefaultModelForProvider(this.config, provider) || null;
2178
+ : fallbackProviderModel(this.config, provider) ?? null;
2147
2179
  const effort = normalizeThinkingEffort(candidate?.thinkingEffort ?? this.config.defaultThinkingEffort);
2148
2180
  const thinkingEffort = effort?.includes(":") && !effort.startsWith(`${provider}:`) ? "off" : effort;
2149
2181
  const requestedMode = candidate?.mode ?? session.mode;
@@ -2177,7 +2209,8 @@ export class StructuredSessionManager {
2177
2209
  type: "status",
2178
2210
  sessionId: updated.id,
2179
2211
  data: { ...buildStructuredOutputPayload(updated), command: updated.command,
2180
- mode: updated.mode, autoApprovePermissions: updated.autoApprovePermissions },
2212
+ mode: updated.mode, autoApprovePermissions: updated.autoApprovePermissions,
2213
+ employeeAvatar: updated.employeeAvatar },
2181
2214
  });
2182
2215
  return updated;
2183
2216
  }
@@ -2553,9 +2586,7 @@ export class StructuredSessionManager {
2553
2586
  type: "status",
2554
2587
  sessionId,
2555
2588
  data: {
2556
- ...buildStructuredOutputPayload(session),
2557
- status: session.status,
2558
- exitCode: session.exitCode,
2589
+ ...structuredQuietStatusData(session),
2559
2590
  },
2560
2591
  });
2561
2592
  return true;
@@ -2650,455 +2681,8 @@ export class StructuredSessionManager {
2650
2681
  stats.total++;
2651
2682
  session.approvalStats = stats;
2652
2683
  }
2653
- // ---------------------------------------------------------------------------
2654
- // Streaming codex exec --json execution
2655
- // ---------------------------------------------------------------------------
2656
- async runCodexStreaming(sessionId, session, prompt, requestId) {
2657
- let emitTimer = null;
2658
- const syncSnapshot = (turnState) => {
2659
- const current = this.currentSessionForRequest(sessionId, requestId);
2660
- if (!current)
2661
- return;
2662
- const turn = {
2663
- role: "assistant",
2664
- content: this.compactContentBlocks([...turnState.blocks], turnState.result),
2665
- usage: turnState.usage,
2666
- };
2667
- const messages = upsertAssistantMessage(current.messages, turn);
2668
- const patched = {
2669
- ...current,
2670
- claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
2671
- messages,
2672
- output: turnState.result || current.output,
2673
- structuredState: this.noteTurnActivity(sessionId, {
2674
- ...current.structuredState,
2675
- model: turnState.model ?? current.structuredState?.model,
2676
- }),
2677
- };
2678
- this.sessions.set(sessionId, patched);
2679
- this.saveStreamingSnapshot(patched);
2680
- };
2681
- const flushEmit = () => {
2682
- if (emitTimer)
2683
- this.clearStreamEmitTimer(emitTimer);
2684
- emitTimer = null;
2685
- const current = this.currentSessionForRequest(sessionId, requestId);
2686
- if (current) {
2687
- this.emit({
2688
- type: "output",
2689
- sessionId,
2690
- data: buildIncrementalStructuredPayload(current, this.config.cardDefaults ?? {}),
2691
- });
2692
- }
2693
- };
2694
- const scheduleEmit = () => {
2695
- if (!emitTimer) {
2696
- emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
2697
- }
2698
- };
2699
- const execution = startEmployeeKnowledgeRunner(this.storage, this.codexRunner, {
2700
- session,
2701
- prompt,
2702
- env: buildChildEnv(this.config.inheritEnv !== false),
2703
- }, {
2704
- isActive: () => this.isCurrentRequest(sessionId, requestId),
2705
- onStdout: (text) => this.logger?.appendStructuredStdout(sessionId, text),
2706
- onStderr: (text) => this.logger?.appendStructuredStderr(sessionId, text),
2707
- onEvent: (event) => this.logger?.appendStreamEvent(sessionId, event),
2708
- onUpdate: (turnState) => {
2709
- syncSnapshot(turnState);
2710
- scheduleEmit();
2711
- },
2712
- });
2713
- this.pendingRunnerExecutions.set(sessionId, execution);
2714
- this.logger?.appendStructuredSpawn(sessionId, {
2715
- kind: "codex-exec",
2716
- provider: "codex",
2717
- pid: execution.pid,
2718
- cwd: session.cwd,
2719
- args: execution.args,
2720
- prompt: prompt.slice(0, 2048),
2721
- promptLength: prompt.length,
2722
- threadId: session.claudeSessionId,
2723
- spawnedAt: execution.spawnedAt,
2724
- });
2725
- let result;
2726
- try {
2727
- result = await execution.completion;
2728
- }
2729
- finally {
2730
- const released = this.releasePendingRunnerExecution(sessionId, execution);
2731
- if (released)
2732
- this.cancelStreamingCheckpointTimer(sessionId);
2733
- }
2734
- if (!this.isCurrentRequest(sessionId, requestId)) {
2735
- if (emitTimer)
2736
- this.clearStreamEmitTimer(emitTimer);
2737
- return;
2738
- }
2739
- flushEmit();
2740
- this.throwUnacceptedInput(sessionId, result, "codex exec", "(PATH 中找不到 codex 可执行文件;请确认 codex 已安装,或重跑 `wand service:install` 刷新服务的 PATH)");
2741
- this.logger?.appendStructuredSpawn(sessionId, {
2742
- kind: "codex-exec-close",
2743
- pid: execution.pid,
2744
- spawnedAt: execution.spawnedAt,
2745
- closedAt: new Date().toISOString(),
2746
- exitCode: result.exitCode,
2747
- stderrTail: result.stderr.slice(-2048),
2748
- codexErrors: result.errors,
2749
- codexTurnFailed: result.primaryError,
2750
- });
2751
- const current = this.currentSessionForRequest(sessionId, requestId);
2752
- if (!current)
2753
- return;
2754
- const interruptedByUser = this.interruptedWith.has(sessionId);
2755
- const interruptPrompt = this.interruptedWith.get(sessionId);
2756
- if ((result.primaryError || (result.exitCode !== 0 && result.exitCode !== null) || result.signal) && !interruptedByUser) {
2757
- const errorText = this.formatStructuredExitError("codex exec", result.exitCode, result.signal, { stderr: result.stderr, primary: result.primaryError, extras: result.errors });
2758
- const failed = this.finishStructuredFailure(current, typeof result.exitCode === "number" ? result.exitCode : 1, errorText, result.state);
2759
- this.sessions.set(sessionId, failed);
2760
- this.saveAuthoritativeSession(failed);
2761
- this.emitStructuredSnapshot(failed);
2762
- this.emitStructuredSnapshot(failed, "ended");
2763
- throw new PersistedStructuredRunnerError(errorText);
2764
- }
2765
- const messages = this.buildCompletedAssistantMessages(current, result.state);
2766
- const keepRunning = !!interruptPrompt;
2767
- const finished = {
2768
- ...current,
2769
- status: keepRunning ? "running" : "idle",
2770
- exitCode: keepRunning ? null : 0,
2771
- endedAt: keepRunning ? null : new Date().toISOString(),
2772
- output: result.state.result,
2773
- claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
2774
- messages,
2775
- queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
2776
- pendingEscalation: null,
2777
- permissionBlocked: false,
2778
- structuredState: {
2779
- ...this.settleTurn(sessionId, current.structuredState),
2780
- model: result.state.model ?? current.structuredState?.model,
2781
- inFlight: false,
2782
- activeRequestId: null,
2783
- lastError: null,
2784
- },
2785
- };
2786
- this.sessions.set(sessionId, finished);
2787
- this.saveAuthoritativeSession(finished);
2788
- this.emitStructuredSnapshot(finished);
2789
- if (!keepRunning)
2790
- this.emitStructuredSnapshot(finished, "ended");
2791
- if (interruptPrompt) {
2792
- this.interruptedWith.delete(sessionId);
2793
- this.preserveQueueOnInterrupt.delete(sessionId);
2794
- setImmediate(() => {
2795
- this.sendMessage(sessionId, interruptPrompt).catch((error) => {
2796
- console.error("[WAND] codex interrupt-and-send failed:", error);
2797
- });
2798
- });
2799
- return;
2800
- }
2801
- setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
2802
- }
2803
- async runGrokStreaming(sessionId, session, prompt, requestId) {
2804
- let emitTimer = null;
2805
- const syncSnapshot = (turnState) => {
2806
- const current = this.currentSessionForRequest(sessionId, requestId);
2807
- if (!current)
2808
- return;
2809
- const turn = {
2810
- role: "assistant",
2811
- content: this.compactContentBlocks([...turnState.blocks], turnState.result),
2812
- usage: turnState.usage,
2813
- };
2814
- const messages = upsertAssistantMessage(current.messages, turn);
2815
- const patched = {
2816
- ...current,
2817
- claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
2818
- messages,
2819
- output: turnState.result || current.output,
2820
- // 每个 provider 帧都是本轮还在活动的证据:刷新 lastActivityAt 并重挂静默窗口。
2821
- structuredState: this.noteTurnActivity(sessionId, current.structuredState),
2822
- };
2823
- this.sessions.set(sessionId, patched);
2824
- this.saveStreamingSnapshot(patched);
2825
- };
2826
- const flushEmit = () => {
2827
- if (emitTimer)
2828
- this.clearStreamEmitTimer(emitTimer);
2829
- emitTimer = null;
2830
- const current = this.currentSessionForRequest(sessionId, requestId);
2831
- if (current)
2832
- this.emit({
2833
- type: "output",
2834
- sessionId,
2835
- data: buildIncrementalStructuredPayload(current, this.config.cardDefaults ?? {}),
2836
- });
2837
- };
2838
- const scheduleEmit = () => {
2839
- if (!emitTimer)
2840
- emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
2841
- };
2842
- const execution = startEmployeeKnowledgeRunner(this.storage, this.grokRunner, {
2843
- session,
2844
- prompt,
2845
- env: buildChildEnv(this.config.inheritEnv !== false),
2846
- }, {
2847
- isActive: () => this.isCurrentRequest(sessionId, requestId),
2848
- onStdout: (text) => this.logger?.appendStructuredStdout(sessionId, text),
2849
- onStderr: (text) => this.logger?.appendStructuredStderr(sessionId, text),
2850
- onEvent: (event) => this.logger?.appendStreamEvent(sessionId, event),
2851
- onUpdate: (turnState) => { syncSnapshot(turnState); scheduleEmit(); },
2852
- });
2853
- this.pendingRunnerExecutions.set(sessionId, execution);
2854
- this.logger?.appendStructuredSpawn(sessionId, {
2855
- kind: "grok-headless",
2856
- provider: "grok",
2857
- pid: execution.pid,
2858
- cwd: session.cwd,
2859
- args: execution.args,
2860
- prompt: prompt.slice(0, 2048),
2861
- promptLength: prompt.length,
2862
- sessionId: session.claudeSessionId,
2863
- spawnedAt: execution.spawnedAt,
2864
- });
2865
- let result;
2866
- try {
2867
- result = await execution.completion;
2868
- }
2869
- finally {
2870
- const released = this.releasePendingRunnerExecution(sessionId, execution);
2871
- if (released)
2872
- this.cancelStreamingCheckpointTimer(sessionId);
2873
- }
2874
- if (!this.isCurrentRequest(sessionId, requestId)) {
2875
- if (emitTimer)
2876
- this.clearStreamEmitTimer(emitTimer);
2877
- return;
2878
- }
2879
- flushEmit();
2880
- this.throwUnacceptedInput(sessionId, result, "grok", "(PATH 中找不到 grok;请安装 Grok Build CLI,或重跑 `wand service:install` 刷新服务 PATH)");
2881
- this.logger?.appendStructuredSpawn(sessionId, {
2882
- kind: "grok-headless-close",
2883
- pid: execution.pid,
2884
- spawnedAt: execution.spawnedAt,
2885
- closedAt: new Date().toISOString(),
2886
- exitCode: result.exitCode,
2887
- stderrTail: result.stderr.slice(-2048),
2888
- primaryError: result.primaryError,
2889
- });
2890
- const current = this.currentSessionForRequest(sessionId, requestId);
2891
- if (!current)
2892
- return;
2893
- const interruptedByUser = this.interruptedWith.has(sessionId);
2894
- const interruptPrompt = this.interruptedWith.get(sessionId);
2895
- if ((result.primaryError || (result.exitCode !== 0 && result.exitCode !== null) || result.signal) && !interruptedByUser) {
2896
- const errorText = this.formatStructuredExitError("grok", result.exitCode, result.signal, { stderr: result.stderr, primary: result.primaryError });
2897
- const failed = this.finishStructuredFailure(current, typeof result.exitCode === "number" ? result.exitCode : 1, errorText, result.state);
2898
- this.sessions.set(sessionId, failed);
2899
- this.saveAuthoritativeSession(failed);
2900
- this.emitStructuredSnapshot(failed);
2901
- this.emitStructuredSnapshot(failed, "ended");
2902
- throw new PersistedStructuredRunnerError(errorText);
2903
- }
2904
- const messages = this.buildCompletedAssistantMessages(current, result.state);
2905
- const keepRunning = !!interruptPrompt;
2906
- const finished = {
2907
- ...current,
2908
- status: keepRunning ? "running" : "idle",
2909
- exitCode: keepRunning ? null : 0,
2910
- endedAt: keepRunning ? null : new Date().toISOString(),
2911
- output: result.state.result,
2912
- claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
2913
- messages,
2914
- queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
2915
- pendingEscalation: null,
2916
- permissionBlocked: false,
2917
- structuredState: {
2918
- ...this.settleTurn(sessionId, current.structuredState),
2919
- model: result.state.model ?? current.structuredState?.model,
2920
- inFlight: false,
2921
- activeRequestId: null,
2922
- lastError: null,
2923
- },
2924
- };
2925
- this.sessions.set(sessionId, finished);
2926
- this.saveAuthoritativeSession(finished);
2927
- this.emitStructuredSnapshot(finished);
2928
- if (!keepRunning)
2929
- this.emitStructuredSnapshot(finished, "ended");
2930
- if (interruptPrompt) {
2931
- this.interruptedWith.delete(sessionId);
2932
- this.preserveQueueOnInterrupt.delete(sessionId);
2933
- setImmediate(() => this.sendMessage(sessionId, interruptPrompt).catch((error) => {
2934
- console.error("[WAND] grok interrupt-and-send failed:", error);
2935
- }));
2936
- return;
2937
- }
2938
- setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
2939
- }
2940
- async runOpenCodeStreaming(sessionId, session, prompt, requestId) {
2941
- let emitTimer = null;
2942
- const syncSnapshot = (turnState) => {
2943
- const current = this.currentSessionForRequest(sessionId, requestId);
2944
- if (!current)
2945
- return;
2946
- const turn = {
2947
- role: "assistant",
2948
- content: this.compactContentBlocks([...turnState.blocks], turnState.result),
2949
- usage: turnState.usage,
2950
- };
2951
- const messages = upsertAssistantMessage(current.messages, turn);
2952
- const patched = {
2953
- ...current,
2954
- claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
2955
- messages,
2956
- output: turnState.result || current.output,
2957
- // 每个 provider 帧都是本轮还在活动的证据:刷新 lastActivityAt 并重挂静默窗口。
2958
- structuredState: this.noteTurnActivity(sessionId, current.structuredState),
2959
- };
2960
- this.sessions.set(sessionId, patched);
2961
- this.saveStreamingSnapshot(patched);
2962
- };
2963
- const flushEmit = () => {
2964
- if (emitTimer)
2965
- this.clearStreamEmitTimer(emitTimer);
2966
- emitTimer = null;
2967
- const current = this.currentSessionForRequest(sessionId, requestId);
2968
- if (current) {
2969
- this.emit({
2970
- type: "output",
2971
- sessionId,
2972
- data: buildIncrementalStructuredPayload(current, this.config.cardDefaults ?? {}),
2973
- });
2974
- }
2975
- };
2976
- const scheduleEmit = () => {
2977
- if (!emitTimer) {
2978
- emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
2979
- }
2980
- };
2981
- const execution = startEmployeeKnowledgeRunner(this.storage, this.openCodeRunner, {
2982
- session,
2983
- prompt,
2984
- env: buildChildEnv(this.config.inheritEnv !== false),
2985
- }, {
2986
- isActive: () => this.isCurrentRequest(sessionId, requestId),
2987
- onStdout: (text) => this.logger?.appendStructuredStdout(sessionId, text),
2988
- onStderr: (text) => this.logger?.appendStructuredStderr(sessionId, text),
2989
- onEvent: (event) => this.logger?.appendStreamEvent(sessionId, event),
2990
- onUpdate: (turnState) => {
2991
- syncSnapshot(turnState);
2992
- scheduleEmit();
2993
- },
2994
- });
2995
- this.pendingRunnerExecutions.set(sessionId, execution);
2996
- this.logger?.appendStructuredSpawn(sessionId, {
2997
- kind: "opencode-run",
2998
- provider: "opencode",
2999
- pid: execution.pid,
3000
- cwd: session.cwd,
3001
- args: execution.args,
3002
- prompt: prompt.slice(0, 2048),
3003
- promptLength: prompt.length,
3004
- sessionId: session.claudeSessionId,
3005
- spawnedAt: execution.spawnedAt,
3006
- });
3007
- let result;
3008
- try {
3009
- result = await execution.completion;
3010
- }
3011
- finally {
3012
- const released = this.releasePendingRunnerExecution(sessionId, execution);
3013
- if (released)
3014
- this.cancelStreamingCheckpointTimer(sessionId);
3015
- }
3016
- if (!this.isCurrentRequest(sessionId, requestId)) {
3017
- if (emitTimer)
3018
- this.clearStreamEmitTimer(emitTimer);
3019
- return;
3020
- }
3021
- flushEmit();
3022
- this.throwUnacceptedInput(sessionId, result, "opencode run", "(PATH 中找不到 opencode;请安装 opencode-ai,或重跑 `wand service:install` 刷新服务 PATH)");
3023
- this.logger?.appendStructuredSpawn(sessionId, {
3024
- kind: "opencode-run-close",
3025
- pid: execution.pid,
3026
- spawnedAt: execution.spawnedAt,
3027
- closedAt: new Date().toISOString(),
3028
- exitCode: result.exitCode,
3029
- stderrTail: result.stderr.slice(-2048),
3030
- primaryError: result.primaryError,
3031
- });
3032
- const current = this.currentSessionForRequest(sessionId, requestId);
3033
- if (!current)
3034
- return;
3035
- const interruptedByUser = this.interruptedWith.has(sessionId);
3036
- const interruptPrompt = this.interruptedWith.get(sessionId);
3037
- if ((result.primaryError || (result.exitCode !== 0 && result.exitCode !== null) || result.signal) && !interruptedByUser) {
3038
- const legacyHint = /unknown command|unknown flag|No help topic for 'run'/i.test(result.stderr)
3039
- ? "\n检测到旧版 OpenCode CLI;请卸载 0.0.x 旧包并安装 `opencode-ai@latest`。"
3040
- : "";
3041
- const errorText = this.formatStructuredExitError("opencode run", result.exitCode, result.signal, { stderr: result.stderr, primary: result.primaryError }) + legacyHint;
3042
- const failed = this.finishStructuredFailure(current, typeof result.exitCode === "number" ? result.exitCode : 1, errorText, result.state);
3043
- this.sessions.set(sessionId, failed);
3044
- this.saveAuthoritativeSession(failed);
3045
- this.emitStructuredSnapshot(failed);
3046
- this.emitStructuredSnapshot(failed, "ended");
3047
- throw new PersistedStructuredRunnerError(errorText);
3048
- }
3049
- const messages = this.buildCompletedAssistantMessages(current, result.state);
3050
- const keepRunning = !!interruptPrompt;
3051
- const finished = {
3052
- ...current,
3053
- status: keepRunning ? "running" : "idle",
3054
- exitCode: keepRunning ? null : 0,
3055
- endedAt: keepRunning ? null : new Date().toISOString(),
3056
- output: result.state.result,
3057
- claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
3058
- messages,
3059
- queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
3060
- pendingEscalation: null,
3061
- permissionBlocked: false,
3062
- structuredState: {
3063
- ...this.settleTurn(sessionId, current.structuredState),
3064
- model: result.state.model ?? current.structuredState?.model,
3065
- inFlight: false,
3066
- activeRequestId: null,
3067
- lastError: null,
3068
- },
3069
- };
3070
- this.sessions.set(sessionId, finished);
3071
- this.saveAuthoritativeSession(finished);
3072
- this.emitStructuredSnapshot(finished);
3073
- if (!keepRunning)
3074
- this.emitStructuredSnapshot(finished, "ended");
3075
- if (interruptPrompt) {
3076
- this.interruptedWith.delete(sessionId);
3077
- this.preserveQueueOnInterrupt.delete(sessionId);
3078
- setImmediate(() => {
3079
- this.sendMessage(sessionId, interruptPrompt).catch((error) => {
3080
- console.error("[WAND] opencode interrupt-and-send failed:", error);
3081
- });
3082
- });
3083
- return;
3084
- }
3085
- setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
3086
- }
3087
- // ---------------------------------------------------------------------------
3088
- // Streaming claude -p execution
3089
- // ---------------------------------------------------------------------------
3090
- /**
3091
- * Spawn `claude -p --output-format stream-json` and parse NDJSON lines as
3092
- * they arrive, emitting incremental WebSocket events so the UI can render
3093
- * text / thinking / tool_use blocks in real-time.
3094
- *
3095
- * Permission handling:
3096
- * - Non-root + full-access/managed: --permission-mode bypassPermissions
3097
- * - Non-root + auto-edit: --permission-mode acceptEdits
3098
- * - Root: --permission-mode acceptEdits + --allowedTools (extends approval
3099
- * outside CWD). stdin is always "ignore" — no ACP bidirectional control.
3100
- */
3101
- async runClaudeStreaming(sessionId, session, prompt, requestId, options = {}) {
2684
+ /** One turn lifecycle for every structured adapter; provider protocol reducers stay separate. */
2685
+ async runStructuredStreaming(sessionId, session, prompt, requestId, options = {}) {
3102
2686
  let emitTimer = null;
3103
2687
  const syncSnapshot = (turnState) => {
3104
2688
  const current = this.currentSessionForRequest(sessionId, requestId);
@@ -3150,21 +2734,46 @@ export class StructuredSessionManager {
3150
2734
  const provider = options.provider ?? "claude";
3151
2735
  const commandLabel = options.commandLabel ?? "claude -p";
3152
2736
  const logKind = options.logKind ?? "claude-print";
2737
+ // 只在有多条候选可退的员工会话上看门:单候选会话没有可退的下一条链路,终止它只会丢工作。
2738
+ const candidateChain = !!session.employeeId && (session.employeeCandidates?.length ?? 0) > 1;
2739
+ let startupTimedOut = false;
2740
+ let turnCompleted = false;
2741
+ const disarmStartupWatchdog = () => {
2742
+ const timer = this.candidateStartupWatchdogs.get(sessionId);
2743
+ if (!timer)
2744
+ return;
2745
+ clearTimeout(timer);
2746
+ this.candidateStartupWatchdogs.delete(sessionId);
2747
+ };
3153
2748
  const execution = startEmployeeKnowledgeRunner(this.storage, runner, {
3154
2749
  session,
3155
2750
  prompt,
3156
2751
  env: buildChildEnv(this.config.inheritEnv !== false),
3157
2752
  }, {
3158
2753
  isActive: () => this.isCurrentRequest(sessionId, requestId),
3159
- onStdout: (text) => this.logger?.appendStructuredStdout(sessionId, text),
2754
+ onStdout: (text) => {
2755
+ if (text)
2756
+ disarmStartupWatchdog();
2757
+ this.logger?.appendStructuredStdout(sessionId, text);
2758
+ },
3160
2759
  onStderr: (text) => this.logger?.appendStructuredStderr(sessionId, text),
3161
2760
  onEvent: (event) => this.logger?.appendStreamEvent(sessionId, event),
3162
2761
  onUpdate: (turnState) => {
2762
+ disarmStartupWatchdog();
3163
2763
  syncSnapshot(turnState);
3164
2764
  scheduleEmit();
3165
2765
  },
3166
2766
  });
3167
2767
  this.pendingRunnerExecutions.set(sessionId, execution);
2768
+ if (candidateChain) {
2769
+ this.candidateStartupWatchdogs.set(sessionId, setTimeout(() => {
2770
+ this.candidateStartupWatchdogs.delete(sessionId);
2771
+ if (turnCompleted || !this.isCurrentRequest(sessionId, requestId))
2772
+ return;
2773
+ startupTimedOut = true;
2774
+ execution.interrupt();
2775
+ }, EMPLOYEE_CANDIDATE_STARTUP_TIMEOUT_MS));
2776
+ }
3168
2777
  this.logger?.appendStructuredSpawn(sessionId, {
3169
2778
  kind: logKind,
3170
2779
  provider,
@@ -3174,6 +2783,7 @@ export class StructuredSessionManager {
3174
2783
  prompt: prompt.slice(0, 2048),
3175
2784
  promptLength: prompt.length,
3176
2785
  claudeSessionId: session.claudeSessionId,
2786
+ ...(provider === "codex" ? { threadId: session.claudeSessionId } : { sessionId: session.claudeSessionId }),
3177
2787
  spawnedAt: execution.spawnedAt,
3178
2788
  });
3179
2789
  let result;
@@ -3181,6 +2791,8 @@ export class StructuredSessionManager {
3181
2791
  result = await execution.completion;
3182
2792
  }
3183
2793
  finally {
2794
+ turnCompleted = true;
2795
+ disarmStartupWatchdog();
3184
2796
  const released = this.releasePendingRunnerExecution(sessionId, execution);
3185
2797
  if (released)
3186
2798
  this.cancelStreamingCheckpointTimer(sessionId);
@@ -3191,7 +2803,18 @@ export class StructuredSessionManager {
3191
2803
  return;
3192
2804
  }
3193
2805
  flushEmit();
3194
- this.throwUnacceptedInput(sessionId, result, commandLabel, `(PATH 中找不到 ${provider === "qoder" ? "qodercli" : provider === "gemini" ? "gemini" : provider === "pi" ? "pi" : "claude"} 可执行文件;${options.installHint ?? "请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"})`);
2806
+ // 看门狗只在本轮确实没有产出任何东西时才算「未开始执行」;已经出过内容的那一轮维持原判定。
2807
+ if (startupTimedOut && !hasStructuredExecutionProgress(result.state)) {
2808
+ const unaccepted = { kind: "preflight", delivery: "rejected", retryable: true };
2809
+ result = {
2810
+ ...result,
2811
+ inputAccepted: false,
2812
+ primaryError: result.primaryError?.trim()
2813
+ || `${commandLabel} 在 ${EMPLOYEE_CANDIDATE_STARTUP_TIMEOUT_MS / 1000} 秒内没有任何协议输出,本轮未开始执行。`,
2814
+ failure: unaccepted,
2815
+ };
2816
+ }
2817
+ this.throwUnacceptedInput(sessionId, result, commandLabel, `(PATH 中找不到 ${providerCliCommand(provider)} 可执行文件;${options.installHint ?? `请确认 ${providerCliCommand(provider)} 已安装,或重跑 wand service:install 刷新服务的 PATH`})`);
3195
2818
  this.logger?.appendStructuredSpawn(sessionId, {
3196
2819
  kind: `${logKind}-close`,
3197
2820
  pid: execution.pid,
@@ -3199,24 +2822,31 @@ export class StructuredSessionManager {
3199
2822
  closedAt: new Date().toISOString(),
3200
2823
  exitCode: result.exitCode,
3201
2824
  stderrTail: result.stderr.slice(-2048),
2825
+ primaryError: result.primaryError,
2826
+ ...(provider === "codex" ? { codexErrors: result.errors, codexTurnFailed: result.primaryError } : {}),
3202
2827
  });
3203
2828
  const current = this.currentSessionForRequest(sessionId, requestId);
3204
2829
  if (!current)
3205
2830
  return;
3206
2831
  const interruptedByUser = this.interruptedWith.has(sessionId);
3207
- const interruptedForQuestion = result.stopReason === "ask-user-question";
2832
+ const supportsToolContinuation = provider !== "codex" && provider !== "opencode" && provider !== "grok";
2833
+ const interruptedForQuestion = supportsToolContinuation && result.stopReason === "ask-user-question";
3208
2834
  const failedExit = (result.exitCode !== null && result.exitCode !== 0) || result.signal !== null;
3209
2835
  // A protocol failure remains a failure even if the CLI exits 0 or already produced work.
3210
2836
  // Earlier work is preserved below and must never be replayed on another candidate.
3211
2837
  const failure = result.failure ?? classifyStructuredFailure(result);
3212
2838
  if (failure && !interruptedByUser && !interruptedForQuestion) {
3213
- const errorText = !failedExit && result.primaryError
2839
+ let errorText = supportsToolContinuation && !failedExit && result.primaryError
3214
2840
  ? result.primaryError.trim()
3215
2841
  : this.formatStructuredExitError(commandLabel, result.exitCode, result.signal, {
3216
2842
  stderr: result.stderr,
3217
2843
  stdoutTail: result.stdoutTail,
3218
2844
  primary: result.primaryError,
2845
+ extras: result.errors,
3219
2846
  });
2847
+ if (provider === "opencode" && /unknown command|unknown flag|No help topic for 'run'/i.test(result.stderr)) {
2848
+ errorText += "\n检测到旧版 OpenCode CLI;请卸载 0.0.x 旧包并安装 `opencode-ai@latest`。";
2849
+ }
3220
2850
  const failed = this.finishStructuredFailure(current, failedExit && typeof result.exitCode === "number" ? result.exitCode : 1, errorText, result.state);
3221
2851
  if (provider === "pi" && isMissingPiSession(result.stderr, current.claudeSessionId)) {
3222
2852
  failed.claudeSessionId = null;
@@ -3282,7 +2912,7 @@ export class StructuredSessionManager {
3282
2912
  return;
3283
2913
  }
3284
2914
  const lastToolUse = [...result.state.blocks].reverse().find((block) => block.type === "tool_use");
3285
- if (lastToolUse?.name === "ExitPlanMode" && result.state.sessionId) {
2915
+ if (supportsToolContinuation && lastToolUse?.name === "ExitPlanMode" && result.state.sessionId) {
3286
2916
  setImmediate(() => {
3287
2917
  this.sendMessage(sessionId, "Plan approved. Proceed with the implementation.").catch((error) => {
3288
2918
  console.error("[WAND] Auto-continue after ExitPlanMode failed:", error);