@co0ontty/wand 4.83.1 → 4.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/agent-dispatch.js +5 -1
- package/dist/ai-team-availability.d.ts +1 -1
- package/dist/ai-team-prompts.js +13 -4
- package/dist/ai-team-runner.d.ts +80 -5
- package/dist/ai-team-runner.js +332 -55
- package/dist/ai-team-types.d.ts +92 -5
- package/dist/ai-team-types.js +9 -2
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +3 -0
- package/dist/cli.js +18 -1
- package/dist/commit-task-archive.d.ts +3 -1
- package/dist/commit-task-archive.js +21 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +73 -0
- package/dist/conversation-mentions.d.ts +16 -0
- package/dist/conversation-mentions.js +32 -0
- package/dist/conversation-service.d.ts +90 -0
- package/dist/conversation-service.js +759 -0
- package/dist/conversation-session-preview.d.ts +9 -0
- package/dist/conversation-session-preview.js +47 -0
- package/dist/conversation-task-preview.d.ts +5 -0
- package/dist/conversation-task-preview.js +8 -0
- package/dist/conversation-types.d.ts +80 -0
- package/dist/conversation-types.js +7 -0
- package/dist/core-extension-host.d.ts +27 -0
- package/dist/core-extension-host.js +138 -0
- package/dist/core-runner.d.ts +86 -0
- package/dist/core-runner.js +757 -0
- package/dist/core-status-cli.d.ts +9 -0
- package/dist/core-status-cli.js +280 -0
- package/dist/core-turn-tracker.d.ts +12 -0
- package/dist/core-turn-tracker.js +36 -0
- package/dist/decision-runner.d.ts +6 -0
- package/dist/decision-runner.js +3 -1
- package/dist/decision-tool.d.ts +2 -0
- package/dist/decision-tool.js +49 -22
- package/dist/env-utils.d.ts +7 -3
- package/dist/env-utils.js +30 -5
- package/dist/git-quick-commit.d.ts +5 -0
- package/dist/git-quick-commit.js +9 -3
- package/dist/git-utils.js +2 -1
- package/dist/harness-engine.d.ts +110 -0
- package/dist/harness-engine.js +331 -0
- package/dist/mission-diff.js +2 -1
- package/dist/model-auto-assign.d.ts +50 -0
- package/dist/model-auto-assign.js +163 -0
- package/dist/model-group-runner.d.ts +4 -0
- package/dist/model-group-runner.js +72 -0
- package/dist/model-groups.d.ts +38 -0
- package/dist/model-groups.js +138 -0
- package/dist/models.d.ts +12 -0
- package/dist/models.js +102 -8
- package/dist/npm-update-utils.js +2 -1
- package/dist/openrouter-free-cli.d.ts +25 -0
- package/dist/openrouter-free-cli.js +109 -0
- package/dist/openrouter-free-models.d.ts +85 -0
- package/dist/openrouter-free-models.js +458 -0
- package/dist/openrouter-free-selection.d.ts +5 -0
- package/dist/openrouter-free-selection.js +7 -0
- package/dist/path-repair.d.ts +10 -0
- package/dist/path-repair.js +50 -5
- package/dist/pi-auto-resources.d.ts +12 -0
- package/dist/pi-auto-resources.js +151 -0
- package/dist/pi-execution-types.d.ts +48 -0
- package/dist/pi-execution-types.js +1 -0
- package/dist/pi-execution.d.ts +9 -0
- package/dist/pi-execution.js +187 -0
- package/dist/pi-extension-resources.d.ts +13 -0
- package/dist/pi-extension-resources.js +71 -0
- package/dist/pi-openrouter-free-bridge.d.ts +2 -0
- package/dist/pi-openrouter-free-bridge.js +80 -0
- package/dist/pi-resource-bridge.d.ts +2 -0
- package/dist/pi-resource-bridge.js +73 -0
- package/dist/pi-resource-catalog.d.ts +18 -0
- package/dist/pi-resource-catalog.js +82 -0
- package/dist/pi-resource-recommendation.d.ts +29 -0
- package/dist/pi-resource-recommendation.js +169 -0
- package/dist/pi-resource-run.d.ts +8 -0
- package/dist/pi-resource-run.js +28 -0
- package/dist/pi-session-settings.d.ts +157 -0
- package/dist/pi-session-settings.js +166 -0
- package/dist/platform-action-catalog.d.ts +24 -0
- package/dist/platform-action-catalog.js +141 -0
- package/dist/platform-action-service.d.ts +37 -0
- package/dist/platform-action-service.js +138 -0
- package/dist/platform-action-types.d.ts +125 -0
- package/dist/platform-action-types.js +7 -0
- package/dist/process-manager.d.ts +21 -1
- package/dist/process-manager.js +168 -15
- package/dist/provider-catalog.js +1 -1
- package/dist/provider-cli-updater.js +29 -8
- package/dist/provider-history-scanner.d.ts +9 -0
- package/dist/provider-history-scanner.js +107 -1
- package/dist/retention.d.ts +19 -7
- package/dist/retention.js +171 -28
- package/dist/server-ai-team-routes.js +6 -2
- package/dist/server-conversation-routes.d.ts +5 -0
- package/dist/server-conversation-routes.js +91 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +85 -11
- package/dist/server-pi-recommendation-routes.d.ts +13 -0
- package/dist/server-pi-recommendation-routes.js +67 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.d.ts +4 -3
- package/dist/server-session-routes.js +182 -37
- package/dist/server-settings-routes.d.ts +5 -0
- package/dist/server-settings-routes.js +46 -0
- package/dist/server-task-routes.js +49 -4
- package/dist/server-team-dispatch-routes.d.ts +19 -0
- package/dist/server-team-dispatch-routes.js +71 -0
- package/dist/server-update-routes.d.ts +1 -1
- package/dist/server-update-routes.js +10 -5
- package/dist/server-workspace-routes.js +44 -24
- package/dist/server.d.ts +1 -0
- package/dist/server.js +148 -30
- package/dist/session-ai-context.d.ts +5 -1
- package/dist/session-ai-context.js +9 -4
- package/dist/session-registry.d.ts +4 -0
- package/dist/session-registry.js +47 -13
- package/dist/session-transport.js +8 -1
- package/dist/settings-web-access.d.ts +15 -0
- package/dist/settings-web-access.js +37 -0
- package/dist/silicon-employee-defaults.d.ts +29 -0
- package/dist/silicon-employee-defaults.js +41 -0
- package/dist/silicon-employee-dispatch.d.ts +5 -1
- package/dist/silicon-employee-dispatch.js +10 -4
- package/dist/silicon-employee-draft.d.ts +21 -4
- package/dist/silicon-employee-draft.js +103 -16
- package/dist/storage.d.ts +73 -2
- package/dist/storage.js +362 -26
- package/dist/structured-claude-protocol.js +1 -1
- package/dist/structured-client-protocol.js +11 -33
- package/dist/structured-content.d.ts +23 -0
- package/dist/structured-content.js +72 -0
- package/dist/structured-failure.d.ts +16 -0
- package/dist/structured-failure.js +36 -0
- package/dist/structured-pi-adapter.d.ts +8 -3
- package/dist/structured-pi-adapter.js +131 -30
- package/dist/structured-provider-common.d.ts +6 -0
- package/dist/structured-provider-common.js +23 -1
- package/dist/structured-runner.d.ts +45 -0
- package/dist/structured-session-manager.d.ts +78 -4
- package/dist/structured-session-manager.js +513 -129
- package/dist/structured-thinking.d.ts +15 -0
- package/dist/structured-thinking.js +46 -0
- package/dist/subagent-dispatch.d.ts +56 -0
- package/dist/subagent-dispatch.js +102 -0
- package/dist/system-employee.d.ts +4 -3
- package/dist/system-employee.js +7 -4
- package/dist/task-retention.d.ts +23 -0
- package/dist/task-retention.js +69 -0
- package/dist/task-types.d.ts +4 -0
- package/dist/task-types.js +3 -0
- package/dist/team-dispatch.d.ts +101 -0
- package/dist/team-dispatch.js +269 -0
- package/dist/tool-preview.js +7 -1
- package/dist/tui/commands.js +50 -1
- package/dist/tui/ipc-protocol.d.ts +1 -1
- package/dist/tui/ipc-server.d.ts +4 -0
- package/dist/tui/ipc-server.js +23 -0
- package/dist/turn-heartbeat.d.ts +35 -0
- package/dist/turn-heartbeat.js +64 -0
- package/dist/types.d.ts +147 -1
- package/dist/user-profile.d.ts +33 -0
- package/dist/user-profile.js +70 -0
- package/dist/web-ui/chat-message-meta.d.ts +11 -0
- package/dist/web-ui/chat-message-meta.js +47 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +795 -95
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -6808
- package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/index.d.ts +1 -1
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/markdown.d.ts +2 -0
- package/dist/web-ui/markdown.js +111 -0
- package/dist/web-ui/page.d.ts +4 -0
- package/dist/web-ui/page.js +8 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
- package/dist/web-ui/react/local-preview/controller.js +159 -0
- package/dist/web-ui/running-activity.d.ts +63 -0
- package/dist/web-ui/running-activity.js +134 -0
- package/dist/web-ui/styles.js +2 -11
- package/package.json +7 -2
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { defaultModelGroupSelector, isAutoAssignSelector, resolveModelGroupModels } from "./model-groups.js";
|
|
2
|
+
import { resolveAutoAssign } from "./model-auto-assign.js";
|
|
3
|
+
import { withModelGroups } from "./model-group-runner.js";
|
|
4
|
+
import { classifyStructuredFailure, hasStructuredExecutionProgress } from "./structured-failure.js";
|
|
5
|
+
import { withAutomaticPiResources } from "./pi-auto-resources.js";
|
|
1
6
|
import { randomUUID } from "node:crypto";
|
|
2
7
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
3
8
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
@@ -7,6 +12,10 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
7
12
|
import { getDefaultModelForProvider } from "./config.js";
|
|
8
13
|
import { recordIterationPrompt } from "./iteration-log.js";
|
|
9
14
|
import { startEmployeeKnowledgeRunner } from "./employee-knowledge.js";
|
|
15
|
+
import { CoreRunner } from "./core-runner.js";
|
|
16
|
+
import { CoreTurnTracker } from "./core-turn-tracker.js";
|
|
17
|
+
import { coreHarnessAgentDir, resolveHarnessEngineSync } from "./harness-engine.js";
|
|
18
|
+
import { cliPiSettingsRejection, defaultPiCliSessionSettings, defaultPiSessionSettings, patchPiSessionSettings } from "./pi-session-settings.js";
|
|
10
19
|
import { withDecisionAccess } from "./decision-runner.js";
|
|
11
20
|
import { signalNameFromNumber } from "./signal-utils.js";
|
|
12
21
|
import { provisionalSessionTopic, SessionNativeTitleTracker, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
@@ -26,6 +35,7 @@ import { PiRunner, applyPiEvent, isMissingPiSession } from "./structured-pi-adap
|
|
|
26
35
|
import { GeminiRunner, applyGeminiEvent, isMissingGeminiSession } from "./structured-gemini-adapter.js";
|
|
27
36
|
import { structuredRunId, } from "./structured-exec-host.js";
|
|
28
37
|
import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, } from "./structured-provider-common.js";
|
|
38
|
+
import { TurnQuietHeartbeat } from "./turn-heartbeat.js";
|
|
29
39
|
import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
|
|
30
40
|
import { RETENTION_IDLE_MS } from "./retention.js";
|
|
31
41
|
class PersistedStructuredRunnerError extends Error {
|
|
@@ -34,12 +44,17 @@ class PersistedStructuredRunnerError extends Error {
|
|
|
34
44
|
this.name = "PersistedStructuredRunnerError";
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
|
-
/**
|
|
38
|
-
class
|
|
39
|
-
|
|
47
|
+
/** Runner-owned refusal facts, never inferred by schedulers from a displayed error. */
|
|
48
|
+
class UnacceptedStructuredInputError extends Error {
|
|
49
|
+
failure;
|
|
50
|
+
#state;
|
|
51
|
+
constructor(message, failure, state) {
|
|
40
52
|
super(message);
|
|
41
|
-
this.
|
|
53
|
+
this.failure = failure;
|
|
54
|
+
this.name = "UnacceptedStructuredInputError";
|
|
55
|
+
this.#state = state;
|
|
42
56
|
}
|
|
57
|
+
get state() { return this.#state; }
|
|
43
58
|
}
|
|
44
59
|
function buildReplayProcessor(session) {
|
|
45
60
|
const runner = session.runner;
|
|
@@ -113,7 +128,7 @@ function buildReplayProcessor(session) {
|
|
|
113
128
|
state,
|
|
114
129
|
stderr: "",
|
|
115
130
|
primaryError: null,
|
|
116
|
-
feed: (line) => {
|
|
131
|
+
feed: (line, observedAt) => {
|
|
117
132
|
const trimmed = line.trim();
|
|
118
133
|
if (!trimmed)
|
|
119
134
|
return false;
|
|
@@ -130,7 +145,7 @@ function buildReplayProcessor(session) {
|
|
|
130
145
|
? applyOpenCodeEvent(state, event)
|
|
131
146
|
: runner === "gemini-cli-json"
|
|
132
147
|
? applyGeminiEvent(state, event)
|
|
133
|
-
: applyPiEvent(state, event);
|
|
148
|
+
: applyPiEvent(state, event, observedAt);
|
|
134
149
|
if (error)
|
|
135
150
|
processor.primaryError = error;
|
|
136
151
|
return true;
|
|
@@ -376,6 +391,23 @@ class TruncatedReplayView {
|
|
|
376
391
|
function appendNoticeTurn(messages, turn) {
|
|
377
392
|
return [...(messages ?? []), { ...turn, createdAt: isoNow(), completedAt: isoNow() }];
|
|
378
393
|
}
|
|
394
|
+
/** core 引擎的压缩通知 → 可持久化的会话上下文(历史不删,只记切点与摘要)。 */
|
|
395
|
+
function compactionToSessionContext(notice) {
|
|
396
|
+
return {
|
|
397
|
+
summary: notice.summary,
|
|
398
|
+
fromTurnIndex: notice.fromTurnIndex,
|
|
399
|
+
tokensBefore: notice.tokensBefore,
|
|
400
|
+
compactions: notice.compactions,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function cancelPendingResourceSelection(messages) {
|
|
404
|
+
const last = messages?.at(-1);
|
|
405
|
+
if (!messages || last?.role !== "assistant" || last.resourceSelection?.status !== "selecting")
|
|
406
|
+
return messages;
|
|
407
|
+
return [...messages.slice(0, -1), { ...last, completedAt: isoNow(), resourceSelection: {
|
|
408
|
+
...last.resourceSelection, status: "cancelled", label: "本轮自动选择已取消",
|
|
409
|
+
} }];
|
|
410
|
+
}
|
|
379
411
|
function upsertAssistantMessage(messages, turn, complete = false, stampNewTools = true) {
|
|
380
412
|
const msgs = [...(messages ?? [])];
|
|
381
413
|
const last = msgs[msgs.length - 1];
|
|
@@ -383,6 +415,8 @@ function upsertAssistantMessage(messages, turn, complete = false, stampNewTools
|
|
|
383
415
|
const createdAt = (last?.role === "assistant" ? last.createdAt : undefined) ?? turn.createdAt ?? observedAt;
|
|
384
416
|
const next = {
|
|
385
417
|
...turn,
|
|
418
|
+
...(turn.resourceSelection ? { resourceSelection: turn.resourceSelection }
|
|
419
|
+
: last?.role === "assistant" && last.resourceSelection ? { resourceSelection: last.resourceSelection } : {}),
|
|
386
420
|
createdAt,
|
|
387
421
|
content: stampNewToolUseTimes(turn.content, last?.role === "assistant" ? last.content : undefined, observedAt, stampNewTools),
|
|
388
422
|
};
|
|
@@ -401,6 +435,8 @@ export class StructuredSessionManager {
|
|
|
401
435
|
config;
|
|
402
436
|
logger;
|
|
403
437
|
execHost;
|
|
438
|
+
decisionRuntime;
|
|
439
|
+
autoAssignEvaluate;
|
|
404
440
|
sessions = new Map();
|
|
405
441
|
// Both freshly started runners and adopted daemon runs own the same input gate.
|
|
406
442
|
pendingRunnerExecutions = new Map();
|
|
@@ -408,6 +444,11 @@ export class StructuredSessionManager {
|
|
|
408
444
|
// Never persisted or included in session DTOs.
|
|
409
445
|
unacceptedTeamStarts = new Map();
|
|
410
446
|
interruptedWith = new Map();
|
|
447
|
+
/**
|
|
448
|
+
* 「智能分配」正在结算的会话:这段窗口内还没有 runner 归属,新输入必须排队而不是另起一轮。
|
|
449
|
+
* token.cancelled 由 stop / delete 置位,结算回来后不得再启动。
|
|
450
|
+
*/
|
|
451
|
+
autoAssignPending = new Map();
|
|
411
452
|
preserveQueueOnInterrupt = new Set();
|
|
412
453
|
/** Last wall-clock time (ms) a streaming checkpoint reached SQLite. */
|
|
413
454
|
lastStreamSaveAt = new Map();
|
|
@@ -424,6 +465,11 @@ export class StructuredSessionManager {
|
|
|
424
465
|
relayHandlers = new Map();
|
|
425
466
|
emitEvent = null;
|
|
426
467
|
archiveTimer = null;
|
|
468
|
+
/**
|
|
469
|
+
* 静默期心跳:回合在飞且连续 TURN_QUIET_HEARTBEAT_MS 没有任何事件时,用现有的
|
|
470
|
+
* status 事件重发一次权威快照(含最新 turn 锚点)。timer 只在 inFlight 期间存在。
|
|
471
|
+
*/
|
|
472
|
+
turnHeartbeat = new TurnQuietHeartbeat((sessionId) => this.publishQuietHeartbeat(sessionId));
|
|
427
473
|
topicCoordinator = new SessionTopicCoordinator();
|
|
428
474
|
nativeTitles = new SessionNativeTitleTracker();
|
|
429
475
|
streamEmitTimers = new Set();
|
|
@@ -433,6 +479,12 @@ export class StructuredSessionManager {
|
|
|
433
479
|
grokRunner;
|
|
434
480
|
qoderRunner;
|
|
435
481
|
piRunner;
|
|
482
|
+
/** 进程内 core 引擎供员工候选显式启用;普通 Pi 主功能不使用它。 */
|
|
483
|
+
coreRunner;
|
|
484
|
+
/** 宿主是否显式注入了 pi 的 CLI runner / core runner,供系统级引擎状态使用。 */
|
|
485
|
+
piRunnerSupplied;
|
|
486
|
+
coreRunnerSupplied;
|
|
487
|
+
coreRestartDrains = 0;
|
|
436
488
|
geminiRunner;
|
|
437
489
|
/** Structured CLI runs that were mid-flight when the previous web process died. */
|
|
438
490
|
pendingRecoveryIds = [];
|
|
@@ -440,19 +492,28 @@ export class StructuredSessionManager {
|
|
|
440
492
|
detachedRecoveryRetryTimer = null;
|
|
441
493
|
detachedRecoveryFailureLogged = false;
|
|
442
494
|
disposed = false;
|
|
443
|
-
constructor(storage, config, logger = null, runners = {}, execHost, decisionRuntime = () => null
|
|
495
|
+
constructor(storage, config, logger = null, runners = {}, execHost, decisionRuntime = () => null, openRouter,
|
|
496
|
+
/** 「智能分配」专用的本地决策入口:只有真正能跑时才注入,没注入则用系统员工回退。 */
|
|
497
|
+
autoAssignEvaluate = () => undefined) {
|
|
444
498
|
this.storage = storage;
|
|
445
499
|
this.config = config;
|
|
446
500
|
this.logger = logger;
|
|
447
501
|
this.execHost = execHost;
|
|
448
|
-
|
|
502
|
+
this.decisionRuntime = decisionRuntime;
|
|
503
|
+
this.autoAssignEvaluate = autoAssignEvaluate;
|
|
504
|
+
const wrap = (runner) => withModelGroups(withDecisionAccess(runner, storage, decisionRuntime), config);
|
|
449
505
|
this.claudeCliRunner = wrap(runners.claudeCli ?? new ClaudeCliRunner({ language: () => this.config.language }, this.execHost));
|
|
450
506
|
this.codexRunner = wrap(runners.codex ?? new CodexRunner(undefined, this.execHost));
|
|
451
507
|
this.openCodeRunner = wrap(runners.opencode ?? new OpenCodeRunner(undefined, this.execHost));
|
|
452
508
|
this.grokRunner = wrap(runners.grok ?? new GrokRunner(undefined, this.execHost));
|
|
453
509
|
this.qoderRunner = wrap(runners.qoder ?? new QoderRunner(undefined, this.execHost));
|
|
454
|
-
this.piRunner = wrap(runners.pi ?? new PiRunner(undefined, this.execHost));
|
|
510
|
+
this.piRunner = wrap(withAutomaticPiResources(runners.pi ?? new PiRunner(undefined, this.execHost, coreHarnessAgentDir(config.harness), openRouter ? { storage, service: openRouter } : undefined), config, { evaluate: () => this.decisionRuntime()?.evaluate }));
|
|
511
|
+
this.coreRunner = withModelGroups(runners.core ?? new CoreRunner({ config: this.config,
|
|
512
|
+
decisionAccess: decisionRuntime, openRouter }), config);
|
|
513
|
+
this.piRunnerSupplied = runners.pi !== undefined;
|
|
514
|
+
this.coreRunnerSupplied = runners.core !== undefined;
|
|
455
515
|
this.geminiRunner = wrap(runners.gemini ?? new GeminiRunner(undefined, this.execHost));
|
|
516
|
+
// 能力探测放到 server 启动处 await(warmCoreHarness),这里不再另起预热。
|
|
456
517
|
for (const snapshot of this.storage.loadSessions()) {
|
|
457
518
|
if ((snapshot.sessionKind ?? "pty") !== "structured")
|
|
458
519
|
continue;
|
|
@@ -466,7 +527,9 @@ export class StructuredSessionManager {
|
|
|
466
527
|
? storedRunner
|
|
467
528
|
: defaultStructuredRunner(provider);
|
|
468
529
|
const recoverableDetachedRun = snapshot.status === "running"
|
|
469
|
-
&& this.execHost?.persistent === true
|
|
530
|
+
&& this.execHost?.persistent === true
|
|
531
|
+
// 进程内 core 回合不会比服务活得更久;重启后只能算中断,不能去 daemon 里领养。
|
|
532
|
+
&& snapshot.structuredState?.engine !== "core";
|
|
470
533
|
if (recoverableDetachedRun)
|
|
471
534
|
this.pendingRecoveryIds.push(snapshot.id);
|
|
472
535
|
const restored = {
|
|
@@ -483,6 +546,8 @@ export class StructuredSessionManager {
|
|
|
483
546
|
pendingEscalation: null,
|
|
484
547
|
permissionBlocked: false,
|
|
485
548
|
structuredState: {
|
|
549
|
+
// 保留既有可读投影(engine / contextUsage / compactions / phase),只覆盖受管字段。
|
|
550
|
+
...(snapshot.structuredState ?? {}),
|
|
486
551
|
provider,
|
|
487
552
|
runner,
|
|
488
553
|
model: snapshot.structuredState?.model ?? snapshot.selectedModel ?? undefined,
|
|
@@ -493,6 +558,11 @@ export class StructuredSessionManager {
|
|
|
493
558
|
activeRequestId: recoverableDetachedRun
|
|
494
559
|
? snapshot.structuredState?.activeRequestId ?? `recover-pending-${snapshot.id}`
|
|
495
560
|
: null,
|
|
561
|
+
// 锚点跟着真实运行事实走:还能被 terminald 领养的回合保留重启前那一轮的起点
|
|
562
|
+
// (attach 成功后由 adoptTurn 重新挂静默心跳);服务重启导致中断的回合降级为
|
|
563
|
+
// idle,锚点一并清 null,不伪造「还在跑」。
|
|
564
|
+
turnStartedAt: recoverableDetachedRun ? snapshot.structuredState?.turnStartedAt ?? null : null,
|
|
565
|
+
lastActivityAt: recoverableDetachedRun ? snapshot.structuredState?.lastActivityAt ?? null : null,
|
|
496
566
|
},
|
|
497
567
|
selectedModel: snapshot.selectedModel ?? null,
|
|
498
568
|
titleGenerating: false,
|
|
@@ -534,12 +604,51 @@ export class StructuredSessionManager {
|
|
|
534
604
|
return;
|
|
535
605
|
this.emitEvent = emitEvent;
|
|
536
606
|
}
|
|
607
|
+
getCoreTurnStatus() {
|
|
608
|
+
const ids = new Set(CoreTurnTracker.getActiveTurnIds());
|
|
609
|
+
// Include manager-owned executions through their final durable checkpoint.
|
|
610
|
+
for (const [id] of this.pendingRunnerExecutions) {
|
|
611
|
+
if (this.sessions.get(id)?.structuredState?.engine === "core")
|
|
612
|
+
ids.add(id);
|
|
613
|
+
}
|
|
614
|
+
const activeTurnCount = Math.max(CoreTurnTracker.getActiveTurnCount(), ids.size);
|
|
615
|
+
return { hasActiveTurns: activeTurnCount > 0, activeTurnCount, activeTurnIds: [...ids] };
|
|
616
|
+
}
|
|
617
|
+
/** Keep current turns running; new Pi inputs stay in the durable input queue. */
|
|
618
|
+
beginCoreRestartDrain() {
|
|
619
|
+
if (this.disposed)
|
|
620
|
+
throw new Error("StructuredSessionManager has been disposed.");
|
|
621
|
+
this.coreRestartDrains += 1;
|
|
622
|
+
let released = false;
|
|
623
|
+
return () => {
|
|
624
|
+
if (released)
|
|
625
|
+
return;
|
|
626
|
+
released = true;
|
|
627
|
+
this.coreRestartDrains -= 1;
|
|
628
|
+
if (this.coreRestartDrains === 0)
|
|
629
|
+
this.resumeQueuedPiMessages();
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/** Resume accepted inputs after a cancelled drain or after a successful restart. */
|
|
633
|
+
resumeQueuedPiMessages() {
|
|
634
|
+
if (this.disposed || this.coreRestartDrains > 0)
|
|
635
|
+
return;
|
|
636
|
+
for (const session of this.sessions.values()) {
|
|
637
|
+
if (session.provider === "pi" && session.status === "idle"
|
|
638
|
+
&& !session.structuredState?.inFlight && !session.structuredState?.lastError
|
|
639
|
+
&& (session.queuedMessages?.length ?? 0) > 0) {
|
|
640
|
+
setImmediate(() => { void this.flushNextQueuedMessage(session.id); });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
537
644
|
/** Stop every runner and flush terminal state before storage is closed. */
|
|
538
645
|
dispose() {
|
|
539
646
|
if (this.disposed)
|
|
540
647
|
return;
|
|
541
648
|
this.disposed = true;
|
|
542
649
|
this.unacceptedTeamStarts.clear();
|
|
650
|
+
// 静默心跳不跨服务关闭存活(会话级 timer 不允许泄漏)。
|
|
651
|
+
this.turnHeartbeat.dispose();
|
|
543
652
|
if (this.archiveTimer) {
|
|
544
653
|
clearInterval(this.archiveTimer);
|
|
545
654
|
this.archiveTimer = null;
|
|
@@ -568,13 +677,14 @@ export class StructuredSessionManager {
|
|
|
568
677
|
continue;
|
|
569
678
|
const cancelled = {
|
|
570
679
|
...session,
|
|
680
|
+
messages: cancelPendingResourceSelection(session.messages),
|
|
571
681
|
status: "idle",
|
|
572
682
|
exitCode: null,
|
|
573
683
|
endedAt: null,
|
|
574
684
|
pendingEscalation: null,
|
|
575
685
|
permissionBlocked: false,
|
|
576
686
|
structuredState: {
|
|
577
|
-
...(session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
687
|
+
...this.settleTurn(id, session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
578
688
|
inFlight: false,
|
|
579
689
|
activeRequestId: null,
|
|
580
690
|
lastError: null,
|
|
@@ -594,6 +704,9 @@ export class StructuredSessionManager {
|
|
|
594
704
|
this.pendingRunnerExecutions.clear();
|
|
595
705
|
this.interruptedWith.clear();
|
|
596
706
|
this.preserveQueueOnInterrupt.clear();
|
|
707
|
+
for (const token of this.autoAssignPending.values())
|
|
708
|
+
token.cancelled = true;
|
|
709
|
+
this.autoAssignPending.clear();
|
|
597
710
|
for (const timer of this.streamCheckpointTimers.values())
|
|
598
711
|
clearTimeout(timer);
|
|
599
712
|
this.streamCheckpointTimers.clear();
|
|
@@ -663,7 +776,7 @@ export class StructuredSessionManager {
|
|
|
663
776
|
exitCode: null,
|
|
664
777
|
endedAt: null,
|
|
665
778
|
structuredState: {
|
|
666
|
-
...session.structuredState,
|
|
779
|
+
...this.settleTurn(sessionId, session.structuredState),
|
|
667
780
|
inFlight: false,
|
|
668
781
|
activeRequestId: null,
|
|
669
782
|
lastError: "服务重启,上一轮已中断。",
|
|
@@ -728,12 +841,12 @@ export class StructuredSessionManager {
|
|
|
728
841
|
status: "running",
|
|
729
842
|
exitCode: null,
|
|
730
843
|
endedAt: null,
|
|
731
|
-
structuredState: {
|
|
844
|
+
structuredState: this.adoptTurn(sessionId, {
|
|
732
845
|
...snapshot.structuredState,
|
|
733
846
|
inFlight: true,
|
|
734
847
|
activeRequestId: requestId,
|
|
735
848
|
lastError: null,
|
|
736
|
-
},
|
|
849
|
+
}),
|
|
737
850
|
};
|
|
738
851
|
this.sessions.set(sessionId, resumed);
|
|
739
852
|
this.saveAuthoritativeSession(resumed);
|
|
@@ -760,11 +873,11 @@ export class StructuredSessionManager {
|
|
|
760
873
|
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
761
874
|
if (!current)
|
|
762
875
|
return;
|
|
763
|
-
const structuredState = {
|
|
876
|
+
const structuredState = this.noteTurnActivity(sessionId, {
|
|
764
877
|
...current.structuredState,
|
|
765
878
|
model: turnState.model ?? current.structuredState?.model,
|
|
766
879
|
phase: turnState.phase ?? current.structuredState?.phase,
|
|
767
|
-
};
|
|
880
|
+
});
|
|
768
881
|
if (replayTruncated) {
|
|
769
882
|
// 锚点未定前(首轮 feed 中)只有半截 blocks,先只落元数据。
|
|
770
883
|
if (!replayView.ready) {
|
|
@@ -829,7 +942,7 @@ export class StructuredSessionManager {
|
|
|
829
942
|
let lastStdoutSeq = initialState.stdoutSeq;
|
|
830
943
|
let lastStderrSeq = initialState.stderrSeq;
|
|
831
944
|
const feedLine = (line) => {
|
|
832
|
-
onApplied(processor.feed(line));
|
|
945
|
+
onApplied(processor.feed(line, replayingHistory ? undefined : new Date().toISOString()));
|
|
833
946
|
};
|
|
834
947
|
const feedDelta = (text) => {
|
|
835
948
|
carry += text;
|
|
@@ -988,7 +1101,7 @@ export class StructuredSessionManager {
|
|
|
988
1101
|
pendingEscalation: null,
|
|
989
1102
|
permissionBlocked: false,
|
|
990
1103
|
structuredState: {
|
|
991
|
-
...current.structuredState,
|
|
1104
|
+
...this.settleTurn(sessionId, current.structuredState),
|
|
992
1105
|
model: processor.state.model ?? current.structuredState?.model,
|
|
993
1106
|
inFlight: false,
|
|
994
1107
|
activeRequestId: null,
|
|
@@ -1125,9 +1238,11 @@ export class StructuredSessionManager {
|
|
|
1125
1238
|
this.emitStructuredSnapshot(updated);
|
|
1126
1239
|
return updated;
|
|
1127
1240
|
}
|
|
1128
|
-
/**
|
|
1241
|
+
/** 归档会先停掉还在跑的回合,会话 ID 和 provider session id 都留着。 */
|
|
1129
1242
|
setSessionArchived(id, archived) {
|
|
1130
|
-
const current = this.requireSession(id)
|
|
1243
|
+
const current = archived && this.requireSession(id).status === "running"
|
|
1244
|
+
? this.stop(id)
|
|
1245
|
+
: this.requireSession(id);
|
|
1131
1246
|
const archivedAt = archived ? new Date().toISOString() : null;
|
|
1132
1247
|
const updated = { ...current, archived, archivedAt };
|
|
1133
1248
|
this.sessions.set(id, updated);
|
|
@@ -1246,10 +1361,22 @@ export class StructuredSessionManager {
|
|
|
1246
1361
|
: null;
|
|
1247
1362
|
const selectedModel = options.model?.trim() || null;
|
|
1248
1363
|
const initialThinkingEffort = normalizeThinkingEffort(options.thinkingEffort);
|
|
1364
|
+
const autoCompaction = this.config.harness?.compaction?.enabled ?? true;
|
|
1365
|
+
// 员工 SDK 会话不静默继承用户的全局扩展,也不继承 CLI 专属的资源/CodeMode 默认。
|
|
1366
|
+
const sdkEmployeeSession = provider === "pi"
|
|
1367
|
+
&& options.employeeCandidates?.[options.employeeCandidateIndex ?? 0]?.engine === "sdk";
|
|
1368
|
+
// CLI Pi 会话从「上次设置」起步:用户在任一 Pi 会话改过的自动选择 / CodeMode / 资源选择
|
|
1369
|
+
// 就是新会话的默认,不需要每开一个会话重配一次。autoCompaction 始终取当前配置。
|
|
1370
|
+
const rememberedPiSettings = provider === "pi" && !sdkEmployeeSession ? this.storage.getPiSessionDefaults() : null;
|
|
1249
1371
|
const snapshot = {
|
|
1250
1372
|
id,
|
|
1373
|
+
// Pi 会话从创建起就带着自己的工具/功能安排:面板显示的设置与首轮真实启动参数必须一致。
|
|
1374
|
+
...(provider === "pi" ? { piSettings: sdkEmployeeSession ? defaultPiSessionSettings(autoCompaction)
|
|
1375
|
+
: rememberedPiSettings ? { ...rememberedPiSettings, autoCompaction }
|
|
1376
|
+
: defaultPiCliSessionSettings(autoCompaction) } : {}),
|
|
1251
1377
|
sessionKind: "structured",
|
|
1252
1378
|
sessionSource: options.sessionSource ?? "interactive",
|
|
1379
|
+
title: options.title?.trim() || undefined,
|
|
1253
1380
|
automationId: options.automationId,
|
|
1254
1381
|
employeeId: options.employeeId,
|
|
1255
1382
|
employeeName: options.employeeName,
|
|
@@ -1297,6 +1424,9 @@ export class StructuredSessionManager {
|
|
|
1297
1424
|
inFlight: false,
|
|
1298
1425
|
activeRequestId: null,
|
|
1299
1426
|
lastError: null,
|
|
1427
|
+
// 新会话还没有回合在飞:锚点显式为 null,客户端不会看到「已运行 N 分钟」。
|
|
1428
|
+
turnStartedAt: null,
|
|
1429
|
+
lastActivityAt: null,
|
|
1300
1430
|
},
|
|
1301
1431
|
autoRecovered: false,
|
|
1302
1432
|
autoApprovePermissions: shouldAutoApproveForMode(options.mode),
|
|
@@ -1339,40 +1469,54 @@ export class StructuredSessionManager {
|
|
|
1339
1469
|
return session.structuredState?.activeRequestId
|
|
1340
1470
|
?? (session.status === "failed" && !session.structuredState?.inFlight ? failed?.requestId ?? null : null);
|
|
1341
1471
|
}
|
|
1342
|
-
/**
|
|
1472
|
+
/** Compatibility boolean API; typed consumers preserve the reason without parsing user-facing text. */
|
|
1343
1473
|
consumeUnacceptedTeamStartup(id, requestId) {
|
|
1474
|
+
return this.consumeUnacceptedTeamFailure(id, requestId) !== null;
|
|
1475
|
+
}
|
|
1476
|
+
/** Consume only the exact failed request observed by dispatch, once, before a newer request can replace it. */
|
|
1477
|
+
consumeUnacceptedTeamFailure(id, requestId) {
|
|
1344
1478
|
const fact = this.unacceptedTeamStarts.get(id);
|
|
1345
1479
|
if (!fact || fact.requestId !== requestId)
|
|
1346
|
-
return
|
|
1480
|
+
return null;
|
|
1347
1481
|
this.unacceptedTeamStarts.delete(id);
|
|
1348
1482
|
const session = this.sessions.get(id);
|
|
1349
1483
|
return !this.disposed && session?.status === "failed"
|
|
1350
|
-
&& !session.structuredState?.activeRequestId && !session.structuredState?.inFlight;
|
|
1484
|
+
&& !session.structuredState?.activeRequestId && !session.structuredState?.inFlight ? fact.failure : null;
|
|
1485
|
+
}
|
|
1486
|
+
employeeCandidateModel(agent) {
|
|
1487
|
+
return agent.model === "default"
|
|
1488
|
+
? defaultModelGroupSelector(this.config.modelGroups, agent.provider, getDefaultModelForProvider(this.config, agent.provider))
|
|
1489
|
+
|| getDefaultModelForProvider(this.config, agent.provider) || null : agent.model;
|
|
1351
1490
|
}
|
|
1352
|
-
/** Only an unstarted employee conversation may
|
|
1491
|
+
/** Only an unstarted employee conversation may replay its explicitly rejected first input. */
|
|
1353
1492
|
retryEmployeeCandidate(id, original, prompt, error) {
|
|
1354
1493
|
// Team runs own candidate scheduling; keep the snapshot without a second retry loop.
|
|
1355
|
-
if (original.automationId?.startsWith("ai-team:"))
|
|
1494
|
+
if (original.automationId?.startsWith("ai-team:") || !(error instanceof UnacceptedStructuredInputError))
|
|
1356
1495
|
return null;
|
|
1357
1496
|
const candidates = original.employeeCandidates ?? [];
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1497
|
+
const index = original.employeeCandidateIndex ?? 0;
|
|
1498
|
+
const nextIndex = index + 1;
|
|
1499
|
+
if (!original.employeeId || (original.messages?.length ?? 0) !== 0 || original.claudeSessionId
|
|
1500
|
+
|| !error.failure.retryable || nextIndex >= candidates.length)
|
|
1360
1501
|
return null;
|
|
1361
|
-
|
|
1362
|
-
|
|
1502
|
+
const selected = candidates[index];
|
|
1503
|
+
// Explicit tool/model changes, including changes during the pending request, remain authoritative.
|
|
1504
|
+
if (!selected || original.provider !== selected.provider
|
|
1505
|
+
|| (original.selectedModel && original.selectedModel !== this.employeeCandidateModel(selected)))
|
|
1363
1506
|
return null;
|
|
1364
1507
|
const current = this.sessions.get(id);
|
|
1365
|
-
if (!current ||
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
return null;
|
|
1369
|
-
if (!(error instanceof UnacceptedStructuredSpawnError))
|
|
1508
|
+
if (!current || current.status !== "running" || current.provider !== original.provider
|
|
1509
|
+
|| current.selectedModel !== original.selectedModel || current.mode !== original.mode
|
|
1510
|
+
|| current.piSettings !== original.piSettings)
|
|
1370
1511
|
return null;
|
|
1371
1512
|
const next = candidates[nextIndex];
|
|
1372
1513
|
const runner = resolveStructuredRunner(next.provider, undefined);
|
|
1373
|
-
const model = next
|
|
1374
|
-
|
|
1375
|
-
|
|
1514
|
+
const model = this.employeeCandidateModel(next);
|
|
1515
|
+
const sameTool = next.provider === original.provider && (next.engine ?? "cli") === (selected.engine ?? "cli");
|
|
1516
|
+
const piSettings = next.provider !== "pi" ? undefined : sameTool ? current.piSettings
|
|
1517
|
+
: next.engine === "sdk" ? defaultPiSessionSettings(this.config.harness?.compaction?.enabled ?? true)
|
|
1518
|
+
: { ...(this.storage.getPiSessionDefaults() ?? defaultPiCliSessionSettings()),
|
|
1519
|
+
autoCompaction: this.config.harness?.compaction?.enabled ?? true };
|
|
1376
1520
|
const retry = {
|
|
1377
1521
|
...current,
|
|
1378
1522
|
provider: next.provider,
|
|
@@ -1383,8 +1527,12 @@ export class StructuredSessionManager {
|
|
|
1383
1527
|
selectedModel: model,
|
|
1384
1528
|
thinkingEffort: next.thinkingEffort,
|
|
1385
1529
|
employeeCandidateIndex: nextIndex,
|
|
1530
|
+
piSettings,
|
|
1386
1531
|
claudeSessionId: null,
|
|
1532
|
+
harnessContext: undefined,
|
|
1533
|
+
harnessExtensionState: undefined,
|
|
1387
1534
|
messages: [],
|
|
1535
|
+
output: "",
|
|
1388
1536
|
status: "idle",
|
|
1389
1537
|
exitCode: null,
|
|
1390
1538
|
endedAt: null,
|
|
@@ -1393,11 +1541,24 @@ export class StructuredSessionManager {
|
|
|
1393
1541
|
model: model ?? undefined,
|
|
1394
1542
|
},
|
|
1395
1543
|
};
|
|
1544
|
+
this.logger?.appendStructuredSpawn(id, { kind: "candidate-fallback", failureKind: error.failure.kind,
|
|
1545
|
+
fromProvider: original.provider, toProvider: next.provider, candidateIndex: nextIndex });
|
|
1396
1546
|
this.sessions.set(id, retry);
|
|
1397
1547
|
this.saveAuthoritativeSession(retry);
|
|
1398
|
-
|
|
1548
|
+
// sendMessage publishes the new running snapshot synchronously; do not expose a transient idle slot.
|
|
1399
1549
|
return this.sendMessage(id, prompt);
|
|
1400
1550
|
}
|
|
1551
|
+
throwUnacceptedInput(sessionId, result, command, missingHint) {
|
|
1552
|
+
const failure = result.failure ?? classifyStructuredFailure(result);
|
|
1553
|
+
if (!failure?.retryable || this.interruptedWith.has(sessionId))
|
|
1554
|
+
return;
|
|
1555
|
+
const message = result.spawnError
|
|
1556
|
+
? `${command} 启动失败:${result.spawnError.message}${result.spawnError.code === "ENOENT" ? missingHint : ""}`
|
|
1557
|
+
: result.primaryError?.trim() || `${command} 在执行前拒绝了输入。`;
|
|
1558
|
+
this.logger?.appendStructuredSpawn(sessionId, { kind: "structured-input-rejected", command,
|
|
1559
|
+
exitCode: result.exitCode, failureKind: failure.kind });
|
|
1560
|
+
throw new UnacceptedStructuredInputError(message, failure, result.state);
|
|
1561
|
+
}
|
|
1401
1562
|
/** 往转发会话里追加别的参与者的发言。 */
|
|
1402
1563
|
appendRelayTurns(id, turns) {
|
|
1403
1564
|
const session = this.sessions.get(id);
|
|
@@ -1453,7 +1614,11 @@ export class StructuredSessionManager {
|
|
|
1453
1614
|
}
|
|
1454
1615
|
}
|
|
1455
1616
|
}
|
|
1617
|
+
// Pi 主功能现在固定走 CLI;core SDK 重启 drain 不影响 CLI 会话入场。
|
|
1456
1618
|
this.maybeGenerateSessionTopic(id, prompt);
|
|
1619
|
+
// 「智能分配」结算窗口:还没有 runner 归属,新输入先排队,不能另起一轮。
|
|
1620
|
+
if (this.autoAssignPending.has(id))
|
|
1621
|
+
return this.enqueueInput(session, prompt);
|
|
1457
1622
|
if (session.structuredState?.inFlight) {
|
|
1458
1623
|
const runnerExecution = this.pendingRunnerExecutions.get(id);
|
|
1459
1624
|
// interrupt() only requests cancellation; completion can settle later.
|
|
@@ -1465,7 +1630,7 @@ export class StructuredSessionManager {
|
|
|
1465
1630
|
status: "idle",
|
|
1466
1631
|
endedAt: session.endedAt ?? new Date().toISOString(),
|
|
1467
1632
|
structuredState: {
|
|
1468
|
-
...session.structuredState,
|
|
1633
|
+
...this.settleTurn(id, session.structuredState),
|
|
1469
1634
|
inFlight: false,
|
|
1470
1635
|
activeRequestId: null,
|
|
1471
1636
|
},
|
|
@@ -1503,23 +1668,60 @@ export class StructuredSessionManager {
|
|
|
1503
1668
|
return session;
|
|
1504
1669
|
}
|
|
1505
1670
|
else {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1671
|
+
return this.enqueueInput(session, prompt);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
// 「智能分配」:只有真拿到这一轮提示词才能结算成本次使用的分组。结算后钉在会话上,
|
|
1675
|
+
// 后续轮次用同一个分组(不在会话中途换模型);客户端选择器同步显示实际分组。
|
|
1676
|
+
if (isAutoAssignSelector(session.selectedModel)) {
|
|
1677
|
+
const token = { cancelled: false };
|
|
1678
|
+
this.autoAssignPending.set(id, token);
|
|
1679
|
+
let assigned;
|
|
1680
|
+
try {
|
|
1681
|
+
assigned = await resolveAutoAssign({
|
|
1682
|
+
provider: session.provider ?? "claude",
|
|
1683
|
+
prompt,
|
|
1684
|
+
groups: this.config.modelGroups,
|
|
1685
|
+
evaluate: this.autoAssignEvaluate(),
|
|
1686
|
+
ai: resolveSystemAiContext(session, this.config, this.storage.getSystemSiliconEmployee()),
|
|
1687
|
+
cwd: session.cwd,
|
|
1688
|
+
language: this.config.language,
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
catch (error) {
|
|
1692
|
+
// 结算本身绝不阻断发送:拿不到结论就按默认分组继续。
|
|
1693
|
+
console.warn("[AutoAssign] 分配分组失败,改用默认分组:", getErrorMessage(error));
|
|
1694
|
+
assigned = { selector: "", group: null, strategy: "default", calls: 0 };
|
|
1695
|
+
}
|
|
1696
|
+
finally {
|
|
1697
|
+
if (this.autoAssignPending.get(id) === token)
|
|
1698
|
+
this.autoAssignPending.delete(id);
|
|
1699
|
+
}
|
|
1700
|
+
const latest = this.sessions.get(id);
|
|
1701
|
+
if (!latest || token.cancelled || this.disposed)
|
|
1702
|
+
return latest ?? session;
|
|
1703
|
+
session = latest;
|
|
1704
|
+
// 结算期间用户可能自己换了模型;那是更强的意愿,不覆盖。
|
|
1705
|
+
if (isAutoAssignSelector(session.selectedModel)) {
|
|
1706
|
+
const resolved = {
|
|
1516
1707
|
...session,
|
|
1517
|
-
|
|
1708
|
+
selectedModel: assigned.selector || null,
|
|
1709
|
+
structuredState: {
|
|
1710
|
+
...(session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
1711
|
+
...(assigned.selector ? { model: assigned.selector } : {}),
|
|
1712
|
+
},
|
|
1518
1713
|
};
|
|
1519
|
-
this.sessions.set(id,
|
|
1520
|
-
this.storage.updateSessionRuntimeMetadata(
|
|
1521
|
-
this.
|
|
1522
|
-
|
|
1714
|
+
this.sessions.set(id, resolved);
|
|
1715
|
+
this.storage.updateSessionRuntimeMetadata(resolved);
|
|
1716
|
+
this.emit({
|
|
1717
|
+
type: "status",
|
|
1718
|
+
sessionId: id,
|
|
1719
|
+
data: { sessionKind: "structured", selectedModel: resolved.selectedModel, structuredState: resolved.structuredState },
|
|
1720
|
+
});
|
|
1721
|
+
session = resolved;
|
|
1722
|
+
if (assigned.group) {
|
|
1723
|
+
console.info(`[AutoAssign] ${id} → 分组「${assigned.group.name}」(${assigned.strategy}${assigned.probability === undefined ? "" : `, p=${assigned.probability.toFixed(2)}`})`);
|
|
1724
|
+
}
|
|
1523
1725
|
}
|
|
1524
1726
|
}
|
|
1525
1727
|
// 检测上一轮 assistant 是否有未配对的 AskUserQuestion tool_use(说明前一次
|
|
@@ -1557,12 +1759,12 @@ export class StructuredSessionManager {
|
|
|
1557
1759
|
exitCode: null,
|
|
1558
1760
|
endedAt: null,
|
|
1559
1761
|
messages: [...(session.messages ?? []), userTurn],
|
|
1560
|
-
structuredState: {
|
|
1762
|
+
structuredState: this.beginTurn(id, {
|
|
1561
1763
|
...(session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
1562
1764
|
inFlight: true,
|
|
1563
1765
|
activeRequestId: requestId,
|
|
1564
1766
|
lastError: null,
|
|
1565
|
-
},
|
|
1767
|
+
}),
|
|
1566
1768
|
};
|
|
1567
1769
|
this.sessions.set(id, updated);
|
|
1568
1770
|
if (updated.automationId?.startsWith("ai-team:"))
|
|
@@ -1609,13 +1811,40 @@ export class StructuredSessionManager {
|
|
|
1609
1811
|
});
|
|
1610
1812
|
}
|
|
1611
1813
|
else if (provider === "pi") {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1814
|
+
const employeeAgent = updated.employeeCandidates?.[updated.employeeCandidateIndex ?? 0];
|
|
1815
|
+
const sdkRequested = employeeAgent?.engine === "sdk";
|
|
1816
|
+
// 对话框默认固定走 CLI;只有员工候选明确配置 SDK 时,员工会话才启用 core。
|
|
1817
|
+
const resolution = sdkRequested
|
|
1818
|
+
? resolveHarnessEngineSync({ ...this.config.harness, engine: "core" }, "pi", {
|
|
1819
|
+
coreRunnerSupplied: this.coreRunnerSupplied,
|
|
1820
|
+
})
|
|
1821
|
+
: { engine: "cli", reason: "Pi 主功能固定使用 CLI JSON" };
|
|
1822
|
+
const enginePatched = {
|
|
1823
|
+
...updated,
|
|
1824
|
+
structuredState: {
|
|
1825
|
+
...updated.structuredState,
|
|
1826
|
+
engine: resolution.engine,
|
|
1827
|
+
engineReason: resolution.reason,
|
|
1828
|
+
},
|
|
1829
|
+
};
|
|
1830
|
+
this.sessions.set(id, enginePatched);
|
|
1831
|
+
if (resolution.engine === "core") {
|
|
1832
|
+
await this.runClaudeStreaming(id, enginePatched, prompt, requestId, {
|
|
1833
|
+
runner: this.coreRunner,
|
|
1834
|
+
provider: "pi",
|
|
1835
|
+
commandLabel: "pi core(员工 SDK)",
|
|
1836
|
+
logKind: "pi-core",
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
await this.runClaudeStreaming(id, enginePatched, prompt, requestId, {
|
|
1841
|
+
runner: this.piRunner,
|
|
1842
|
+
provider: "pi",
|
|
1843
|
+
commandLabel: "pi --mode json --print",
|
|
1844
|
+
logKind: "pi-json",
|
|
1845
|
+
installHint: "请安装 @earendil-works/pi-coding-agent(或兼容的 Pi CLI),或重跑 `wand service:install` 刷新服务的 PATH",
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1619
1848
|
}
|
|
1620
1849
|
else if (provider === "gemini") {
|
|
1621
1850
|
await this.runClaudeStreaming(id, updated, prompt, requestId, {
|
|
@@ -1637,12 +1866,8 @@ export class StructuredSessionManager {
|
|
|
1637
1866
|
// Close handlers use this tagged error after they have already persisted
|
|
1638
1867
|
// the detailed failure. Re-throw even if an ended-event listener removed
|
|
1639
1868
|
// the session synchronously; there is no request-id marker to leak.
|
|
1640
|
-
if (error instanceof PersistedStructuredRunnerError)
|
|
1641
|
-
const retry = this.retryEmployeeCandidate(id, session, prompt, error);
|
|
1642
|
-
if (retry)
|
|
1643
|
-
return await retry;
|
|
1869
|
+
if (error instanceof PersistedStructuredRunnerError)
|
|
1644
1870
|
throw error;
|
|
1645
|
-
}
|
|
1646
1871
|
const current = this.sessions.get(id);
|
|
1647
1872
|
if (!current)
|
|
1648
1873
|
throw error;
|
|
@@ -1654,29 +1879,31 @@ export class StructuredSessionManager {
|
|
|
1654
1879
|
const retry = this.retryEmployeeCandidate(id, session, prompt, error);
|
|
1655
1880
|
if (retry)
|
|
1656
1881
|
return await retry;
|
|
1657
|
-
const failed =
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1882
|
+
const failed = error instanceof UnacceptedStructuredInputError
|
|
1883
|
+
? this.finishStructuredFailure(current, 1, message, error.state)
|
|
1884
|
+
: {
|
|
1885
|
+
...current,
|
|
1886
|
+
status: "failed",
|
|
1887
|
+
exitCode: 1,
|
|
1888
|
+
endedAt: new Date().toISOString(),
|
|
1889
|
+
pendingEscalation: null,
|
|
1890
|
+
permissionBlocked: false,
|
|
1891
|
+
structuredState: {
|
|
1892
|
+
...this.settleTurn(id, current.structuredState),
|
|
1893
|
+
inFlight: false,
|
|
1894
|
+
activeRequestId: null,
|
|
1895
|
+
lastError: message,
|
|
1896
|
+
},
|
|
1897
|
+
};
|
|
1671
1898
|
this.sessions.set(id, failed);
|
|
1672
1899
|
this.saveAuthoritativeSession(failed);
|
|
1673
|
-
if (!this.disposed && error instanceof
|
|
1900
|
+
if (!this.disposed && error instanceof UnacceptedStructuredInputError
|
|
1674
1901
|
&& current.automationId?.startsWith("ai-team:")) {
|
|
1675
1902
|
// Available before status/ended listeners run. A later turn/stop/delete invalidates this fact.
|
|
1676
1903
|
if (this.unacceptedTeamStarts.size >= 1024) {
|
|
1677
1904
|
this.unacceptedTeamStarts.delete(this.unacceptedTeamStarts.keys().next().value);
|
|
1678
1905
|
}
|
|
1679
|
-
this.unacceptedTeamStarts.set(id, { requestId });
|
|
1906
|
+
this.unacceptedTeamStarts.set(id, { requestId, failure: error.failure });
|
|
1680
1907
|
}
|
|
1681
1908
|
this.emit({
|
|
1682
1909
|
type: "status",
|
|
@@ -1829,7 +2056,7 @@ export class StructuredSessionManager {
|
|
|
1829
2056
|
const candidate = candidateIndex >= 0 ? candidates[candidateIndex] : undefined;
|
|
1830
2057
|
const runner = defaultStructuredRunner(provider);
|
|
1831
2058
|
const model = candidate && candidate.model !== "default" ? candidate.model
|
|
1832
|
-
: getDefaultModelForProvider(this.config, provider) || null;
|
|
2059
|
+
: defaultModelGroupSelector(this.config.modelGroups, provider, getDefaultModelForProvider(this.config, provider)) || getDefaultModelForProvider(this.config, provider) || null;
|
|
1833
2060
|
const effort = normalizeThinkingEffort(candidate?.thinkingEffort ?? this.config.defaultThinkingEffort);
|
|
1834
2061
|
const thinkingEffort = effort?.includes(":") && !effort.startsWith(`${provider}:`) ? "off" : effort;
|
|
1835
2062
|
const requestedMode = candidate?.mode ?? session.mode;
|
|
@@ -1841,6 +2068,11 @@ export class StructuredSessionManager {
|
|
|
1841
2068
|
...session,
|
|
1842
2069
|
provider,
|
|
1843
2070
|
runner,
|
|
2071
|
+
// 从别的 provider 切到 Pi 的空白会话与新建 Pi 会话同等:以「上次设置」起步。
|
|
2072
|
+
...(provider === "pi" && !session.piSettings ? { piSettings: candidate?.engine === "sdk"
|
|
2073
|
+
? defaultPiSessionSettings(this.config.harness?.compaction?.enabled ?? true)
|
|
2074
|
+
: { ...(this.storage.getPiSessionDefaults() ?? defaultPiCliSessionSettings(this.config.harness?.compaction?.enabled ?? true)),
|
|
2075
|
+
autoCompaction: this.config.harness?.compaction?.enabled ?? true } } : {}),
|
|
1844
2076
|
command: recoveredCommandLabel(runner),
|
|
1845
2077
|
mode,
|
|
1846
2078
|
autoApprovePermissions: shouldAutoApproveForMode(mode),
|
|
@@ -1860,10 +2092,74 @@ export class StructuredSessionManager {
|
|
|
1860
2092
|
});
|
|
1861
2093
|
return updated;
|
|
1862
2094
|
}
|
|
2095
|
+
getPiSettings(sessionId) {
|
|
2096
|
+
const session = this.requireSession(sessionId);
|
|
2097
|
+
if (session.provider !== "pi")
|
|
2098
|
+
throw new Error("这些设置仅适用于 Pi 结构化会话。");
|
|
2099
|
+
const employeeAgent = session.employeeCandidates?.[session.employeeCandidateIndex ?? 0];
|
|
2100
|
+
const sdkRequested = employeeAgent?.engine === "sdk";
|
|
2101
|
+
let resolution;
|
|
2102
|
+
if (sdkRequested) {
|
|
2103
|
+
try {
|
|
2104
|
+
resolution = resolveHarnessEngineSync({ ...this.config.harness, engine: "core" }, "pi", {
|
|
2105
|
+
coreRunnerSupplied: this.coreRunnerSupplied,
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
catch (error) {
|
|
2109
|
+
resolution = { engine: "core", reason: getErrorMessage(error) };
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
else {
|
|
2113
|
+
resolution = { engine: "cli", reason: "Pi 主功能固定使用 CLI JSON" };
|
|
2114
|
+
}
|
|
2115
|
+
const defaults = resolution.engine === "cli"
|
|
2116
|
+
? defaultPiCliSessionSettings(this.config.harness?.compaction?.enabled ?? true)
|
|
2117
|
+
: defaultPiSessionSettings(this.config.harness?.compaction?.enabled ?? true);
|
|
2118
|
+
// Legacy sessions really use Pi discovery. Do not report a new empty selection as applied.
|
|
2119
|
+
if (!session.piSettings)
|
|
2120
|
+
delete defaults.resources;
|
|
2121
|
+
return { settings: session.piSettings ?? defaults, resolution };
|
|
2122
|
+
}
|
|
2123
|
+
/** Metadata-only, synchronous merge: active runs retain their captured settings, queued rounds see these. */
|
|
2124
|
+
setPiSettings(sessionId, patch) {
|
|
2125
|
+
const session = this.requireSession(sessionId);
|
|
2126
|
+
if (session.archived)
|
|
2127
|
+
throw new Error("请先恢复已归档的会话。");
|
|
2128
|
+
const { settings, resolution } = this.getPiSettings(sessionId);
|
|
2129
|
+
const updatedSettings = patchPiSessionSettings(settings, patch);
|
|
2130
|
+
if (resolution.engine === "core" && (patch.resources !== undefined
|
|
2131
|
+
|| patch.codemodeOverride !== undefined
|
|
2132
|
+
|| patch.autoResources !== undefined
|
|
2133
|
+
|| patch.lockedSkills !== undefined)) {
|
|
2134
|
+
throw new Error("当前 SDK 候选尚不支持此会话级设置;没有改用全局资源或 CodeMode 配置。");
|
|
2135
|
+
}
|
|
2136
|
+
if (resolution.engine !== "core") {
|
|
2137
|
+
// 写入裁决与启动参数用同一套能力边界:表达不出的字段/取值不落库。
|
|
2138
|
+
const rejection = cliPiSettingsRejection(patch, updatedSettings);
|
|
2139
|
+
if (rejection)
|
|
2140
|
+
throw new Error(rejection);
|
|
2141
|
+
}
|
|
2142
|
+
// Automatic selection or skill locks need an explicit on/off boundary, not legacy discovery.
|
|
2143
|
+
if ((updatedSettings.autoResources || updatedSettings.lockedSkills?.length) && !updatedSettings.resources) {
|
|
2144
|
+
updatedSettings.resources = { skills: [], mcpServers: [] };
|
|
2145
|
+
}
|
|
2146
|
+
const updated = { ...session, piSettings: updatedSettings };
|
|
2147
|
+
// Do not publish a setting that did not reach durable storage.
|
|
2148
|
+
this.storage.updateSessionRuntimeMetadata(updated);
|
|
2149
|
+
// 记住这次设置:新建 Pi CLI 会话以它起步。SDK 会话表达不出资源/CodeMode,不写回默认。
|
|
2150
|
+
if (resolution.engine === "cli")
|
|
2151
|
+
this.storage.setPiSessionDefaults(updatedSettings);
|
|
2152
|
+
this.sessions.set(sessionId, updated);
|
|
2153
|
+
this.emit({ type: "status", sessionId, data: { sessionKind: "structured", piSettings: updated.piSettings } });
|
|
2154
|
+
return updated;
|
|
2155
|
+
}
|
|
1863
2156
|
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
1864
2157
|
setSessionModel(sessionId, model) {
|
|
1865
2158
|
const session = this.requireSession(sessionId);
|
|
1866
2159
|
const normalized = model?.trim() || null;
|
|
2160
|
+
resolveModelGroupModels(this.config.modelGroups, session.provider ?? "claude", normalized, {
|
|
2161
|
+
preferDefault: !normalized || normalized === "default",
|
|
2162
|
+
});
|
|
1867
2163
|
const updated = {
|
|
1868
2164
|
...session,
|
|
1869
2165
|
selectedModel: normalized,
|
|
@@ -2009,6 +2305,12 @@ export class StructuredSessionManager {
|
|
|
2009
2305
|
this.unacceptedTeamStarts.delete(id);
|
|
2010
2306
|
const session = this.requireSession(id);
|
|
2011
2307
|
this.requireRecoveryControl(id);
|
|
2308
|
+
// 结算中的智能分配还没有 runner:置位取消,结算回来后不得再启动。
|
|
2309
|
+
const pendingAssign = this.autoAssignPending.get(id);
|
|
2310
|
+
if (pendingAssign) {
|
|
2311
|
+
pendingAssign.cancelled = true;
|
|
2312
|
+
this.autoAssignPending.delete(id);
|
|
2313
|
+
}
|
|
2012
2314
|
this.interruptedWith.delete(id);
|
|
2013
2315
|
this.preserveQueueOnInterrupt.delete(id);
|
|
2014
2316
|
// Clearing activeRequestId is the generation barrier: late data/close callbacks
|
|
@@ -2017,13 +2319,14 @@ export class StructuredSessionManager {
|
|
|
2017
2319
|
// 这样前端不会进入"会话已结束/恢复会话"终止态,输入框保持可用,直接展示历史内容。
|
|
2018
2320
|
const cancelled = {
|
|
2019
2321
|
...session,
|
|
2322
|
+
messages: cancelPendingResourceSelection(session.messages),
|
|
2020
2323
|
status: "idle",
|
|
2021
2324
|
exitCode: null,
|
|
2022
2325
|
endedAt: null,
|
|
2023
2326
|
pendingEscalation: null,
|
|
2024
2327
|
permissionBlocked: false,
|
|
2025
2328
|
structuredState: {
|
|
2026
|
-
...(session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
2329
|
+
...this.settleTurn(id, session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
|
|
2027
2330
|
inFlight: false,
|
|
2028
2331
|
activeRequestId: null,
|
|
2029
2332
|
lastError: null,
|
|
@@ -2043,6 +2346,11 @@ export class StructuredSessionManager {
|
|
|
2043
2346
|
delete(id) {
|
|
2044
2347
|
this.unacceptedTeamStarts.delete(id);
|
|
2045
2348
|
this.requireRecoveryControl(id);
|
|
2349
|
+
const pendingAssign = this.autoAssignPending.get(id);
|
|
2350
|
+
if (pendingAssign) {
|
|
2351
|
+
pendingAssign.cancelled = true;
|
|
2352
|
+
this.autoAssignPending.delete(id);
|
|
2353
|
+
}
|
|
2046
2354
|
const runnerExecution = this.pendingRunnerExecutions.get(id);
|
|
2047
2355
|
// Invalidate callback ownership before signalling the runner. Cancellation
|
|
2048
2356
|
// can synchronously wake listeners in some adapter implementations.
|
|
@@ -2052,6 +2360,8 @@ export class StructuredSessionManager {
|
|
|
2052
2360
|
this.releasePendingRunnerExecution(id, runnerExecution);
|
|
2053
2361
|
}
|
|
2054
2362
|
this.clearStreamingCheckpoint(id);
|
|
2363
|
+
// 会话被删除后迟到的回合回调必须失效,静默心跳也不能再给一个不存在的会话发事件。
|
|
2364
|
+
this.turnHeartbeat.cancel(id);
|
|
2055
2365
|
this.interruptedWith.delete(id);
|
|
2056
2366
|
this.preserveQueueOnInterrupt.delete(id);
|
|
2057
2367
|
this.storage.deleteSession(id);
|
|
@@ -2069,7 +2379,7 @@ export class StructuredSessionManager {
|
|
|
2069
2379
|
}
|
|
2070
2380
|
/** True only while this exact turn still owns the session's mutable state. */
|
|
2071
2381
|
isCurrentRequest(sessionId, requestId) {
|
|
2072
|
-
return this.sessions.get(sessionId)?.structuredState?.activeRequestId === requestId;
|
|
2382
|
+
return !this.disposed && this.sessions.get(sessionId)?.structuredState?.activeRequestId === requestId;
|
|
2073
2383
|
}
|
|
2074
2384
|
currentSessionForRequest(sessionId, requestId) {
|
|
2075
2385
|
if (!this.isCurrentRequest(sessionId, requestId))
|
|
@@ -2086,6 +2396,67 @@ export class StructuredSessionManager {
|
|
|
2086
2396
|
this.execHost?.forgetRun(structuredRunId(sessionId));
|
|
2087
2397
|
return true;
|
|
2088
2398
|
}
|
|
2399
|
+
/**
|
|
2400
|
+
* 本轮开始:turnStartedAt / lastActivityAt 同时落下,并给这一轮挂上静默心跳。
|
|
2401
|
+
* 只在新回合(新 requestId)起点未知时调用;daemon 领养走 adoptTurn。
|
|
2402
|
+
*/
|
|
2403
|
+
beginTurn(sessionId, state) {
|
|
2404
|
+
const at = isoNow();
|
|
2405
|
+
this.turnHeartbeat.observe(sessionId);
|
|
2406
|
+
return { ...state, turnStartedAt: at, lastActivityAt: at };
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* 领养 daemon 里仍在飞的回合:起点是重启前那一轮的真实时刻,不能改写成重连时刻;
|
|
2410
|
+
* 锚点缺失(改动之前落库的历史数据)才退回现在。
|
|
2411
|
+
*/
|
|
2412
|
+
adoptTurn(sessionId, state) {
|
|
2413
|
+
const at = isoNow();
|
|
2414
|
+
this.turnHeartbeat.observe(sessionId);
|
|
2415
|
+
return { ...state, turnStartedAt: state.turnStartedAt ?? at, lastActivityAt: at };
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* 观测到本轮活动(输出 chunk / 状态变化):只刷新 lastActivityAt 并重新计算静默窗口;
|
|
2419
|
+
* turnStartedAt 保持不变,客户端才算是「这一轮已经跑了多久」而不是「距离上一条输出多久」。
|
|
2420
|
+
*/
|
|
2421
|
+
noteTurnActivity(sessionId, state) {
|
|
2422
|
+
this.turnHeartbeat.observe(sessionId);
|
|
2423
|
+
return { ...state, lastActivityAt: isoNow() };
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* 本轮收敛:`inFlight` 清 false 的一切路径(结束 / 失败 / 停止 / 中断 / 重启降级 /
|
|
2427
|
+
* 候选降级)都必须走这里——两个锚点一起回 null,静默心跳立即 cancel。
|
|
2428
|
+
* 收敛后不存在任何「还在跑」的读数,客户端不需要再猜上一轮的锚点属于哪一轮。
|
|
2429
|
+
*/
|
|
2430
|
+
settleTurn(sessionId, state) {
|
|
2431
|
+
this.turnHeartbeat.cancel(sessionId);
|
|
2432
|
+
return { ...state, turnStartedAt: null, lastActivityAt: null };
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* 静默窗口到期:确认这一轮仍在飞才补发一次,payload 与一条普通 status 快照完全一致
|
|
2436
|
+
* (不带新字段、不开新事件类型)。返回值决定要不要再挂下一个窗口。
|
|
2437
|
+
*/
|
|
2438
|
+
publishQuietHeartbeat(sessionId) {
|
|
2439
|
+
if (this.disposed)
|
|
2440
|
+
return false;
|
|
2441
|
+
const session = this.sessions.get(sessionId);
|
|
2442
|
+
if (session?.structuredState?.inFlight !== true)
|
|
2443
|
+
return false;
|
|
2444
|
+
// 停在权限 / 升级裁决上的回合不是在「安静地干活」:那一屏已经有 pendingEscalation
|
|
2445
|
+
// 这个显式指示,再广播「仍在运行」是错的。保留窗口(返回 true),但不发事件,
|
|
2446
|
+
// 等用户裁决后 runner 继续产出时会自然重新计时。
|
|
2447
|
+
if (session.pendingEscalation || session.permissionBlocked)
|
|
2448
|
+
return true;
|
|
2449
|
+
this.emit({
|
|
2450
|
+
type: "status",
|
|
2451
|
+
sessionId,
|
|
2452
|
+
data: {
|
|
2453
|
+
...buildStructuredOutputPayload(session),
|
|
2454
|
+
status: session.status,
|
|
2455
|
+
exitCode: session.exitCode,
|
|
2456
|
+
},
|
|
2457
|
+
});
|
|
2458
|
+
return true;
|
|
2459
|
+
}
|
|
2089
2460
|
emitStructuredSnapshot(session, eventType = "output") {
|
|
2090
2461
|
// 排队消息只通过 payload.queuedMessages 单独下发,由各端在消息卡片外的「排队条」
|
|
2091
2462
|
// 里纵向渲染——绝不再把它们当成 __queued 占位 turn 混进 messages 消息流里,否则会
|
|
@@ -2102,6 +2473,21 @@ export class StructuredSessionManager {
|
|
|
2102
2473
|
data,
|
|
2103
2474
|
});
|
|
2104
2475
|
}
|
|
2476
|
+
enqueueInput(session, prompt) {
|
|
2477
|
+
if (isDuplicateStructuredQueueInput(session, prompt)) {
|
|
2478
|
+
const err = new Error("与上一条消息相同,已忽略,不会加入排队。");
|
|
2479
|
+
err.code = "duplicate_queued_message";
|
|
2480
|
+
throw err;
|
|
2481
|
+
}
|
|
2482
|
+
const queue = session.queuedMessages ?? [];
|
|
2483
|
+
if (queue.length >= 10)
|
|
2484
|
+
throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
|
|
2485
|
+
const queued = { ...session, queuedMessages: [...queue, prompt] };
|
|
2486
|
+
this.sessions.set(session.id, queued);
|
|
2487
|
+
this.storage.updateSessionRuntimeMetadata(queued);
|
|
2488
|
+
this.emitStructuredSnapshot(queued);
|
|
2489
|
+
return queued;
|
|
2490
|
+
}
|
|
2105
2491
|
async flushNextQueuedMessage(sessionId) {
|
|
2106
2492
|
if (this.disposed)
|
|
2107
2493
|
return;
|
|
@@ -2181,10 +2567,10 @@ export class StructuredSessionManager {
|
|
|
2181
2567
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2182
2568
|
messages,
|
|
2183
2569
|
output: turnState.result || current.output,
|
|
2184
|
-
structuredState: {
|
|
2570
|
+
structuredState: this.noteTurnActivity(sessionId, {
|
|
2185
2571
|
...current.structuredState,
|
|
2186
2572
|
model: turnState.model ?? current.structuredState?.model,
|
|
2187
|
-
},
|
|
2573
|
+
}),
|
|
2188
2574
|
};
|
|
2189
2575
|
this.sessions.set(sessionId, patched);
|
|
2190
2576
|
this.saveStreamingSnapshot(patched);
|
|
@@ -2248,12 +2634,7 @@ export class StructuredSessionManager {
|
|
|
2248
2634
|
return;
|
|
2249
2635
|
}
|
|
2250
2636
|
flushEmit();
|
|
2251
|
-
|
|
2252
|
-
const hint = result.spawnError.code === "ENOENT"
|
|
2253
|
-
? "(PATH 中找不到 codex 可执行文件;请确认 codex 已安装,或重跑 `wand service:install` 刷新服务的 PATH)"
|
|
2254
|
-
: "";
|
|
2255
|
-
throw new UnacceptedStructuredSpawnError(`codex exec 启动失败:${result.spawnError.message}${hint}`);
|
|
2256
|
-
}
|
|
2637
|
+
this.throwUnacceptedInput(sessionId, result, "codex exec", "(PATH 中找不到 codex 可执行文件;请确认 codex 已安装,或重跑 `wand service:install` 刷新服务的 PATH)");
|
|
2257
2638
|
this.logger?.appendStructuredSpawn(sessionId, {
|
|
2258
2639
|
kind: "codex-exec-close",
|
|
2259
2640
|
pid: execution.pid,
|
|
@@ -2292,7 +2673,7 @@ export class StructuredSessionManager {
|
|
|
2292
2673
|
pendingEscalation: null,
|
|
2293
2674
|
permissionBlocked: false,
|
|
2294
2675
|
structuredState: {
|
|
2295
|
-
...current.structuredState,
|
|
2676
|
+
...this.settleTurn(sessionId, current.structuredState),
|
|
2296
2677
|
model: result.state.model ?? current.structuredState?.model,
|
|
2297
2678
|
inFlight: false,
|
|
2298
2679
|
activeRequestId: null,
|
|
@@ -2333,6 +2714,8 @@ export class StructuredSessionManager {
|
|
|
2333
2714
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2334
2715
|
messages,
|
|
2335
2716
|
output: turnState.result || current.output,
|
|
2717
|
+
// 每个 provider 帧都是本轮还在活动的证据:刷新 lastActivityAt 并重挂静默窗口。
|
|
2718
|
+
structuredState: this.noteTurnActivity(sessionId, current.structuredState),
|
|
2336
2719
|
};
|
|
2337
2720
|
this.sessions.set(sessionId, patched);
|
|
2338
2721
|
this.saveStreamingSnapshot(patched);
|
|
@@ -2391,12 +2774,7 @@ export class StructuredSessionManager {
|
|
|
2391
2774
|
return;
|
|
2392
2775
|
}
|
|
2393
2776
|
flushEmit();
|
|
2394
|
-
|
|
2395
|
-
const hint = result.spawnError.code === "ENOENT"
|
|
2396
|
-
? "(PATH 中找不到 grok;请安装 Grok Build CLI,或重跑 `wand service:install` 刷新服务 PATH)"
|
|
2397
|
-
: "";
|
|
2398
|
-
throw new UnacceptedStructuredSpawnError(`grok 启动失败:${result.spawnError.message}${hint}`);
|
|
2399
|
-
}
|
|
2777
|
+
this.throwUnacceptedInput(sessionId, result, "grok", "(PATH 中找不到 grok;请安装 Grok Build CLI,或重跑 `wand service:install` 刷新服务 PATH)");
|
|
2400
2778
|
this.logger?.appendStructuredSpawn(sessionId, {
|
|
2401
2779
|
kind: "grok-headless-close",
|
|
2402
2780
|
pid: execution.pid,
|
|
@@ -2434,7 +2812,7 @@ export class StructuredSessionManager {
|
|
|
2434
2812
|
pendingEscalation: null,
|
|
2435
2813
|
permissionBlocked: false,
|
|
2436
2814
|
structuredState: {
|
|
2437
|
-
...current.structuredState,
|
|
2815
|
+
...this.settleTurn(sessionId, current.structuredState),
|
|
2438
2816
|
model: result.state.model ?? current.structuredState?.model,
|
|
2439
2817
|
inFlight: false,
|
|
2440
2818
|
activeRequestId: null,
|
|
@@ -2473,6 +2851,8 @@ export class StructuredSessionManager {
|
|
|
2473
2851
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2474
2852
|
messages,
|
|
2475
2853
|
output: turnState.result || current.output,
|
|
2854
|
+
// 每个 provider 帧都是本轮还在活动的证据:刷新 lastActivityAt 并重挂静默窗口。
|
|
2855
|
+
structuredState: this.noteTurnActivity(sessionId, current.structuredState),
|
|
2476
2856
|
};
|
|
2477
2857
|
this.sessions.set(sessionId, patched);
|
|
2478
2858
|
this.saveStreamingSnapshot(patched);
|
|
@@ -2536,12 +2916,7 @@ export class StructuredSessionManager {
|
|
|
2536
2916
|
return;
|
|
2537
2917
|
}
|
|
2538
2918
|
flushEmit();
|
|
2539
|
-
|
|
2540
|
-
const hint = result.spawnError.code === "ENOENT"
|
|
2541
|
-
? "(PATH 中找不到 opencode;请安装 opencode-ai,或重跑 `wand service:install` 刷新服务 PATH)"
|
|
2542
|
-
: "";
|
|
2543
|
-
throw new UnacceptedStructuredSpawnError(`opencode run 启动失败:${result.spawnError.message}${hint}`);
|
|
2544
|
-
}
|
|
2919
|
+
this.throwUnacceptedInput(sessionId, result, "opencode run", "(PATH 中找不到 opencode;请安装 opencode-ai,或重跑 `wand service:install` 刷新服务 PATH)");
|
|
2545
2920
|
this.logger?.appendStructuredSpawn(sessionId, {
|
|
2546
2921
|
kind: "opencode-run-close",
|
|
2547
2922
|
pid: execution.pid,
|
|
@@ -2582,7 +2957,7 @@ export class StructuredSessionManager {
|
|
|
2582
2957
|
pendingEscalation: null,
|
|
2583
2958
|
permissionBlocked: false,
|
|
2584
2959
|
structuredState: {
|
|
2585
|
-
...current.structuredState,
|
|
2960
|
+
...this.settleTurn(sessionId, current.structuredState),
|
|
2586
2961
|
model: result.state.model ?? current.structuredState?.model,
|
|
2587
2962
|
inFlight: false,
|
|
2588
2963
|
activeRequestId: null,
|
|
@@ -2626,13 +3001,14 @@ export class StructuredSessionManager {
|
|
|
2626
3001
|
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
2627
3002
|
if (!current)
|
|
2628
3003
|
return;
|
|
2629
|
-
const hasAssistantContent = turnState.blocks.length > 0 || !!turnState.result;
|
|
3004
|
+
const hasAssistantContent = turnState.blocks.length > 0 || !!turnState.result || !!turnState.resourceSelection;
|
|
2630
3005
|
let messages = [...(current.messages ?? [])];
|
|
2631
3006
|
if (hasAssistantContent) {
|
|
2632
3007
|
const turn = {
|
|
2633
3008
|
role: "assistant",
|
|
2634
3009
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
2635
3010
|
usage: turnState.usage,
|
|
3011
|
+
...(turnState.resourceSelection ? { resourceSelection: turnState.resourceSelection } : {}),
|
|
2636
3012
|
};
|
|
2637
3013
|
messages = upsertAssistantMessage(current.messages, turn);
|
|
2638
3014
|
}
|
|
@@ -2641,11 +3017,11 @@ export class StructuredSessionManager {
|
|
|
2641
3017
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2642
3018
|
messages,
|
|
2643
3019
|
output: turnState.result || current.output,
|
|
2644
|
-
structuredState: {
|
|
3020
|
+
structuredState: this.noteTurnActivity(sessionId, {
|
|
2645
3021
|
...current.structuredState,
|
|
2646
3022
|
model: turnState.model ?? current.structuredState?.model,
|
|
2647
3023
|
phase: turnState.phase ?? current.structuredState?.phase,
|
|
2648
|
-
},
|
|
3024
|
+
}),
|
|
2649
3025
|
};
|
|
2650
3026
|
this.sessions.set(sessionId, patched);
|
|
2651
3027
|
this.saveStreamingSnapshot(patched, hasAssistantContent ? undefined : { metadata: true });
|
|
@@ -2712,12 +3088,7 @@ export class StructuredSessionManager {
|
|
|
2712
3088
|
return;
|
|
2713
3089
|
}
|
|
2714
3090
|
flushEmit();
|
|
2715
|
-
|
|
2716
|
-
const hint = result.spawnError.code === "ENOENT"
|
|
2717
|
-
? `(PATH 中找不到 ${provider === "qoder" ? "qodercli" : provider === "gemini" ? "gemini" : provider === "pi" ? "pi" : "claude"} 可执行文件;${options.installHint ?? "请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"})`
|
|
2718
|
-
: "";
|
|
2719
|
-
throw new UnacceptedStructuredSpawnError(`${commandLabel} 启动失败:${result.spawnError.message}${hint}`);
|
|
2720
|
-
}
|
|
3091
|
+
this.throwUnacceptedInput(sessionId, result, commandLabel, `(PATH 中找不到 ${provider === "qoder" ? "qodercli" : provider === "gemini" ? "gemini" : provider === "pi" ? "pi" : "claude"} 可执行文件;${options.installHint ?? "请确认 claude 已安装,或重跑 `wand service:install` 刷新服务的 PATH"})`);
|
|
2721
3092
|
this.logger?.appendStructuredSpawn(sessionId, {
|
|
2722
3093
|
kind: `${logKind}-close`,
|
|
2723
3094
|
pid: execution.pid,
|
|
@@ -2732,11 +3103,11 @@ export class StructuredSessionManager {
|
|
|
2732
3103
|
const interruptedByUser = this.interruptedWith.has(sessionId);
|
|
2733
3104
|
const interruptedForQuestion = result.stopReason === "ask-user-question";
|
|
2734
3105
|
const failedExit = (result.exitCode !== null && result.exitCode !== 0) || result.signal !== null;
|
|
2735
|
-
//
|
|
2736
|
-
//
|
|
2737
|
-
const
|
|
2738
|
-
if (
|
|
2739
|
-
const errorText =
|
|
3106
|
+
// A protocol failure remains a failure even if the CLI exits 0 or already produced work.
|
|
3107
|
+
// Earlier work is preserved below and must never be replayed on another candidate.
|
|
3108
|
+
const failure = result.failure ?? classifyStructuredFailure(result);
|
|
3109
|
+
if (failure && !interruptedByUser && !interruptedForQuestion) {
|
|
3110
|
+
const errorText = !failedExit && result.primaryError
|
|
2740
3111
|
? result.primaryError.trim()
|
|
2741
3112
|
: this.formatStructuredExitError(commandLabel, result.exitCode, result.signal, {
|
|
2742
3113
|
stderr: result.stderr,
|
|
@@ -2771,13 +3142,20 @@ export class StructuredSessionManager {
|
|
|
2771
3142
|
pendingEscalation: null,
|
|
2772
3143
|
permissionBlocked: false,
|
|
2773
3144
|
structuredState: {
|
|
2774
|
-
...current.structuredState,
|
|
3145
|
+
...this.settleTurn(sessionId, current.structuredState),
|
|
2775
3146
|
model: result.state.model ?? current.structuredState?.model,
|
|
2776
3147
|
inFlight: false,
|
|
2777
3148
|
activeRequestId: null,
|
|
2778
3149
|
lastError: null,
|
|
2779
3150
|
phase: undefined,
|
|
3151
|
+
...(result.state.contextUsage ? { contextUsage: result.state.contextUsage } : {}),
|
|
3152
|
+
// core 压缩状态:历史不删,只记录切点与摘要,下一轮按它重建模型上下文。
|
|
3153
|
+
...(result.state.compaction
|
|
3154
|
+
? { compactions: result.state.compaction.compactions, harnessContext: compactionToSessionContext(result.state.compaction) }
|
|
3155
|
+
: {}),
|
|
2780
3156
|
},
|
|
3157
|
+
...(result.state.compaction ? { harnessContext: compactionToSessionContext(result.state.compaction) } : {}),
|
|
3158
|
+
...(result.state.harnessExtensionState ? { harnessExtensionState: result.state.harnessExtensionState } : {}),
|
|
2781
3159
|
};
|
|
2782
3160
|
this.sessions.set(sessionId, finished);
|
|
2783
3161
|
this.saveAuthoritativeSession(finished);
|
|
@@ -2849,6 +3227,7 @@ export class StructuredSessionManager {
|
|
|
2849
3227
|
role: "assistant",
|
|
2850
3228
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
2851
3229
|
usage: turnState.usage,
|
|
3230
|
+
...(turnState.resourceSelection ? { resourceSelection: turnState.resourceSelection } : {}),
|
|
2852
3231
|
};
|
|
2853
3232
|
return upsertAssistantMessage(current.messages, assistantTurn, true, stampNewTools);
|
|
2854
3233
|
}
|
|
@@ -2911,12 +3290,15 @@ export class StructuredSessionManager {
|
|
|
2911
3290
|
const failureTurn = {
|
|
2912
3291
|
role: "assistant",
|
|
2913
3292
|
content: [{ type: "text", text: `结构化会话执行失败:${errorText}` }],
|
|
3293
|
+
...(turnState.resourceSelection ? { resourceSelection: turnState.resourceSelection } : {}),
|
|
2914
3294
|
};
|
|
2915
|
-
//
|
|
2916
|
-
//
|
|
3295
|
+
// Keep actual work and append the failure, including protocol errors with process exit 0.
|
|
3296
|
+
// A truncated replay must retain its complete local checkpoint rather than rebuild from a tail.
|
|
2917
3297
|
const msgs = options.keepTranscript
|
|
2918
3298
|
? appendNoticeTurn(current.messages, failureTurn)
|
|
2919
|
-
:
|
|
3299
|
+
: hasStructuredExecutionProgress(turnState)
|
|
3300
|
+
? appendNoticeTurn(this.buildCompletedAssistantMessages(current, turnState), failureTurn)
|
|
3301
|
+
: upsertAssistantMessage(current.messages, failureTurn, true);
|
|
2920
3302
|
return {
|
|
2921
3303
|
...current,
|
|
2922
3304
|
status: "failed",
|
|
@@ -2927,8 +3309,10 @@ export class StructuredSessionManager {
|
|
|
2927
3309
|
messages: msgs,
|
|
2928
3310
|
pendingEscalation: null,
|
|
2929
3311
|
permissionBlocked: false,
|
|
3312
|
+
...(turnState.compaction ? { harnessContext: compactionToSessionContext(turnState.compaction) } : {}),
|
|
3313
|
+
...(turnState.harnessExtensionState ? { harnessExtensionState: turnState.harnessExtensionState } : {}),
|
|
2930
3314
|
structuredState: {
|
|
2931
|
-
...current.structuredState,
|
|
3315
|
+
...this.settleTurn(current.id, current.structuredState),
|
|
2932
3316
|
model: turnState.model ?? current.structuredState?.model,
|
|
2933
3317
|
inFlight: false,
|
|
2934
3318
|
activeRequestId: null,
|