@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.
- package/README.md +5 -3
- package/dist/agent-dispatch.js +4 -9
- package/dist/ai-team-employee-binding.js +10 -9
- package/dist/ai-team-runner.d.ts +2 -1
- package/dist/ai-team-runner.js +38 -2
- package/dist/ai-team-types.js +3 -2
- package/dist/ai-text-runner.d.ts +61 -0
- package/dist/ai-text-runner.js +653 -0
- package/dist/avatar-input.d.ts +2 -0
- package/dist/avatar-input.js +19 -0
- package/dist/bounded-process-output.d.ts +5 -0
- package/dist/bounded-process-output.js +9 -0
- package/dist/build-info.json +3 -3
- package/dist/command-error.d.ts +6 -0
- package/dist/command-error.js +8 -0
- package/dist/config.js +11 -42
- package/dist/conversation-service.d.ts +12 -2
- package/dist/conversation-service.js +104 -25
- package/dist/conversation-session-preview.d.ts +2 -0
- package/dist/conversation-session-preview.js +4 -0
- package/dist/decision-client.js +22 -6
- package/dist/decision-expert-employee.js +2 -2
- package/dist/decision-expert-identity.d.ts +1 -1
- package/dist/decision-expert-identity.js +1 -1
- package/dist/decision-expert-service.d.ts +1 -0
- package/dist/decision-expert-service.js +82 -76
- package/dist/decision-hardware.js +4 -3
- package/dist/decision-types.d.ts +4 -0
- package/dist/decision-types.js +4 -0
- package/dist/deployment-lock.d.ts +13 -0
- package/dist/deployment-lock.js +189 -0
- package/dist/employee-text.js +1 -1
- package/dist/fixed-employee-avatar.d.ts +7 -0
- package/dist/fixed-employee-avatar.js +13 -0
- package/dist/git-quick-commit.d.ts +5 -40
- package/dist/git-quick-commit.js +8 -636
- package/dist/hr-employee-identity.d.ts +5 -0
- package/dist/hr-employee-identity.js +5 -0
- package/dist/hr-employee.d.ts +5 -0
- package/dist/hr-employee.js +21 -0
- package/dist/middleware/path-safety.js +1 -1
- package/dist/milestone-scope.js +1 -1
- package/dist/model-auto-assign.d.ts +1 -1
- package/dist/model-auto-assign.js +1 -1
- package/dist/models.d.ts +4 -0
- package/dist/models.js +21 -1
- package/dist/npm-update-utils.js +5 -44
- package/dist/pi-auto-resources.js +6 -3
- package/dist/plush-avatar.d.ts +10 -1
- package/dist/plush-avatar.js +13 -1
- package/dist/plush-cat-appearance.d.ts +97 -0
- package/dist/plush-cat-appearance.js +118 -0
- package/dist/plush-cat-whiskers.d.ts +6 -0
- package/dist/plush-cat-whiskers.js +42 -0
- package/dist/process-manager.d.ts +22 -21
- package/dist/process-manager.js +49 -83
- package/dist/prompt-optimizer.d.ts +1 -1
- package/dist/prompt-optimizer.js +1 -1
- package/dist/render-host.js +1 -19
- package/dist/scheduled-task-service.d.ts +41 -0
- package/dist/scheduled-task-service.js +294 -0
- package/dist/scheduled-task-state.d.ts +11 -0
- package/dist/scheduled-task-state.js +65 -0
- package/dist/scheduled-task-time.d.ts +4 -0
- package/dist/scheduled-task-time.js +82 -0
- package/dist/scheduled-task-types.d.ts +71 -0
- package/dist/scheduled-task-types.js +1 -0
- package/dist/server-ai-team-routes.js +3 -17
- package/dist/server-employee-routes.d.ts +3 -0
- package/dist/server-employee-routes.js +20 -38
- package/dist/server-file-routes.js +44 -1
- package/dist/server-pi-recommendation-routes.js +13 -4
- package/dist/server-request.d.ts +2 -0
- package/dist/server-request.js +9 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-scheduled-task-routes.d.ts +4 -0
- package/dist/server-scheduled-task-routes.js +34 -0
- package/dist/server-session-routes.d.ts +5 -13
- package/dist/server-session-routes.js +35 -70
- package/dist/server-task-routes.d.ts +6 -21
- package/dist/server-task-routes.js +25 -120
- package/dist/server-team-dispatch-routes.js +12 -2
- package/dist/server-workspace-routes.d.ts +1 -1
- package/dist/server-workspace-routes.js +33 -78
- package/dist/server.js +55 -20
- package/dist/session-model.d.ts +15 -0
- package/dist/session-model.js +23 -0
- package/dist/session-registry.d.ts +5 -1
- package/dist/session-registry.js +20 -11
- package/dist/session-request.d.ts +9 -0
- package/dist/session-request.js +27 -0
- package/dist/session-topic.d.ts +1 -1
- package/dist/session-topic.js +1 -1
- package/dist/session-transport.d.ts +3 -1
- package/dist/session-transport.js +11 -5
- package/dist/silicon-employee-draft.d.ts +2 -2
- package/dist/silicon-employee-draft.js +3 -3
- package/dist/socket-probe.d.ts +2 -0
- package/dist/socket-probe.js +20 -0
- package/dist/storage-schedules.d.ts +20 -0
- package/dist/storage-schedules.js +78 -0
- package/dist/storage-schema.d.ts +3 -0
- package/dist/storage-schema.js +702 -0
- package/dist/storage.d.ts +42 -8
- package/dist/storage.js +182 -715
- package/dist/structured-claude-adapter.js +12 -0
- package/dist/structured-claude-protocol.d.ts +4 -0
- package/dist/structured-claude-protocol.js +18 -4
- package/dist/structured-codex-adapter.js +11 -0
- package/dist/structured-exec-pump.js +79 -38
- package/dist/structured-failure.d.ts +21 -0
- package/dist/structured-failure.js +14 -0
- package/dist/structured-gemini-adapter.js +12 -2
- package/dist/structured-grok-adapter.js +12 -2
- package/dist/structured-opencode-adapter.js +8 -1
- package/dist/structured-qoder-adapter.js +14 -0
- package/dist/structured-session-manager.d.ts +13 -18
- package/dist/structured-session-manager.js +147 -517
- package/dist/system-employee.d.ts +1 -1
- package/dist/system-employee.js +1 -1
- package/dist/task-archive-policy.js +1 -1
- package/dist/task-read-model.d.ts +27 -0
- package/dist/task-read-model.js +47 -0
- package/dist/task-retention.d.ts +1 -1
- package/dist/task-retention.js +1 -1
- package/dist/task-title-service.d.ts +30 -0
- package/dist/task-title-service.js +122 -0
- package/dist/task-title.d.ts +1 -1
- package/dist/task-title.js +1 -1
- package/dist/terminal-daemon-client.js +1 -18
- package/dist/user-memory.js +1 -1
- package/dist/web-ui/asset-runtime.d.ts +12 -0
- package/dist/web-ui/asset-runtime.js +6 -0
- package/dist/web-ui/composer-avatar-activity.d.ts +6 -0
- package/dist/web-ui/composer-avatar-activity.js +29 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/module-manifest.json +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chat-render-PY6DIXNZ.js +24 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-2YZLCGYN.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-6CFHW3FA.js +111 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-6XQEBIEX.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-DDSO7OX7.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-G3CXNMA2.js +20 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-LGVAPHRS.js +36 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-NURCNMRU.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-PCMFGZKX.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-QRNA4D7E.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-TMRPHNJO.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-WC7RK7LP.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-YOXXNXOX.js +2 -0
- package/dist/web-ui/content/modules/5478be410b112154/chunk-YX475LXO.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/employee-card-MDWKU4V3.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-56WPSON2.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-5QOHKHNC.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-AWIJ5L2K.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-BOZLGIT5.js +69 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-EZV22Q7Y.js +4 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-IW43Z5ZF.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-JT7NIUQL.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-JWEIDB55.js +3 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-OU5TRYH6.js +2 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-PRSTCPVE.js +5 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-TWPJI3HE.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-WZGJSNML.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-X6KQVFD6.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-YKCQFMQR.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/host-bridge-2LZX6UUZ.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/library-bridge-MQFJGTGY.js +4 -0
- package/dist/web-ui/content/modules/5478be410b112154/page-T3VUCU2W.js +3 -0
- package/dist/web-ui/content/modules/5478be410b112154/panel-HJBGBBPY.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/runtime.js +920 -0
- package/dist/web-ui/content/modules/5478be410b112154/sha256-ANXV72UC.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/task-board-host-AVMDIHIV.js +2 -0
- package/dist/web-ui/content/modules/5478be410b112154/workspace-agent-picker-KI7QVSK3.js +1 -0
- package/dist/web-ui/content/modules/5478be410b112154/workspace-window-L542FGP2.js +1 -0
- package/dist/web-ui/content/plush-avatar.js +177 -177
- package/dist/web-ui/content/scripts.js +1 -994
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +32 -0
- package/dist/web-ui/embedded-assets.js +10 -8
- package/dist/web-ui/markdown.d.ts +3 -2
- package/dist/web-ui/markdown.js +61 -48
- package/dist/web-ui/message-file-target.d.ts +13 -0
- package/dist/web-ui/message-file-target.js +85 -0
- package/dist/web-ui/provider-identity.d.ts +1 -1
- package/dist/web-ui/provider-identity.js +3 -4
- package/dist/web-ui/react/local-preview/controller.d.ts +2 -0
- package/dist/web-ui/react/local-preview/controller.js +12 -0
- package/dist/web-ui/scripts.d.ts +10 -0
- package/dist/web-ui/scripts.js +42 -5
- package/package.json +6 -3
- package/browser-extension/icons/wand-128.png +0 -0
- package/browser-extension/icons/wand-16.png +0 -0
- package/browser-extension/icons/wand-32.png +0 -0
- package/browser-extension/icons/wand-48.png +0 -0
- package/dist/web-ui/content/licenses/codex-LICENSE.txt +0 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
/**
|
|
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 (
|
|
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
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
:
|
|
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
|
-
...
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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);
|