@co0ontty/wand 4.80.0 → 4.80.1-beta.g1fbbefb
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/dist/ai-team-availability.js +0 -1
- package/dist/ai-team-live.d.ts +3 -1
- package/dist/ai-team-live.js +31 -2
- package/dist/ai-team-runner.d.ts +2 -1
- package/dist/ai-team-runner.js +17 -7
- package/dist/ai-team-types.d.ts +8 -0
- package/dist/ai-team-types.js +4 -0
- package/dist/build-info.json +4 -4
- package/dist/claude-pty-bridge.js +16 -1
- package/dist/cli.js +0 -9
- package/dist/config.d.ts +2 -10
- package/dist/config.js +1 -73
- package/dist/git-quick-commit.d.ts +3 -6
- package/dist/git-quick-commit.js +38 -75
- package/dist/language-prompt.d.ts +1 -1
- package/dist/language-prompt.js +1 -1
- package/dist/native-session-title.d.ts +17 -0
- package/dist/native-session-title.js +257 -0
- package/dist/process-manager.d.ts +3 -0
- package/dist/process-manager.js +37 -6
- package/dist/server-session-routes.js +4 -29
- package/dist/server-settings-routes.js +2 -177
- package/dist/server-task-routes.js +1 -1
- package/dist/server-workspace-routes.js +1 -1
- package/dist/server.js +1 -13
- package/dist/session-ai-context.d.ts +3 -7
- package/dist/session-ai-context.js +0 -25
- package/dist/session-topic.d.ts +19 -0
- package/dist/session-topic.js +53 -3
- package/dist/session-transport.js +0 -1
- package/dist/silicon-employee-draft.d.ts +1 -1
- package/dist/storage.js +12 -18
- package/dist/structured-claude-adapter.d.ts +0 -6
- package/dist/structured-claude-adapter.js +1 -5
- package/dist/structured-provider-common.d.ts +8 -4
- package/dist/structured-provider-common.js +14 -15
- package/dist/structured-session-manager.d.ts +4 -40
- package/dist/structured-session-manager.js +53 -773
- package/dist/types.d.ts +12 -27
- package/dist/web-ui/content/ai-teams.js +9 -9
- package/dist/web-ui/content/scripts.js +70 -70
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/ws-broadcast.js +19 -7
- package/package.json +1 -2
- package/dist/claude-sdk-runner.d.ts +0 -59
- package/dist/claude-sdk-runner.js +0 -188
- package/dist/claude-skills.d.ts +0 -6
- package/dist/claude-skills.js +0 -53
- package/dist/system-ai.d.ts +0 -27
- package/dist/system-ai.js +0 -424
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { query as sdkQuery } from "@anthropic-ai/claude-agent-sdk";
|
|
3
2
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
4
3
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
5
4
|
import { buildChildEnv } from "./env-utils.js";
|
|
@@ -7,17 +6,16 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
7
6
|
import { getDefaultModelForProvider } from "./config.js";
|
|
8
7
|
import { recordIterationPrompt } from "./iteration-log.js";
|
|
9
8
|
import { signalNameFromNumber } from "./signal-utils.js";
|
|
10
|
-
import {
|
|
11
|
-
import { provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
9
|
+
import { provisionalSessionTopic, SessionNativeTitleTracker, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
12
10
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
13
11
|
import { isSessionProvider, providerCliCommand } from "./session-provider.js";
|
|
14
|
-
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
12
|
+
import { resolveSessionProvider, resolveSystemAiContext } from "./session-ai-context.js";
|
|
13
|
+
import { readNativeSessionTitle } from "./native-session-title.js";
|
|
15
14
|
import { CodexRunner } from "./structured-codex-adapter.js";
|
|
16
15
|
import { CodexProtocolReducer } from "./structured-codex-protocol.js";
|
|
17
16
|
import { normalizeStructuredToolResultContent } from "./structured-content.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { captureTaskMeta, extractClaudeAssistantMessage, extractClaudeModelName, normalizeClaudeToolInput, stampParentTaskResults, stampSelfTask, tagSubagentBlocks, ClaudeCliProtocolReducer, } from "./structured-claude-protocol.js";
|
|
17
|
+
import { ClaudeCliRunner, } from "./structured-claude-adapter.js";
|
|
18
|
+
import { ClaudeCliProtocolReducer, } from "./structured-claude-protocol.js";
|
|
21
19
|
import { OpenCodeRunner, applyOpenCodeEvent } from "./structured-opencode-adapter.js";
|
|
22
20
|
import { GrokRunner, applyGrokEvent } from "./structured-grok-adapter.js";
|
|
23
21
|
import { QoderRunner } from "./structured-qoder-adapter.js";
|
|
@@ -220,7 +218,6 @@ function buildStructuredOutputPayload(snapshot) {
|
|
|
220
218
|
output: snapshot.output,
|
|
221
219
|
messages: snapshot.messages ? enrichStructuredMessages(snapshot.messages, snapshot.id) : undefined,
|
|
222
220
|
queuedMessages: snapshot.queuedMessages,
|
|
223
|
-
queuedMessageSkills: snapshot.queuedMessageSkills,
|
|
224
221
|
sessionKind: "structured",
|
|
225
222
|
structuredState: snapshot.structuredState,
|
|
226
223
|
provider: snapshot.provider,
|
|
@@ -288,7 +285,6 @@ function buildIncrementalStructuredPayload(snapshot, cardDefaults) {
|
|
|
288
285
|
wandProtocolVersion: WAND_PROTOCOL_VERSION,
|
|
289
286
|
incremental: true,
|
|
290
287
|
queuedMessages: snapshot.queuedMessages,
|
|
291
|
-
queuedMessageSkills: snapshot.queuedMessageSkills,
|
|
292
288
|
sessionKind: "structured",
|
|
293
289
|
structuredState: snapshot.structuredState,
|
|
294
290
|
lastMessage,
|
|
@@ -398,30 +394,10 @@ export class StructuredSessionManager {
|
|
|
398
394
|
storage;
|
|
399
395
|
config;
|
|
400
396
|
logger;
|
|
401
|
-
sdkQueryFactory;
|
|
402
397
|
execHost;
|
|
403
398
|
sessions = new Map();
|
|
404
399
|
pendingRunnerExecutions = new Map();
|
|
405
|
-
pendingSdkAbort = new Map();
|
|
406
|
-
/**
|
|
407
|
-
* Active SDK Query handle per session, kept around so we can call
|
|
408
|
-
* `query.interrupt()` for a graceful stop instead of aborting via signal.
|
|
409
|
-
* Only populated while an SDK call is in flight.
|
|
410
|
-
*/
|
|
411
|
-
pendingSdkQueries = new Map();
|
|
412
|
-
/** In-flight canUseTool waiters. One pending prompt per session. */
|
|
413
|
-
pendingPermissions = new Map();
|
|
414
|
-
/** approve_turn memory, scoped to the current SDK query. */
|
|
415
|
-
turnApprovalMemory = new Map();
|
|
416
400
|
interruptedWith = new Map();
|
|
417
|
-
interruptedSkills = new Map();
|
|
418
|
-
/**
|
|
419
|
-
* Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
|
|
420
|
-
* 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
|
|
421
|
-
* interrupt 语义是"算了,做这个",把队列也作废。但 queue-promote 的语义是
|
|
422
|
-
* "先做这条,剩下的队列还要继续",所以这里打个标记,让退出 handler 保留 queue。
|
|
423
|
-
* 收到后必须 delete 掉,避免下一次普通 interrupt 误带 flag。
|
|
424
|
-
*/
|
|
425
401
|
preserveQueueOnInterrupt = new Set();
|
|
426
402
|
/** Last wall-clock time (ms) a streaming checkpoint reached SQLite. */
|
|
427
403
|
lastStreamSaveAt = new Map();
|
|
@@ -439,6 +415,7 @@ export class StructuredSessionManager {
|
|
|
439
415
|
emitEvent = null;
|
|
440
416
|
archiveTimer = null;
|
|
441
417
|
topicCoordinator = new SessionTopicCoordinator();
|
|
418
|
+
nativeTitles = new SessionNativeTitleTracker();
|
|
442
419
|
streamEmitTimers = new Set();
|
|
443
420
|
claudeCliRunner;
|
|
444
421
|
codexRunner;
|
|
@@ -453,11 +430,10 @@ export class StructuredSessionManager {
|
|
|
453
430
|
detachedRecoveryRetryTimer = null;
|
|
454
431
|
detachedRecoveryFailureLogged = false;
|
|
455
432
|
disposed = false;
|
|
456
|
-
constructor(storage, config, logger = null,
|
|
433
|
+
constructor(storage, config, logger = null, runners = {}, execHost) {
|
|
457
434
|
this.storage = storage;
|
|
458
435
|
this.config = config;
|
|
459
436
|
this.logger = logger;
|
|
460
|
-
this.sdkQueryFactory = sdkQueryFactory;
|
|
461
437
|
this.execHost = execHost;
|
|
462
438
|
this.claudeCliRunner = runners.claudeCli ?? new ClaudeCliRunner({ language: () => this.config.language }, this.execHost);
|
|
463
439
|
this.codexRunner = runners.codex ?? new CodexRunner(undefined, this.execHost);
|
|
@@ -477,9 +453,8 @@ export class StructuredSessionManager {
|
|
|
477
453
|
// rely on the provider/runner invariant without making startup fail.
|
|
478
454
|
const runner = isStructuredRunnerForProvider(provider, storedRunner)
|
|
479
455
|
? storedRunner
|
|
480
|
-
: defaultStructuredRunner(provider
|
|
456
|
+
: defaultStructuredRunner(provider);
|
|
481
457
|
const recoverableDetachedRun = snapshot.status === "running"
|
|
482
|
-
&& runner !== "claude-sdk"
|
|
483
458
|
&& this.execHost?.persistent === true;
|
|
484
459
|
if (recoverableDetachedRun)
|
|
485
460
|
this.pendingRecoveryIds.push(snapshot.id);
|
|
@@ -494,7 +469,6 @@ export class StructuredSessionManager {
|
|
|
494
469
|
autoApprovePermissions: snapshot.autoApprovePermissions ?? shouldAutoApproveForMode(snapshot.mode),
|
|
495
470
|
approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
|
|
496
471
|
queuedMessages: snapshot.queuedMessages ?? [],
|
|
497
|
-
queuedMessageSkills: (snapshot.queuedMessages ?? []).map((_, index) => snapshot.queuedMessageSkills?.[index] ?? []),
|
|
498
472
|
pendingEscalation: null,
|
|
499
473
|
permissionBlocked: false,
|
|
500
474
|
structuredState: {
|
|
@@ -567,8 +541,6 @@ export class StructuredSessionManager {
|
|
|
567
541
|
this.streamEmitTimers.clear();
|
|
568
542
|
const activeSessionIds = new Set([
|
|
569
543
|
...this.pendingRunnerExecutions.keys(),
|
|
570
|
-
...this.pendingSdkQueries.keys(),
|
|
571
|
-
...this.pendingSdkAbort.keys(),
|
|
572
544
|
...Array.from(this.sessions.values())
|
|
573
545
|
.filter((session) => session.structuredState?.inFlight)
|
|
574
546
|
.map((session) => session.id),
|
|
@@ -580,7 +552,7 @@ export class StructuredSessionManager {
|
|
|
580
552
|
const session = this.sessions.get(id);
|
|
581
553
|
if (!session)
|
|
582
554
|
continue;
|
|
583
|
-
if (detachSafe
|
|
555
|
+
if (detachSafe)
|
|
584
556
|
continue;
|
|
585
557
|
const cancelled = {
|
|
586
558
|
...session,
|
|
@@ -603,25 +575,12 @@ export class StructuredSessionManager {
|
|
|
603
575
|
catch { /* best-effort shutdown flush */ }
|
|
604
576
|
}
|
|
605
577
|
for (const [executionId, execution] of this.pendingRunnerExecutions) {
|
|
606
|
-
if (detachSafe
|
|
578
|
+
if (detachSafe)
|
|
607
579
|
continue;
|
|
608
580
|
execution.interrupt();
|
|
609
581
|
}
|
|
610
|
-
for (const [sessionId, waiter] of this.pendingPermissions) {
|
|
611
|
-
waiter.resolve(structuredPermissionDenied("服务已关闭"));
|
|
612
|
-
this.pendingPermissions.delete(sessionId);
|
|
613
|
-
}
|
|
614
|
-
this.turnApprovalMemory.clear();
|
|
615
|
-
for (const query of this.pendingSdkQueries.values()) {
|
|
616
|
-
void query.interrupt().catch(() => { });
|
|
617
|
-
}
|
|
618
|
-
for (const controller of this.pendingSdkAbort.values())
|
|
619
|
-
controller.abort();
|
|
620
582
|
this.pendingRunnerExecutions.clear();
|
|
621
|
-
this.pendingSdkQueries.clear();
|
|
622
|
-
this.pendingSdkAbort.clear();
|
|
623
583
|
this.interruptedWith.clear();
|
|
624
|
-
this.interruptedSkills.clear();
|
|
625
584
|
this.preserveQueueOnInterrupt.clear();
|
|
626
585
|
for (const timer of this.streamCheckpointTimers.values())
|
|
627
586
|
clearTimeout(timer);
|
|
@@ -629,6 +588,7 @@ export class StructuredSessionManager {
|
|
|
629
588
|
this.streamCheckpointDirty.clear();
|
|
630
589
|
this.lastStreamSaveAt.clear();
|
|
631
590
|
this.topicCoordinator.clear();
|
|
591
|
+
this.nativeTitles.clear();
|
|
632
592
|
this.emitEvent = null;
|
|
633
593
|
}
|
|
634
594
|
// ---------------------------------------------------------------------------
|
|
@@ -1142,8 +1102,11 @@ export class StructuredSessionManager {
|
|
|
1142
1102
|
// 迭代提示词记录:和会话标题共用同一套「有没有信息量」判断,用户不用多做一步。
|
|
1143
1103
|
recordIterationPrompt(this.storage, session, input, "session");
|
|
1144
1104
|
const blockedTitles = sessionTopicBlocklistForSnapshot(session, this.storage);
|
|
1105
|
+
// CLI 自己起了名字就归它,模型标题与输入首行都不再覆盖;取不到原生标题才走系统硅基员工。
|
|
1106
|
+
const hasNativeTitle = this.nativeTitles.has(id);
|
|
1145
1107
|
const provisional = provisionalSessionTopic(input, blockedTitles);
|
|
1146
1108
|
if (provisional
|
|
1109
|
+
&& !hasNativeTitle
|
|
1147
1110
|
&& shouldGenerateSessionTopicFromInput(input)
|
|
1148
1111
|
&& (session.title !== provisional.title || session.description !== provisional.description)) {
|
|
1149
1112
|
this.setSessionTopic(id, provisional.title, provisional.description);
|
|
@@ -1154,12 +1117,22 @@ export class StructuredSessionManager {
|
|
|
1154
1117
|
cwd: session.cwd,
|
|
1155
1118
|
language: this.config.language,
|
|
1156
1119
|
ai: resolveSystemAiContext(session, this.config, this.storage.getSystemSiliconEmployee()),
|
|
1120
|
+
readNativeTitle: () => readNativeSessionTitle(resolveSessionProvider(session), session.claudeSessionId, session.cwd),
|
|
1121
|
+
hasNativeTitle,
|
|
1157
1122
|
onGenerating: (generating) => {
|
|
1158
1123
|
if (!this.disposed)
|
|
1159
1124
|
this.setSessionTopicGenerating(id, generating);
|
|
1160
1125
|
},
|
|
1161
|
-
onTopic: ({ title, description }) => {
|
|
1162
|
-
if (
|
|
1126
|
+
onTopic: ({ title, description, source }) => {
|
|
1127
|
+
if (this.disposed || !this.sessions.has(id))
|
|
1128
|
+
return;
|
|
1129
|
+
if (source === "native") {
|
|
1130
|
+
this.applyNativeSessionTitle(id, title, blockedTitles);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (this.nativeTitles.has(id))
|
|
1134
|
+
return;
|
|
1135
|
+
if (shouldAcceptGeneratedSessionTitle(title, blockedTitles)) {
|
|
1163
1136
|
this.setSessionTopic(id, title, description);
|
|
1164
1137
|
}
|
|
1165
1138
|
},
|
|
@@ -1168,6 +1141,16 @@ export class StructuredSessionManager {
|
|
|
1168
1141
|
},
|
|
1169
1142
|
});
|
|
1170
1143
|
}
|
|
1144
|
+
/** 原生标题优先于模型标题:只受「不能是任务名/目录名」约束,命中且变化时才落库。 */
|
|
1145
|
+
applyNativeSessionTitle(id, title, blockedTitles) {
|
|
1146
|
+
this.nativeTitles.record(id, title);
|
|
1147
|
+
const current = this.sessions.get(id);
|
|
1148
|
+
if (!current || !shouldAcceptGeneratedSessionTitle(title, blockedTitles))
|
|
1149
|
+
return;
|
|
1150
|
+
if (current.title === title && current.description === title)
|
|
1151
|
+
return;
|
|
1152
|
+
this.setSessionTopic(id, title, title);
|
|
1153
|
+
}
|
|
1171
1154
|
createSession(options) {
|
|
1172
1155
|
if (this.disposed)
|
|
1173
1156
|
throw new Error("StructuredSessionManager has been disposed.");
|
|
@@ -1178,7 +1161,7 @@ export class StructuredSessionManager {
|
|
|
1178
1161
|
throw new Error(`不支持的结构化 provider: ${String(requestedProvider)}`);
|
|
1179
1162
|
}
|
|
1180
1163
|
const provider = requestedProvider;
|
|
1181
|
-
const runner = resolveStructuredRunner(provider, options.runner
|
|
1164
|
+
const runner = resolveStructuredRunner(provider, options.runner);
|
|
1182
1165
|
const baseCwd = resolveSessionCwd(options.cwd, this.config.defaultCwd);
|
|
1183
1166
|
const worktreeSetup = options.worktreeEnabled
|
|
1184
1167
|
? prepareSessionWorktree({ cwd: baseCwd, sessionId: id, spec: options.worktreeSpec })
|
|
@@ -1212,8 +1195,8 @@ export class StructuredSessionManager {
|
|
|
1212
1195
|
? "pi --mode json --print"
|
|
1213
1196
|
: provider === "gemini"
|
|
1214
1197
|
? "gemini -p --output-format stream-json"
|
|
1215
|
-
: runner === "claude-
|
|
1216
|
-
? "claude-
|
|
1198
|
+
: runner === "claude-cli-print"
|
|
1199
|
+
? "claude -p (stream-json)"
|
|
1217
1200
|
: "claude -p --output-format stream-json",
|
|
1218
1201
|
cwd: worktreeSetup?.cwd ?? baseCwd,
|
|
1219
1202
|
mode: options.mode,
|
|
@@ -1268,7 +1251,7 @@ export class StructuredSessionManager {
|
|
|
1268
1251
|
return titled;
|
|
1269
1252
|
}
|
|
1270
1253
|
/** Only an unstarted employee conversation may change CLI after a failed first turn. */
|
|
1271
|
-
retryEmployeeCandidate(id, original, prompt, error
|
|
1254
|
+
retryEmployeeCandidate(id, original, prompt, error) {
|
|
1272
1255
|
const candidates = original.employeeCandidates ?? [];
|
|
1273
1256
|
const nextIndex = (original.employeeCandidateIndex ?? 0) + 1;
|
|
1274
1257
|
if (!original.employeeId || (original.messages?.length ?? 0) !== 0 || nextIndex >= candidates.length)
|
|
@@ -1281,7 +1264,7 @@ export class StructuredSessionManager {
|
|
|
1281
1264
|
if (!(error instanceof UnacceptedStructuredSpawnError))
|
|
1282
1265
|
return null;
|
|
1283
1266
|
const next = candidates[nextIndex];
|
|
1284
|
-
const runner = resolveStructuredRunner(next.provider, undefined
|
|
1267
|
+
const runner = resolveStructuredRunner(next.provider, undefined);
|
|
1285
1268
|
const model = next.model === "default"
|
|
1286
1269
|
? getDefaultModelForProvider(this.config, next.provider) || null
|
|
1287
1270
|
: next.model;
|
|
@@ -1308,7 +1291,7 @@ export class StructuredSessionManager {
|
|
|
1308
1291
|
this.sessions.set(id, retry);
|
|
1309
1292
|
this.saveAuthoritativeSession(retry);
|
|
1310
1293
|
this.emitStructuredSnapshot(retry);
|
|
1311
|
-
return this.sendMessage(id, prompt
|
|
1294
|
+
return this.sendMessage(id, prompt);
|
|
1312
1295
|
}
|
|
1313
1296
|
/** 往转发会话里追加别的参与者的发言。 */
|
|
1314
1297
|
appendRelayTurns(id, turns) {
|
|
@@ -1349,7 +1332,6 @@ export class StructuredSessionManager {
|
|
|
1349
1332
|
}
|
|
1350
1333
|
return this.sessions.get(id) ?? posted;
|
|
1351
1334
|
}
|
|
1352
|
-
const skills = opts?.skills ?? [];
|
|
1353
1335
|
if (opts?.idempotencyKey) {
|
|
1354
1336
|
const mapKey = `${id}:${opts.idempotencyKey}`;
|
|
1355
1337
|
if (this.seenIdempotencyKeys.has(mapKey)) {
|
|
@@ -1369,17 +1351,12 @@ export class StructuredSessionManager {
|
|
|
1369
1351
|
this.maybeGenerateSessionTopic(id, prompt);
|
|
1370
1352
|
if (session.structuredState?.inFlight) {
|
|
1371
1353
|
const runnerExecution = this.pendingRunnerExecutions.get(id);
|
|
1372
|
-
const sdkAbort = this.pendingSdkAbort.get(id);
|
|
1373
|
-
const sdkQueryHandle = this.pendingSdkQueries.get(id);
|
|
1374
1354
|
// interrupt() only requests cancellation; completion can settle later.
|
|
1375
1355
|
// Treat runner-map ownership as the authoritative in-flight state.
|
|
1376
1356
|
const childActive = Boolean(runnerExecution);
|
|
1377
|
-
|
|
1378
|
-
if (!childActive && !sdkAlive) {
|
|
1357
|
+
if (!childActive) {
|
|
1379
1358
|
if (runnerExecution)
|
|
1380
1359
|
this.releasePendingRunnerExecution(id, runnerExecution);
|
|
1381
|
-
if (sdkAbort)
|
|
1382
|
-
this.releasePendingSdkAbort(id, sdkAbort);
|
|
1383
1360
|
const recovered = {
|
|
1384
1361
|
...session,
|
|
1385
1362
|
status: "idle",
|
|
@@ -1396,7 +1373,6 @@ export class StructuredSessionManager {
|
|
|
1396
1373
|
}
|
|
1397
1374
|
else if (opts?.interrupt) {
|
|
1398
1375
|
this.interruptedWith.set(id, prompt);
|
|
1399
|
-
this.interruptedSkills.set(id, skills);
|
|
1400
1376
|
if (opts.preserveQueue) {
|
|
1401
1377
|
this.preserveQueueOnInterrupt.add(id);
|
|
1402
1378
|
// 「立即发送」排队条某一条:interrupt 把它作为新输入重发,但该条仍留在
|
|
@@ -1409,12 +1385,7 @@ export class StructuredSessionManager {
|
|
|
1409
1385
|
const removeAt = queue.indexOf(prompt);
|
|
1410
1386
|
if (removeAt !== -1) {
|
|
1411
1387
|
const trimmedQueue = queue.slice(0, removeAt).concat(queue.slice(removeAt + 1));
|
|
1412
|
-
|
|
1413
|
-
session = {
|
|
1414
|
-
...session,
|
|
1415
|
-
queuedMessages: trimmedQueue,
|
|
1416
|
-
queuedMessageSkills: queuedMessageSkills.slice(0, removeAt).concat(queuedMessageSkills.slice(removeAt + 1)),
|
|
1417
|
-
};
|
|
1388
|
+
session = { ...session, queuedMessages: trimmedQueue };
|
|
1418
1389
|
this.sessions.set(id, session);
|
|
1419
1390
|
this.storage.updateSessionRuntimeMetadata(session);
|
|
1420
1391
|
this.emitStructuredSnapshot(session);
|
|
@@ -1425,11 +1396,6 @@ export class StructuredSessionManager {
|
|
|
1425
1396
|
this.preserveQueueOnInterrupt.delete(id);
|
|
1426
1397
|
}
|
|
1427
1398
|
runnerExecution?.interrupt();
|
|
1428
|
-
if (sdkQueryHandle) {
|
|
1429
|
-
void sdkQueryHandle.interrupt().catch(() => { });
|
|
1430
|
-
}
|
|
1431
|
-
if (sdkAbort)
|
|
1432
|
-
sdkAbort.abort();
|
|
1433
1399
|
return session;
|
|
1434
1400
|
}
|
|
1435
1401
|
else {
|
|
@@ -1445,7 +1411,6 @@ export class StructuredSessionManager {
|
|
|
1445
1411
|
const queued = {
|
|
1446
1412
|
...session,
|
|
1447
1413
|
queuedMessages: [...queue, prompt],
|
|
1448
|
-
queuedMessageSkills: [...queue.map((_, index) => session.queuedMessageSkills?.[index] ?? []), skills],
|
|
1449
1414
|
};
|
|
1450
1415
|
this.sessions.set(id, queued);
|
|
1451
1416
|
this.storage.updateSessionRuntimeMetadata(queued);
|
|
@@ -1554,9 +1519,6 @@ export class StructuredSessionManager {
|
|
|
1554
1519
|
installHint: "请安装 @google/gemini-cli ≥ 0.11(`npm i -g @google/gemini-cli@latest`),或重跑 `wand service:install` 刷新服务的 PATH",
|
|
1555
1520
|
});
|
|
1556
1521
|
}
|
|
1557
|
-
else if (runner === "claude-sdk") {
|
|
1558
|
-
await this.runClaudeSdkStreaming(id, updated, prompt, requestId, skills);
|
|
1559
|
-
}
|
|
1560
1522
|
else {
|
|
1561
1523
|
await this.runClaudeStreaming(id, updated, cliClaudePrompt, requestId);
|
|
1562
1524
|
}
|
|
@@ -1569,7 +1531,7 @@ export class StructuredSessionManager {
|
|
|
1569
1531
|
// the detailed failure. Re-throw even if an ended-event listener removed
|
|
1570
1532
|
// the session synchronously; there is no request-id marker to leak.
|
|
1571
1533
|
if (error instanceof PersistedStructuredRunnerError) {
|
|
1572
|
-
const retry = this.retryEmployeeCandidate(id, session, prompt, error
|
|
1534
|
+
const retry = this.retryEmployeeCandidate(id, session, prompt, error);
|
|
1573
1535
|
if (retry)
|
|
1574
1536
|
return await retry;
|
|
1575
1537
|
throw error;
|
|
@@ -1582,11 +1544,9 @@ export class StructuredSessionManager {
|
|
|
1582
1544
|
if (!this.isCurrentRequest(id, requestId)) {
|
|
1583
1545
|
return current;
|
|
1584
1546
|
}
|
|
1585
|
-
const retry = this.retryEmployeeCandidate(id, session, prompt, error
|
|
1547
|
+
const retry = this.retryEmployeeCandidate(id, session, prompt, error);
|
|
1586
1548
|
if (retry)
|
|
1587
1549
|
return await retry;
|
|
1588
|
-
this.settlePendingPermission(id, undefined, structuredPermissionDenied("会话执行失败"));
|
|
1589
|
-
this.turnApprovalMemory.delete(id);
|
|
1590
1550
|
const failed = {
|
|
1591
1551
|
...current,
|
|
1592
1552
|
status: "failed",
|
|
@@ -1637,7 +1597,6 @@ export class StructuredSessionManager {
|
|
|
1637
1597
|
const updated = {
|
|
1638
1598
|
...session,
|
|
1639
1599
|
queuedMessages: reordered,
|
|
1640
|
-
queuedMessageSkills: order.map((idx) => (session.queuedMessageSkills ?? [])[idx] ?? []),
|
|
1641
1600
|
};
|
|
1642
1601
|
this.sessions.set(sessionId, updated);
|
|
1643
1602
|
this.storage.updateSessionRuntimeMetadata(updated);
|
|
@@ -1673,11 +1632,9 @@ export class StructuredSessionManager {
|
|
|
1673
1632
|
throw new Error("排队消息已变化,请按最新顺序重试。");
|
|
1674
1633
|
}
|
|
1675
1634
|
const next = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
1676
|
-
const skills = session.queuedMessageSkills ?? [];
|
|
1677
1635
|
const updated = {
|
|
1678
1636
|
...session,
|
|
1679
1637
|
queuedMessages: next,
|
|
1680
|
-
queuedMessageSkills: skills.slice(0, index).concat(skills.slice(index + 1)),
|
|
1681
1638
|
};
|
|
1682
1639
|
this.sessions.set(sessionId, updated);
|
|
1683
1640
|
this.storage.updateSessionRuntimeMetadata(updated);
|
|
@@ -1702,15 +1659,11 @@ export class StructuredSessionManager {
|
|
|
1702
1659
|
throw new Error("排队消息已变化,请按最新顺序重试。");
|
|
1703
1660
|
}
|
|
1704
1661
|
const prompt = queue[index];
|
|
1705
|
-
const skills = (session.queuedMessageSkills ?? [])[index] ?? [];
|
|
1706
1662
|
const remaining = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
1707
|
-
const remainingSkills = (session.queuedMessageSkills ?? []).slice(0, index)
|
|
1708
|
-
.concat((session.queuedMessageSkills ?? []).slice(index + 1));
|
|
1709
1663
|
const inFlight = session.status === "running" && session.structuredState?.inFlight === true;
|
|
1710
1664
|
const updated = {
|
|
1711
1665
|
...session,
|
|
1712
1666
|
queuedMessages: remaining,
|
|
1713
|
-
queuedMessageSkills: remainingSkills,
|
|
1714
1667
|
};
|
|
1715
1668
|
this.sessions.set(sessionId, updated);
|
|
1716
1669
|
this.storage.updateSessionRuntimeMetadata(updated);
|
|
@@ -1721,7 +1674,6 @@ export class StructuredSessionManager {
|
|
|
1721
1674
|
preserveQueue: inFlight,
|
|
1722
1675
|
queueAlreadyRemoved: true,
|
|
1723
1676
|
idempotencyKey,
|
|
1724
|
-
skills,
|
|
1725
1677
|
});
|
|
1726
1678
|
}
|
|
1727
1679
|
catch {
|
|
@@ -1736,7 +1688,7 @@ export class StructuredSessionManager {
|
|
|
1736
1688
|
if (!session.queuedMessages || session.queuedMessages.length === 0) {
|
|
1737
1689
|
return session;
|
|
1738
1690
|
}
|
|
1739
|
-
const updated = { ...session, queuedMessages: []
|
|
1691
|
+
const updated = { ...session, queuedMessages: [] };
|
|
1740
1692
|
this.sessions.set(sessionId, updated);
|
|
1741
1693
|
this.storage.updateSessionRuntimeMetadata(updated);
|
|
1742
1694
|
this.emitStructuredSnapshot(updated);
|
|
@@ -1863,12 +1815,6 @@ export class StructuredSessionManager {
|
|
|
1863
1815
|
if (approved && scope) {
|
|
1864
1816
|
this.incrementApprovalStats(session, scope);
|
|
1865
1817
|
}
|
|
1866
|
-
if (approved && resolution === "approve_turn") {
|
|
1867
|
-
this.rememberTurnApproval(sessionId, pending.scope, pending.target);
|
|
1868
|
-
}
|
|
1869
|
-
this.settlePendingPermission(sessionId, pending.requestId, approved
|
|
1870
|
-
? { behavior: "allow" }
|
|
1871
|
-
: structuredPermissionDenied("User rejected permission request"));
|
|
1872
1818
|
const updated = {
|
|
1873
1819
|
...session,
|
|
1874
1820
|
pendingEscalation: null,
|
|
@@ -1889,11 +1835,8 @@ export class StructuredSessionManager {
|
|
|
1889
1835
|
return updated;
|
|
1890
1836
|
}
|
|
1891
1837
|
stop(id) {
|
|
1892
|
-
this.settlePendingPermission(id, undefined, structuredPermissionDenied("已停止"));
|
|
1893
|
-
this.turnApprovalMemory.delete(id);
|
|
1894
1838
|
const session = this.requireSession(id);
|
|
1895
1839
|
this.interruptedWith.delete(id);
|
|
1896
|
-
this.interruptedSkills.delete(id);
|
|
1897
1840
|
this.preserveQueueOnInterrupt.delete(id);
|
|
1898
1841
|
// Clearing activeRequestId is the generation barrier: late data/close callbacks
|
|
1899
1842
|
// from the cancelled runner can no longer mutate this session or a replacement turn.
|
|
@@ -1919,47 +1862,22 @@ export class StructuredSessionManager {
|
|
|
1919
1862
|
runnerExecution.interrupt();
|
|
1920
1863
|
this.releasePendingRunnerExecution(id, runnerExecution);
|
|
1921
1864
|
}
|
|
1922
|
-
// SDK runner:先尝试 query.interrupt() 优雅停止,失败再走 abort。
|
|
1923
|
-
// 两个都清掉避免后续重复操作。
|
|
1924
|
-
const sdkQuery = this.pendingSdkQueries.get(id);
|
|
1925
|
-
if (sdkQuery) {
|
|
1926
|
-
void sdkQuery.interrupt().catch(() => { });
|
|
1927
|
-
this.releasePendingSdkQuery(id, sdkQuery);
|
|
1928
|
-
}
|
|
1929
|
-
const sdkAbort = this.pendingSdkAbort.get(id);
|
|
1930
|
-
if (sdkAbort) {
|
|
1931
|
-
sdkAbort.abort();
|
|
1932
|
-
this.releasePendingSdkAbort(id, sdkAbort);
|
|
1933
|
-
}
|
|
1934
1865
|
this.saveAuthoritativeSession(cancelled);
|
|
1935
1866
|
// 仍发 "ended" 事件让各端停掉"回复中"指示 / 灵动岛,但携带的 status 是 idle。
|
|
1936
1867
|
this.emitStructuredSnapshot(cancelled, "ended");
|
|
1937
1868
|
return cancelled;
|
|
1938
1869
|
}
|
|
1939
1870
|
delete(id) {
|
|
1940
|
-
this.settlePendingPermission(id, undefined, structuredPermissionDenied("会话已删除"));
|
|
1941
|
-
this.turnApprovalMemory.delete(id);
|
|
1942
1871
|
const runnerExecution = this.pendingRunnerExecutions.get(id);
|
|
1943
|
-
const sdkQuery = this.pendingSdkQueries.get(id);
|
|
1944
|
-
const sdkAbort = this.pendingSdkAbort.get(id);
|
|
1945
1872
|
// Invalidate callback ownership before signalling the runner. Cancellation
|
|
1946
|
-
// can synchronously wake listeners in some
|
|
1873
|
+
// can synchronously wake listeners in some adapter implementations.
|
|
1947
1874
|
this.sessions.delete(id);
|
|
1948
1875
|
if (runnerExecution) {
|
|
1949
1876
|
runnerExecution.interrupt();
|
|
1950
1877
|
this.releasePendingRunnerExecution(id, runnerExecution);
|
|
1951
1878
|
}
|
|
1952
|
-
if (sdkQuery) {
|
|
1953
|
-
void sdkQuery.interrupt().catch(() => { });
|
|
1954
|
-
this.releasePendingSdkQuery(id, sdkQuery);
|
|
1955
|
-
}
|
|
1956
|
-
if (sdkAbort) {
|
|
1957
|
-
sdkAbort.abort();
|
|
1958
|
-
this.releasePendingSdkAbort(id, sdkAbort);
|
|
1959
|
-
}
|
|
1960
1879
|
this.clearStreamingCheckpoint(id);
|
|
1961
1880
|
this.interruptedWith.delete(id);
|
|
1962
|
-
this.interruptedSkills.delete(id);
|
|
1963
1881
|
this.preserveQueueOnInterrupt.delete(id);
|
|
1964
1882
|
this.storage.deleteSession(id);
|
|
1965
1883
|
this.logger?.deleteSession(id);
|
|
@@ -1993,18 +1911,6 @@ export class StructuredSessionManager {
|
|
|
1993
1911
|
this.execHost?.forgetRun(structuredRunId(sessionId));
|
|
1994
1912
|
return true;
|
|
1995
1913
|
}
|
|
1996
|
-
releasePendingSdkAbort(sessionId, controller) {
|
|
1997
|
-
if (this.pendingSdkAbort.get(sessionId) !== controller)
|
|
1998
|
-
return false;
|
|
1999
|
-
this.pendingSdkAbort.delete(sessionId);
|
|
2000
|
-
return true;
|
|
2001
|
-
}
|
|
2002
|
-
releasePendingSdkQuery(sessionId, query) {
|
|
2003
|
-
if (this.pendingSdkQueries.get(sessionId) !== query)
|
|
2004
|
-
return false;
|
|
2005
|
-
this.pendingSdkQueries.delete(sessionId);
|
|
2006
|
-
return true;
|
|
2007
|
-
}
|
|
2008
1914
|
emitStructuredSnapshot(session, eventType = "output") {
|
|
2009
1915
|
// 排队消息只通过 payload.queuedMessages 单独下发,由各端在消息卡片外的「排队条」
|
|
2010
1916
|
// 里纵向渲染——绝不再把它们当成 __queued 占位 turn 混进 messages 消息流里,否则会
|
|
@@ -2035,17 +1941,15 @@ export class StructuredSessionManager {
|
|
|
2035
1941
|
if (!nextInput) {
|
|
2036
1942
|
return;
|
|
2037
1943
|
}
|
|
2038
|
-
const [nextSkills = [], ...restSkills] = current.queuedMessageSkills ?? [];
|
|
2039
1944
|
const nextSession = {
|
|
2040
1945
|
...current,
|
|
2041
1946
|
queuedMessages: restQueue,
|
|
2042
|
-
queuedMessageSkills: restSkills,
|
|
2043
1947
|
};
|
|
2044
1948
|
this.sessions.set(sessionId, nextSession);
|
|
2045
1949
|
this.storage.updateSessionRuntimeMetadata(nextSession);
|
|
2046
1950
|
this.emitStructuredSnapshot(nextSession);
|
|
2047
1951
|
try {
|
|
2048
|
-
await this.sendMessage(sessionId, nextInput
|
|
1952
|
+
await this.sendMessage(sessionId, nextInput);
|
|
2049
1953
|
}
|
|
2050
1954
|
catch (error) {
|
|
2051
1955
|
console.error("[WAND] flushNextQueuedMessage failed:", error);
|
|
@@ -2055,7 +1959,6 @@ export class StructuredSessionManager {
|
|
|
2055
1959
|
const rescued = {
|
|
2056
1960
|
...afterFail,
|
|
2057
1961
|
queuedMessages: [nextInput, ...(afterFail.queuedMessages ?? [])],
|
|
2058
|
-
queuedMessageSkills: [nextSkills, ...(afterFail.queuedMessageSkills ?? [])],
|
|
2059
1962
|
};
|
|
2060
1963
|
this.sessions.set(sessionId, rescued);
|
|
2061
1964
|
this.storage.updateSessionRuntimeMetadata(rescued);
|
|
@@ -2083,123 +1986,6 @@ export class StructuredSessionManager {
|
|
|
2083
1986
|
stats.total++;
|
|
2084
1987
|
session.approvalStats = stats;
|
|
2085
1988
|
}
|
|
2086
|
-
rememberTurnApproval(sessionId, scope, target) {
|
|
2087
|
-
let memory = this.turnApprovalMemory.get(sessionId);
|
|
2088
|
-
if (!memory) {
|
|
2089
|
-
memory = { scopes: new Set(), targets: new Set() };
|
|
2090
|
-
this.turnApprovalMemory.set(sessionId, memory);
|
|
2091
|
-
}
|
|
2092
|
-
memory.scopes.add(scope);
|
|
2093
|
-
if (target)
|
|
2094
|
-
memory.targets.add(target);
|
|
2095
|
-
}
|
|
2096
|
-
settlePendingPermission(sessionId, requestId, result) {
|
|
2097
|
-
const waiter = this.pendingPermissions.get(sessionId);
|
|
2098
|
-
if (!waiter)
|
|
2099
|
-
return;
|
|
2100
|
-
if (requestId !== undefined && waiter.requestId !== requestId)
|
|
2101
|
-
return;
|
|
2102
|
-
this.pendingPermissions.delete(sessionId);
|
|
2103
|
-
waiter.resolve(result);
|
|
2104
|
-
}
|
|
2105
|
-
async handleCanUseTool(sessionId, requestId, toolName, input, ctx) {
|
|
2106
|
-
const deny = (message) => structuredPermissionDenied(message);
|
|
2107
|
-
if (ctx.signal.aborted || this.disposed)
|
|
2108
|
-
return deny("已取消");
|
|
2109
|
-
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
2110
|
-
if (!current)
|
|
2111
|
-
return deny("会话已结束");
|
|
2112
|
-
const inferred = inferStructuredEscalation(toolName, input, ctx);
|
|
2113
|
-
if (current.autoApprovePermissions) {
|
|
2114
|
-
this.incrementApprovalStats(current, inferred.scope);
|
|
2115
|
-
this.storage.updateSessionRuntimeMetadata(current);
|
|
2116
|
-
return { behavior: "allow" };
|
|
2117
|
-
}
|
|
2118
|
-
const memory = this.turnApprovalMemory.get(sessionId);
|
|
2119
|
-
if (memory && (memory.scopes.has(inferred.scope) || (inferred.target && memory.targets.has(inferred.target)))) {
|
|
2120
|
-
return { behavior: "allow" };
|
|
2121
|
-
}
|
|
2122
|
-
const existing = this.pendingPermissions.get(sessionId);
|
|
2123
|
-
if (existing) {
|
|
2124
|
-
await new Promise((resolve) => {
|
|
2125
|
-
const previous = existing.resolve;
|
|
2126
|
-
existing.resolve = (result) => {
|
|
2127
|
-
previous(result);
|
|
2128
|
-
resolve(result);
|
|
2129
|
-
};
|
|
2130
|
-
});
|
|
2131
|
-
if (ctx.signal.aborted || this.disposed)
|
|
2132
|
-
return deny("已取消");
|
|
2133
|
-
return this.handleCanUseTool(sessionId, requestId, toolName, input, ctx);
|
|
2134
|
-
}
|
|
2135
|
-
const escalation = {
|
|
2136
|
-
requestId: ctx.toolUseID ? `sdk-${ctx.toolUseID}` : `sdk-${Date.now()}`,
|
|
2137
|
-
scope: inferred.scope,
|
|
2138
|
-
runner: "json",
|
|
2139
|
-
source: "tool_permission_request",
|
|
2140
|
-
target: inferred.target,
|
|
2141
|
-
reason: inferred.reason,
|
|
2142
|
-
};
|
|
2143
|
-
return await new Promise((resolve) => {
|
|
2144
|
-
let settled = false;
|
|
2145
|
-
const finish = (result) => {
|
|
2146
|
-
if (settled)
|
|
2147
|
-
return;
|
|
2148
|
-
settled = true;
|
|
2149
|
-
ctx.signal.removeEventListener("abort", onAbort);
|
|
2150
|
-
if (this.pendingPermissions.get(sessionId)?.requestId === escalation.requestId) {
|
|
2151
|
-
this.pendingPermissions.delete(sessionId);
|
|
2152
|
-
}
|
|
2153
|
-
resolve(result);
|
|
2154
|
-
};
|
|
2155
|
-
const onAbort = () => {
|
|
2156
|
-
finish(deny("已取消"));
|
|
2157
|
-
const latest = this.currentSessionForRequest(sessionId, requestId);
|
|
2158
|
-
if (latest?.pendingEscalation?.requestId === escalation.requestId) {
|
|
2159
|
-
const cleared = {
|
|
2160
|
-
...latest,
|
|
2161
|
-
pendingEscalation: null,
|
|
2162
|
-
permissionBlocked: false,
|
|
2163
|
-
};
|
|
2164
|
-
this.sessions.set(sessionId, cleared);
|
|
2165
|
-
this.storage.updateSessionRuntimeMetadata(cleared);
|
|
2166
|
-
this.emit({
|
|
2167
|
-
type: "status",
|
|
2168
|
-
sessionId,
|
|
2169
|
-
data: { permissionBlocked: false, sessionKind: "structured" },
|
|
2170
|
-
});
|
|
2171
|
-
}
|
|
2172
|
-
};
|
|
2173
|
-
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
2174
|
-
this.pendingPermissions.set(sessionId, { requestId: escalation.requestId, resolve: finish });
|
|
2175
|
-
const latest = this.currentSessionForRequest(sessionId, requestId);
|
|
2176
|
-
if (!latest || ctx.signal.aborted) {
|
|
2177
|
-
finish(deny("会话已结束"));
|
|
2178
|
-
return;
|
|
2179
|
-
}
|
|
2180
|
-
const updated = {
|
|
2181
|
-
...latest,
|
|
2182
|
-
pendingEscalation: escalation,
|
|
2183
|
-
permissionBlocked: true,
|
|
2184
|
-
};
|
|
2185
|
-
this.sessions.set(sessionId, updated);
|
|
2186
|
-
this.storage.updateSessionRuntimeMetadata(updated);
|
|
2187
|
-
this.emit({
|
|
2188
|
-
type: "status",
|
|
2189
|
-
sessionId,
|
|
2190
|
-
data: {
|
|
2191
|
-
sessionKind: "structured",
|
|
2192
|
-
permissionBlocked: true,
|
|
2193
|
-
permissionRequest: {
|
|
2194
|
-
scope: escalation.scope,
|
|
2195
|
-
target: escalation.target,
|
|
2196
|
-
prompt: escalation.reason,
|
|
2197
|
-
},
|
|
2198
|
-
pendingEscalation: escalation,
|
|
2199
|
-
},
|
|
2200
|
-
});
|
|
2201
|
-
});
|
|
2202
|
-
}
|
|
2203
1989
|
// ---------------------------------------------------------------------------
|
|
2204
1990
|
// Streaming codex exec --json execution
|
|
2205
1991
|
// ---------------------------------------------------------------------------
|
|
@@ -2328,7 +2114,6 @@ export class StructuredSessionManager {
|
|
|
2328
2114
|
claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
|
|
2329
2115
|
messages,
|
|
2330
2116
|
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2331
|
-
queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2332
2117
|
pendingEscalation: null,
|
|
2333
2118
|
permissionBlocked: false,
|
|
2334
2119
|
structuredState: {
|
|
@@ -2345,12 +2130,10 @@ export class StructuredSessionManager {
|
|
|
2345
2130
|
if (!keepRunning)
|
|
2346
2131
|
this.emitStructuredSnapshot(finished, "ended");
|
|
2347
2132
|
if (interruptPrompt) {
|
|
2348
|
-
const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
|
|
2349
2133
|
this.interruptedWith.delete(sessionId);
|
|
2350
|
-
this.interruptedSkills.delete(sessionId);
|
|
2351
2134
|
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
2352
2135
|
setImmediate(() => {
|
|
2353
|
-
this.sendMessage(sessionId, interruptPrompt
|
|
2136
|
+
this.sendMessage(sessionId, interruptPrompt).catch((error) => {
|
|
2354
2137
|
console.error("[WAND] codex interrupt-and-send failed:", error);
|
|
2355
2138
|
});
|
|
2356
2139
|
});
|
|
@@ -2473,7 +2256,6 @@ export class StructuredSessionManager {
|
|
|
2473
2256
|
claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
|
|
2474
2257
|
messages,
|
|
2475
2258
|
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2476
|
-
queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2477
2259
|
pendingEscalation: null,
|
|
2478
2260
|
permissionBlocked: false,
|
|
2479
2261
|
structuredState: {
|
|
@@ -2490,11 +2272,9 @@ export class StructuredSessionManager {
|
|
|
2490
2272
|
if (!keepRunning)
|
|
2491
2273
|
this.emitStructuredSnapshot(finished, "ended");
|
|
2492
2274
|
if (interruptPrompt) {
|
|
2493
|
-
const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
|
|
2494
2275
|
this.interruptedWith.delete(sessionId);
|
|
2495
|
-
this.interruptedSkills.delete(sessionId);
|
|
2496
2276
|
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
2497
|
-
setImmediate(() => this.sendMessage(sessionId, interruptPrompt
|
|
2277
|
+
setImmediate(() => this.sendMessage(sessionId, interruptPrompt).catch((error) => {
|
|
2498
2278
|
console.error("[WAND] grok interrupt-and-send failed:", error);
|
|
2499
2279
|
}));
|
|
2500
2280
|
return;
|
|
@@ -2624,7 +2404,6 @@ export class StructuredSessionManager {
|
|
|
2624
2404
|
claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
|
|
2625
2405
|
messages,
|
|
2626
2406
|
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2627
|
-
queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2628
2407
|
pendingEscalation: null,
|
|
2629
2408
|
permissionBlocked: false,
|
|
2630
2409
|
structuredState: {
|
|
@@ -2641,12 +2420,10 @@ export class StructuredSessionManager {
|
|
|
2641
2420
|
if (!keepRunning)
|
|
2642
2421
|
this.emitStructuredSnapshot(finished, "ended");
|
|
2643
2422
|
if (interruptPrompt) {
|
|
2644
|
-
const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
|
|
2645
2423
|
this.interruptedWith.delete(sessionId);
|
|
2646
|
-
this.interruptedSkills.delete(sessionId);
|
|
2647
2424
|
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
2648
2425
|
setImmediate(() => {
|
|
2649
|
-
this.sendMessage(sessionId, interruptPrompt
|
|
2426
|
+
this.sendMessage(sessionId, interruptPrompt).catch((error) => {
|
|
2650
2427
|
console.error("[WAND] opencode interrupt-and-send failed:", error);
|
|
2651
2428
|
});
|
|
2652
2429
|
});
|
|
@@ -2816,7 +2593,6 @@ export class StructuredSessionManager {
|
|
|
2816
2593
|
claudeSessionId: result.state.sessionId ?? current.claudeSessionId,
|
|
2817
2594
|
messages,
|
|
2818
2595
|
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2819
|
-
queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2820
2596
|
pendingEscalation: null,
|
|
2821
2597
|
permissionBlocked: false,
|
|
2822
2598
|
structuredState: {
|
|
@@ -2834,12 +2610,10 @@ export class StructuredSessionManager {
|
|
|
2834
2610
|
if (!keepRunning)
|
|
2835
2611
|
this.emitStructuredSnapshot(finished, "ended");
|
|
2836
2612
|
if (interruptPrompt) {
|
|
2837
|
-
const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
|
|
2838
2613
|
this.interruptedWith.delete(sessionId);
|
|
2839
|
-
this.interruptedSkills.delete(sessionId);
|
|
2840
2614
|
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
2841
2615
|
setImmediate(() => {
|
|
2842
|
-
this.sendMessage(sessionId, interruptPrompt
|
|
2616
|
+
this.sendMessage(sessionId, interruptPrompt).catch((error) => {
|
|
2843
2617
|
console.error("[WAND] interrupt-and-send failed:", error);
|
|
2844
2618
|
});
|
|
2845
2619
|
});
|
|
@@ -2863,494 +2637,6 @@ export class StructuredSessionManager {
|
|
|
2863
2637
|
setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
|
|
2864
2638
|
}
|
|
2865
2639
|
// ---------------------------------------------------------------------------
|
|
2866
|
-
// Streaming claude-agent-sdk execution
|
|
2867
|
-
// ---------------------------------------------------------------------------
|
|
2868
|
-
/**
|
|
2869
|
-
* Use @anthropic-ai/claude-agent-sdk instead of spawning claude -p directly.
|
|
2870
|
-
* The SDK still spawns the claude binary but provides typed AsyncGenerator<SDKMessage>
|
|
2871
|
-
* messages, so we skip NDJSON parsing. Options are 1:1 with the CLI flags.
|
|
2872
|
-
*
|
|
2873
|
-
* Streaming is enabled via includePartialMessages: true — the SDK emits
|
|
2874
|
-
* SDKPartialAssistantMessage (type: "stream_event") with BetaRawMessageStreamEvent
|
|
2875
|
-
* payloads for incremental text/thinking/tool_use updates, followed by a final
|
|
2876
|
-
* SDKAssistantMessage with the authoritative complete content.
|
|
2877
|
-
*/
|
|
2878
|
-
async runClaudeSdkStreaming(sessionId, session, prompt, requestId, skills) {
|
|
2879
|
-
const abortController = new AbortController();
|
|
2880
|
-
this.pendingSdkAbort.set(sessionId, abortController);
|
|
2881
|
-
this.turnApprovalMemory.delete(sessionId);
|
|
2882
|
-
const isManaged = session.mode === "managed";
|
|
2883
|
-
let killedForAskUserQuestion = false;
|
|
2884
|
-
// 权限策略 + 系统提示词都通过共享 helper 派生,与 CLI runner 一字不差。
|
|
2885
|
-
const permPolicy = derivePermissionPolicy(session.mode, session.autoApprovePermissions ?? false, session.cwd);
|
|
2886
|
-
const systemPromptParts = buildSessionSystemPromptParts(session, this.config.language);
|
|
2887
|
-
const sdkClaudeBinary = resolveSdkClaudeBinary();
|
|
2888
|
-
// SDK 默认会把整个 process.env 透传给 claude 子进程;这里显式按 inheritEnv 配置组装,
|
|
2889
|
-
// 否则关闭"继承环境变量"开关时 SDK 路径会被静默忽略。
|
|
2890
|
-
const sdkEnv = buildChildEnv(this.config.inheritEnv !== false);
|
|
2891
|
-
const sdkThinking = buildClaudeSdkThinking(session.thinkingEffort);
|
|
2892
|
-
const sdkOptions = {
|
|
2893
|
-
cwd: session.cwd,
|
|
2894
|
-
abortController,
|
|
2895
|
-
env: sdkEnv,
|
|
2896
|
-
permissionMode: permPolicy.permissionMode,
|
|
2897
|
-
...(permPolicy.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {}),
|
|
2898
|
-
...(permPolicy.permissionMode !== "bypassPermissions"
|
|
2899
|
-
? {
|
|
2900
|
-
canUseTool: (toolName, input, ctx) => this.handleCanUseTool(sessionId, requestId, toolName, input, ctx),
|
|
2901
|
-
}
|
|
2902
|
-
: {}),
|
|
2903
|
-
...(permPolicy.allowedTools ? { allowedTools: permPolicy.allowedTools } : {}),
|
|
2904
|
-
...(isManaged ? { disallowedTools: ["AskUserQuestion"] } : {}),
|
|
2905
|
-
skills,
|
|
2906
|
-
thinking: sdkThinking,
|
|
2907
|
-
includePartialMessages: true,
|
|
2908
|
-
// 把子 agent 的 text/thinking 也转发回来,UI 才能把"被 Task 召唤来的协作者"
|
|
2909
|
-
// 渲染成独立角色的群聊消息。关掉这个开关时只会收到子 agent 的 tool_use/tool_result,
|
|
2910
|
-
// text/thinking 被 SDK 吞掉。
|
|
2911
|
-
forwardSubagentText: true,
|
|
2912
|
-
...(systemPromptParts.length > 0 ? { appendSystemPrompt: systemPromptParts.join("\n\n") } : {}),
|
|
2913
|
-
...(sdkClaudeBinary ? { pathToClaudeCodeExecutable: sdkClaudeBinary } : {}),
|
|
2914
|
-
};
|
|
2915
|
-
if (session.claudeSessionId)
|
|
2916
|
-
sdkOptions.resume = session.claudeSessionId;
|
|
2917
|
-
const modelChoice = session.selectedModel?.trim();
|
|
2918
|
-
if (modelChoice && modelChoice !== "default")
|
|
2919
|
-
sdkOptions.model = modelChoice;
|
|
2920
|
-
// Streaming input mode:把这一轮的 user turn 重建成一条 SDKUserMessage 喂给 SDK。
|
|
2921
|
-
// 上层 sendMessage 已经把 userTurn 写进 session.messages 末尾——如果它的内容是
|
|
2922
|
-
// tool_result,说明本次是用户在回答上一轮 AskUserQuestion,否则就是普通文本。
|
|
2923
|
-
// 走 streaming input 而非 string prompt 的好处:tool_result 是真的 tool_result
|
|
2924
|
-
// block,对 Claude 来说就是标准工具回传,不需要 "[对刚才工具的回答…]" 这种文本
|
|
2925
|
-
// 提示让模型脑补语义。
|
|
2926
|
-
const lastUserTurn = (session.messages ?? []).slice().reverse().find((m) => m.role === "user");
|
|
2927
|
-
const lastUserBlock = lastUserTurn?.content?.[0];
|
|
2928
|
-
let sdkInitialMessage;
|
|
2929
|
-
if (lastUserBlock?.type === "tool_result") {
|
|
2930
|
-
// Anthropic 的 tool_result.content 原生支持 string 或 content-block 数组(text/image
|
|
2931
|
-
// 等)。wand 内部 ToolResultBlock 的 array 形态是 `{type: string; ...}` 比官方 union
|
|
2932
|
-
// 宽,但实际取值都是 `{type: "text", text}`,结构上兼容;用 `as` 把宽类型缩到 SDK
|
|
2933
|
-
// 接受的形态即可,比 JSON.stringify 把数组拍成一坨 JSON 文本更忠实。
|
|
2934
|
-
sdkInitialMessage = {
|
|
2935
|
-
type: "user",
|
|
2936
|
-
message: {
|
|
2937
|
-
role: "user",
|
|
2938
|
-
content: [
|
|
2939
|
-
{
|
|
2940
|
-
type: "tool_result",
|
|
2941
|
-
tool_use_id: lastUserBlock.tool_use_id,
|
|
2942
|
-
content: lastUserBlock.content,
|
|
2943
|
-
is_error: lastUserBlock.is_error === true,
|
|
2944
|
-
},
|
|
2945
|
-
],
|
|
2946
|
-
},
|
|
2947
|
-
parent_tool_use_id: null,
|
|
2948
|
-
};
|
|
2949
|
-
}
|
|
2950
|
-
else {
|
|
2951
|
-
sdkInitialMessage = {
|
|
2952
|
-
type: "user",
|
|
2953
|
-
message: {
|
|
2954
|
-
role: "user",
|
|
2955
|
-
content: [{ type: "text", text: prompt }],
|
|
2956
|
-
},
|
|
2957
|
-
parent_tool_use_id: null,
|
|
2958
|
-
};
|
|
2959
|
-
}
|
|
2960
|
-
async function* singleShotPrompt() {
|
|
2961
|
-
yield sdkInitialMessage;
|
|
2962
|
-
}
|
|
2963
|
-
const turnState = {
|
|
2964
|
-
blocks: [],
|
|
2965
|
-
result: "",
|
|
2966
|
-
sessionId: null,
|
|
2967
|
-
model: undefined,
|
|
2968
|
-
usage: undefined,
|
|
2969
|
-
};
|
|
2970
|
-
// Tracks in-progress streaming blocks keyed by content_block index from stream_event.
|
|
2971
|
-
// The map is cleared whenever a complete `assistant` message arrives — its blocks
|
|
2972
|
-
// are then promoted into `finalizedBlocks` below.
|
|
2973
|
-
//
|
|
2974
|
-
// `parentToolUseId` carries through from SDKPartialAssistantMessage so we can
|
|
2975
|
-
// stamp streaming blocks with subagent persona *during* streaming, not only
|
|
2976
|
-
// after the completion event. Without it, subagent text shows up under the
|
|
2977
|
-
// parent's avatar for tens of ms then snaps to the subagent — visible flicker.
|
|
2978
|
-
const streamingBlockByIndex = new Map();
|
|
2979
|
-
// Blocks from messages that have already completed within this turn — including
|
|
2980
|
-
// the parent assistant's prior messages, every subagent assistant message, and
|
|
2981
|
-
// every tool_result. Subagent (Task tool) flows produce many assistant messages
|
|
2982
|
-
// back-to-back; without this list, each new streaming message would visually
|
|
2983
|
-
// erase everything that came before it in the same turn.
|
|
2984
|
-
const finalizedBlocks = [];
|
|
2985
|
-
// Per-turn Task tool_use_id → meta map; populated from the parent assistant's
|
|
2986
|
-
// Task tool_use blocks and consulted when subagent messages arrive.
|
|
2987
|
-
const taskMetaRegistry = new Map();
|
|
2988
|
-
let emitTimer = null;
|
|
2989
|
-
const flushEmit = () => {
|
|
2990
|
-
if (emitTimer) {
|
|
2991
|
-
this.clearStreamEmitTimer(emitTimer);
|
|
2992
|
-
emitTimer = null;
|
|
2993
|
-
}
|
|
2994
|
-
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
2995
|
-
if (!current)
|
|
2996
|
-
return;
|
|
2997
|
-
this.emit({ type: "output", sessionId, data: buildIncrementalStructuredPayload(current, this.config.cardDefaults ?? {}) });
|
|
2998
|
-
};
|
|
2999
|
-
const scheduleEmit = () => {
|
|
3000
|
-
if (!emitTimer)
|
|
3001
|
-
emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
|
|
3002
|
-
};
|
|
3003
|
-
// Rebuild ContentBlock[] from finalized history + the in-progress streaming map.
|
|
3004
|
-
// Returning only the streaming blocks would drop every prior parent/subagent
|
|
3005
|
-
// message in this turn (the original disappearing-output bug).
|
|
3006
|
-
const rebuildStreamingBlocks = () => {
|
|
3007
|
-
const sorted = [...streamingBlockByIndex.entries()].sort((a, b) => a[0] - b[0]);
|
|
3008
|
-
const streaming = [];
|
|
3009
|
-
for (const [, sb] of sorted) {
|
|
3010
|
-
let block = null;
|
|
3011
|
-
if (sb.type === "text") {
|
|
3012
|
-
block = { type: "text", text: sb.text };
|
|
3013
|
-
}
|
|
3014
|
-
else if (sb.type === "thinking") {
|
|
3015
|
-
block = { type: "thinking", thinking: sb.thinking };
|
|
3016
|
-
}
|
|
3017
|
-
else if (sb.type === "tool_use" && sb.id && sb.name) {
|
|
3018
|
-
let input = {};
|
|
3019
|
-
if (sb.finalized && sb.partialInput) {
|
|
3020
|
-
try {
|
|
3021
|
-
input = JSON.parse(sb.partialInput);
|
|
3022
|
-
}
|
|
3023
|
-
catch { /* partial json */ }
|
|
3024
|
-
}
|
|
3025
|
-
block = { type: "tool_use", id: sb.id, name: sb.name, input: normalizeClaudeToolInput(sb.name, input) };
|
|
3026
|
-
}
|
|
3027
|
-
if (!block)
|
|
3028
|
-
continue;
|
|
3029
|
-
if (sb.parentToolUseId) {
|
|
3030
|
-
const [stamped] = tagSubagentBlocks([block], sb.parentToolUseId, taskMetaRegistry);
|
|
3031
|
-
streaming.push(stamped);
|
|
3032
|
-
}
|
|
3033
|
-
else {
|
|
3034
|
-
streaming.push(block);
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3037
|
-
// 流式阶段就给 Task/Agent tool_use 本身盖章,防止"先显示工具卡片几秒再跳为
|
|
3038
|
-
// handoff 行"的闪烁。content_block_start 阶段就有 name=Task/Agent,
|
|
3039
|
-
// stampSelfTask 据此即可命中;agentType 字段藏在 input 里,delta 累计后再由
|
|
3040
|
-
// 后续 captureTaskMeta 回填 registry,下次 rebuild 自动补上更完整的 stamp。
|
|
3041
|
-
captureTaskMeta(streaming, taskMetaRegistry);
|
|
3042
|
-
const stampedStreaming = stampSelfTask(streaming, taskMetaRegistry);
|
|
3043
|
-
return [...finalizedBlocks, ...stampedStreaming];
|
|
3044
|
-
};
|
|
3045
|
-
const syncSnapshot = () => {
|
|
3046
|
-
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
3047
|
-
if (!current)
|
|
3048
|
-
return;
|
|
3049
|
-
const inProgressTurn = {
|
|
3050
|
-
role: "assistant",
|
|
3051
|
-
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
3052
|
-
usage: turnState.usage,
|
|
3053
|
-
};
|
|
3054
|
-
const msgs = upsertAssistantMessage(current.messages, inProgressTurn);
|
|
3055
|
-
const patched = {
|
|
3056
|
-
...current,
|
|
3057
|
-
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
3058
|
-
messages: msgs,
|
|
3059
|
-
output: turnState.result || current.output,
|
|
3060
|
-
structuredState: {
|
|
3061
|
-
...current.structuredState,
|
|
3062
|
-
model: turnState.model ?? current.structuredState?.model,
|
|
3063
|
-
},
|
|
3064
|
-
};
|
|
3065
|
-
this.sessions.set(sessionId, patched);
|
|
3066
|
-
this.saveStreamingSnapshot(patched);
|
|
3067
|
-
};
|
|
3068
|
-
const spawnedAt = new Date().toISOString();
|
|
3069
|
-
this.logger?.appendStructuredSpawn(sessionId, {
|
|
3070
|
-
kind: "claude-sdk",
|
|
3071
|
-
provider: "claude",
|
|
3072
|
-
cwd: session.cwd,
|
|
3073
|
-
permissionMode: permPolicy.permissionMode,
|
|
3074
|
-
prompt: prompt.slice(0, 2048),
|
|
3075
|
-
promptLength: prompt.length,
|
|
3076
|
-
claudeSessionId: session.claudeSessionId,
|
|
3077
|
-
spawnedAt,
|
|
3078
|
-
});
|
|
3079
|
-
let queryHandle;
|
|
3080
|
-
try {
|
|
3081
|
-
queryHandle = this.sdkQueryFactory({ prompt: singleShotPrompt(), options: sdkOptions });
|
|
3082
|
-
}
|
|
3083
|
-
catch (error) {
|
|
3084
|
-
this.releasePendingSdkAbort(sessionId, abortController);
|
|
3085
|
-
throw error;
|
|
3086
|
-
}
|
|
3087
|
-
this.pendingSdkQueries.set(sessionId, queryHandle);
|
|
3088
|
-
try {
|
|
3089
|
-
for await (const msg of queryHandle) {
|
|
3090
|
-
if (abortController.signal.aborted || !this.isCurrentRequest(sessionId, requestId))
|
|
3091
|
-
break;
|
|
3092
|
-
// 同 CLI runner 的关键修复:从任何带 session_id 的 SDK 消息(system / assistant /
|
|
3093
|
-
// user / result)即时捕获并落库。AskUserQuestion 的 interrupt 发生在 assistant
|
|
3094
|
-
// 之后、result 之前,若只在 result 捕获,被 interrupt 的轮次会丢掉 session_id,
|
|
3095
|
-
// 续接时不 resume → 上下文丢失。stream_event 等无 session_id 的消息被 guard 跳过。
|
|
3096
|
-
const msgSessionId = msg.session_id;
|
|
3097
|
-
if (typeof msgSessionId === "string" && msgSessionId && turnState.sessionId !== msgSessionId) {
|
|
3098
|
-
turnState.sessionId = msgSessionId;
|
|
3099
|
-
const cur = this.currentSessionForRequest(sessionId, requestId);
|
|
3100
|
-
if (cur && cur.claudeSessionId !== msgSessionId) {
|
|
3101
|
-
const patched = { ...cur, claudeSessionId: msgSessionId };
|
|
3102
|
-
this.sessions.set(sessionId, patched);
|
|
3103
|
-
this.saveStreamingSnapshot(patched, { metadata: true });
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
// Incremental streaming events (opt-in via includePartialMessages: true)
|
|
3107
|
-
if (msg.type === "stream_event") {
|
|
3108
|
-
const partial = msg;
|
|
3109
|
-
const ev = partial.event;
|
|
3110
|
-
const partialParentId = partial.parent_tool_use_id ?? null;
|
|
3111
|
-
if (ev.type === "content_block_start") {
|
|
3112
|
-
const cb = ev.content_block;
|
|
3113
|
-
const blockType = cb.type;
|
|
3114
|
-
if (blockType === "text" || blockType === "thinking" || blockType === "tool_use") {
|
|
3115
|
-
streamingBlockByIndex.set(ev.index, {
|
|
3116
|
-
type: blockType,
|
|
3117
|
-
id: typeof cb.id === "string" ? cb.id : undefined,
|
|
3118
|
-
name: typeof cb.name === "string" ? cb.name : undefined,
|
|
3119
|
-
text: typeof cb.text === "string" ? cb.text : "",
|
|
3120
|
-
thinking: typeof cb.thinking === "string" ? cb.thinking : "",
|
|
3121
|
-
partialInput: "",
|
|
3122
|
-
finalized: false,
|
|
3123
|
-
parentToolUseId: partialParentId,
|
|
3124
|
-
});
|
|
3125
|
-
turnState.blocks = rebuildStreamingBlocks();
|
|
3126
|
-
syncSnapshot();
|
|
3127
|
-
scheduleEmit();
|
|
3128
|
-
}
|
|
3129
|
-
}
|
|
3130
|
-
else if (ev.type === "content_block_delta") {
|
|
3131
|
-
const sb = streamingBlockByIndex.get(ev.index);
|
|
3132
|
-
if (sb) {
|
|
3133
|
-
const delta = ev.delta;
|
|
3134
|
-
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
3135
|
-
sb.text += delta.text;
|
|
3136
|
-
turnState.result = sb.text;
|
|
3137
|
-
}
|
|
3138
|
-
else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
3139
|
-
sb.thinking += delta.thinking;
|
|
3140
|
-
}
|
|
3141
|
-
else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
3142
|
-
sb.partialInput += delta.partial_json;
|
|
3143
|
-
}
|
|
3144
|
-
turnState.blocks = rebuildStreamingBlocks();
|
|
3145
|
-
syncSnapshot();
|
|
3146
|
-
scheduleEmit();
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
else if (ev.type === "content_block_stop") {
|
|
3150
|
-
const sb = streamingBlockByIndex.get(ev.index);
|
|
3151
|
-
if (sb) {
|
|
3152
|
-
sb.finalized = true;
|
|
3153
|
-
turnState.blocks = rebuildStreamingBlocks();
|
|
3154
|
-
syncSnapshot();
|
|
3155
|
-
scheduleEmit();
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
continue;
|
|
3159
|
-
}
|
|
3160
|
-
// Complete assistant turn — promote streaming content into the finalized
|
|
3161
|
-
// history so subsequent messages (subagents, follow-up parent messages)
|
|
3162
|
-
// append to it instead of erasing it.
|
|
3163
|
-
if (msg.type === "assistant") {
|
|
3164
|
-
const assistantMsg = msg;
|
|
3165
|
-
const extracted = extractClaudeAssistantMessage(assistantMsg.message);
|
|
3166
|
-
// 父 assistant 的 Task tool_use → 注册到本轮 taskMeta map;
|
|
3167
|
-
// 子 agent 的 message(parent_tool_use_id 非空)→ 给每个 block 盖章。
|
|
3168
|
-
const parentToolUseId = assistantMsg.parent_tool_use_id ?? null;
|
|
3169
|
-
if (parentToolUseId === null) {
|
|
3170
|
-
captureTaskMeta(extracted.content, taskMetaRegistry);
|
|
3171
|
-
finalizedBlocks.push(...stampSelfTask(extracted.content, taskMetaRegistry));
|
|
3172
|
-
}
|
|
3173
|
-
else {
|
|
3174
|
-
finalizedBlocks.push(...tagSubagentBlocks(extracted.content, parentToolUseId, taskMetaRegistry));
|
|
3175
|
-
}
|
|
3176
|
-
streamingBlockByIndex.clear();
|
|
3177
|
-
turnState.blocks = rebuildStreamingBlocks();
|
|
3178
|
-
if (assistantMsg.session_id)
|
|
3179
|
-
turnState.sessionId = assistantMsg.session_id;
|
|
3180
|
-
syncSnapshot();
|
|
3181
|
-
scheduleEmit();
|
|
3182
|
-
// Non-managed mode: detect AskUserQuestion. Prefer query.interrupt()
|
|
3183
|
-
// (streaming input mode 的 control message,让 SDK 优雅地停掉当前 turn)
|
|
3184
|
-
// 而不是 abortController.abort()——abort 会让 SDK throw AbortError,整段
|
|
3185
|
-
// try/catch 走异常路径;interrupt 让 for-await 自然结束,行为更干净。
|
|
3186
|
-
// 失败时 fallback 到 abort,保证一定能跳出。
|
|
3187
|
-
//
|
|
3188
|
-
// 注意:interrupt 之后下一次 sendMessage 会重新 spawn 一次 SDK 调用并通过
|
|
3189
|
-
// resume 续接 + tool_result block 回答,不用文本伪造。
|
|
3190
|
-
if (!isManaged && !killedForAskUserQuestion) {
|
|
3191
|
-
const askBlock = extracted.content.find((b) => b.type === "tool_use" && b.name === "AskUserQuestion");
|
|
3192
|
-
if (askBlock) {
|
|
3193
|
-
killedForAskUserQuestion = true;
|
|
3194
|
-
flushEmit();
|
|
3195
|
-
try {
|
|
3196
|
-
await queryHandle.interrupt();
|
|
3197
|
-
}
|
|
3198
|
-
catch (_err) {
|
|
3199
|
-
// interrupt 在某些情况下(已经结束 / SDK 版本不支持)会 reject,
|
|
3200
|
-
// 兜底用 abort 强制退出。
|
|
3201
|
-
abortController.abort();
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
continue;
|
|
3206
|
-
}
|
|
3207
|
-
// Tool results fed back from the claude subprocess (parent's view of a
|
|
3208
|
-
// tool call, or a subagent's tool_result during Task execution).
|
|
3209
|
-
if (msg.type === "user") {
|
|
3210
|
-
const userMsg = msg;
|
|
3211
|
-
const parentToolUseId = userMsg.parent_tool_use_id ?? null;
|
|
3212
|
-
const content = Array.isArray(userMsg.message?.content) ? userMsg.message.content : [];
|
|
3213
|
-
const collected = [];
|
|
3214
|
-
for (const block of content) {
|
|
3215
|
-
const b = block;
|
|
3216
|
-
if (b?.type === "tool_result") {
|
|
3217
|
-
collected.push({
|
|
3218
|
-
type: "tool_result",
|
|
3219
|
-
tool_use_id: typeof b.tool_use_id === "string" ? b.tool_use_id : "",
|
|
3220
|
-
content: this.normalizeToolResultContent(b.content),
|
|
3221
|
-
is_error: b.is_error === true,
|
|
3222
|
-
});
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
if (parentToolUseId === null) {
|
|
3226
|
-
finalizedBlocks.push(...stampParentTaskResults(collected, taskMetaRegistry));
|
|
3227
|
-
}
|
|
3228
|
-
else {
|
|
3229
|
-
finalizedBlocks.push(...tagSubagentBlocks(collected, parentToolUseId, taskMetaRegistry));
|
|
3230
|
-
}
|
|
3231
|
-
turnState.blocks = rebuildStreamingBlocks();
|
|
3232
|
-
syncSnapshot();
|
|
3233
|
-
scheduleEmit();
|
|
3234
|
-
continue;
|
|
3235
|
-
}
|
|
3236
|
-
// Final result — capture session_id, usage, model
|
|
3237
|
-
if (msg.type === "result") {
|
|
3238
|
-
const resultMsg = msg;
|
|
3239
|
-
if (typeof resultMsg.result === "string")
|
|
3240
|
-
turnState.result = resultMsg.result.trim();
|
|
3241
|
-
if (typeof resultMsg.session_id === "string")
|
|
3242
|
-
turnState.sessionId = resultMsg.session_id;
|
|
3243
|
-
turnState.model = extractClaudeModelName(resultMsg.modelUsage) ?? turnState.model;
|
|
3244
|
-
turnState.usage = this.extractSdkUsage(resultMsg);
|
|
3245
|
-
syncSnapshot();
|
|
3246
|
-
scheduleEmit();
|
|
3247
|
-
continue;
|
|
3248
|
-
}
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
catch (err) {
|
|
3252
|
-
// AbortError from abortController.abort() is intentional — fall through to finish logic
|
|
3253
|
-
const isAbort = abortController.signal.aborted || (err instanceof Error && err.name === "AbortError");
|
|
3254
|
-
if (!isAbort) {
|
|
3255
|
-
const releasedAbort = this.releasePendingSdkAbort(sessionId, abortController);
|
|
3256
|
-
const releasedQuery = this.releasePendingSdkQuery(sessionId, queryHandle);
|
|
3257
|
-
if (releasedAbort || releasedQuery)
|
|
3258
|
-
this.cancelStreamingCheckpointTimer(sessionId);
|
|
3259
|
-
if (emitTimer)
|
|
3260
|
-
this.clearStreamEmitTimer(emitTimer);
|
|
3261
|
-
if (!this.isCurrentRequest(sessionId, requestId))
|
|
3262
|
-
return;
|
|
3263
|
-
this.logger?.appendStructuredSpawn(sessionId, {
|
|
3264
|
-
kind: "claude-sdk-error",
|
|
3265
|
-
spawnedAt,
|
|
3266
|
-
closedAt: new Date().toISOString(),
|
|
3267
|
-
error: getErrorMessage(err),
|
|
3268
|
-
});
|
|
3269
|
-
throw err;
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
// Cleanup
|
|
3273
|
-
this.settlePendingPermission(sessionId, undefined, structuredPermissionDenied("本轮已结束"));
|
|
3274
|
-
this.turnApprovalMemory.delete(sessionId);
|
|
3275
|
-
const releasedAbort = this.releasePendingSdkAbort(sessionId, abortController);
|
|
3276
|
-
const releasedQuery = this.releasePendingSdkQuery(sessionId, queryHandle);
|
|
3277
|
-
if (releasedAbort || releasedQuery)
|
|
3278
|
-
this.cancelStreamingCheckpointTimer(sessionId);
|
|
3279
|
-
if (emitTimer)
|
|
3280
|
-
this.clearStreamEmitTimer(emitTimer);
|
|
3281
|
-
if (!this.isCurrentRequest(sessionId, requestId))
|
|
3282
|
-
return;
|
|
3283
|
-
flushEmit();
|
|
3284
|
-
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
3285
|
-
if (!current)
|
|
3286
|
-
return;
|
|
3287
|
-
this.logger?.appendStructuredSpawn(sessionId, {
|
|
3288
|
-
kind: "claude-sdk-close",
|
|
3289
|
-
spawnedAt,
|
|
3290
|
-
closedAt: new Date().toISOString(),
|
|
3291
|
-
killedForAskUserQuestion,
|
|
3292
|
-
sessionId: turnState.sessionId,
|
|
3293
|
-
});
|
|
3294
|
-
const msgs = this.buildCompletedAssistantMessages(current, turnState);
|
|
3295
|
-
const interruptPrompt = this.interruptedWith.get(sessionId);
|
|
3296
|
-
const keepRunning = killedForAskUserQuestion || !!interruptPrompt;
|
|
3297
|
-
const finished = {
|
|
3298
|
-
...current,
|
|
3299
|
-
status: keepRunning ? "running" : "idle",
|
|
3300
|
-
exitCode: keepRunning ? null : 0,
|
|
3301
|
-
endedAt: keepRunning ? null : new Date().toISOString(),
|
|
3302
|
-
output: turnState.result,
|
|
3303
|
-
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
3304
|
-
messages: msgs,
|
|
3305
|
-
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
3306
|
-
queuedMessageSkills: this.resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt),
|
|
3307
|
-
pendingEscalation: null,
|
|
3308
|
-
permissionBlocked: false,
|
|
3309
|
-
structuredState: {
|
|
3310
|
-
...current.structuredState,
|
|
3311
|
-
model: turnState.model ?? current.structuredState?.model,
|
|
3312
|
-
inFlight: false,
|
|
3313
|
-
activeRequestId: null,
|
|
3314
|
-
lastError: null,
|
|
3315
|
-
},
|
|
3316
|
-
};
|
|
3317
|
-
this.sessions.set(sessionId, finished);
|
|
3318
|
-
this.saveAuthoritativeSession(finished);
|
|
3319
|
-
this.emitStructuredSnapshot(finished);
|
|
3320
|
-
if (!keepRunning)
|
|
3321
|
-
this.emitStructuredSnapshot(finished, "ended");
|
|
3322
|
-
if (interruptPrompt) {
|
|
3323
|
-
const interruptedSkills = this.interruptedSkills.get(sessionId) ?? [];
|
|
3324
|
-
this.interruptedWith.delete(sessionId);
|
|
3325
|
-
this.interruptedSkills.delete(sessionId);
|
|
3326
|
-
// 与 codex/cli runner 对齐:清掉"保留队列"标记,避免 stale flag 影响下一次普通 interrupt。
|
|
3327
|
-
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
3328
|
-
setImmediate(() => {
|
|
3329
|
-
this.sendMessage(sessionId, interruptPrompt, { skills: interruptedSkills }).catch((err) => {
|
|
3330
|
-
console.error("[WAND] sdk interrupt-and-send failed:", err);
|
|
3331
|
-
});
|
|
3332
|
-
});
|
|
3333
|
-
return;
|
|
3334
|
-
}
|
|
3335
|
-
if (killedForAskUserQuestion) {
|
|
3336
|
-
if ((finished.queuedMessages?.length ?? 0) > 0) {
|
|
3337
|
-
setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
|
|
3338
|
-
}
|
|
3339
|
-
return;
|
|
3340
|
-
}
|
|
3341
|
-
// Auto-continue after ExitPlanMode (same as CLI runner)
|
|
3342
|
-
const lastToolUse = [...turnState.blocks].reverse().find((b) => b.type === "tool_use");
|
|
3343
|
-
if (lastToolUse && lastToolUse.name === "ExitPlanMode" && turnState.sessionId) {
|
|
3344
|
-
setImmediate(() => {
|
|
3345
|
-
this.sendMessage(sessionId, "Plan approved. Proceed with the implementation.").catch((err) => {
|
|
3346
|
-
console.error("[WAND] sdk auto-continue after ExitPlanMode failed:", err);
|
|
3347
|
-
});
|
|
3348
|
-
});
|
|
3349
|
-
return;
|
|
3350
|
-
}
|
|
3351
|
-
setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
|
|
3352
|
-
}
|
|
3353
|
-
// ---------------------------------------------------------------------------
|
|
3354
2640
|
// Parsing helpers (unchanged logic, extracted from previous implementation)
|
|
3355
2641
|
// ---------------------------------------------------------------------------
|
|
3356
2642
|
compactContentBlocks(blocks, fallbackResult) {
|
|
@@ -3413,12 +2699,6 @@ export class StructuredSessionManager {
|
|
|
3413
2699
|
return [];
|
|
3414
2700
|
return current.queuedMessages;
|
|
3415
2701
|
}
|
|
3416
|
-
resolveQueuedMessageSkillsAfterInterrupt(sessionId, current, interruptPrompt) {
|
|
3417
|
-
if (interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId))
|
|
3418
|
-
return [];
|
|
3419
|
-
const queue = current.queuedMessages ?? [];
|
|
3420
|
-
return queue.map((_, index) => current.queuedMessageSkills?.[index] ?? []);
|
|
3421
|
-
}
|
|
3422
2702
|
normalizeToolResultContent(content) {
|
|
3423
2703
|
return normalizeStructuredToolResultContent(content);
|
|
3424
2704
|
}
|