@co0ontty/wand 4.80.0 → 4.80.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/ai-team-availability.js +0 -1
  2. package/dist/ai-team-live.d.ts +3 -1
  3. package/dist/ai-team-live.js +31 -2
  4. package/dist/ai-team-runner.d.ts +2 -1
  5. package/dist/ai-team-runner.js +17 -7
  6. package/dist/ai-team-types.d.ts +8 -0
  7. package/dist/ai-team-types.js +4 -0
  8. package/dist/build-info.json +3 -3
  9. package/dist/claude-pty-bridge.js +16 -1
  10. package/dist/cli.js +0 -9
  11. package/dist/config.d.ts +2 -10
  12. package/dist/config.js +1 -73
  13. package/dist/git-quick-commit.d.ts +3 -6
  14. package/dist/git-quick-commit.js +38 -75
  15. package/dist/language-prompt.d.ts +1 -1
  16. package/dist/language-prompt.js +1 -1
  17. package/dist/native-session-title.d.ts +17 -0
  18. package/dist/native-session-title.js +257 -0
  19. package/dist/process-manager.d.ts +3 -0
  20. package/dist/process-manager.js +37 -6
  21. package/dist/server-session-routes.js +4 -29
  22. package/dist/server-settings-routes.js +2 -177
  23. package/dist/server-task-routes.js +1 -1
  24. package/dist/server-workspace-routes.js +1 -1
  25. package/dist/server.js +1 -13
  26. package/dist/session-ai-context.d.ts +3 -7
  27. package/dist/session-ai-context.js +0 -25
  28. package/dist/session-topic.d.ts +19 -0
  29. package/dist/session-topic.js +53 -3
  30. package/dist/session-transport.js +0 -1
  31. package/dist/silicon-employee-draft.d.ts +1 -1
  32. package/dist/storage.js +12 -18
  33. package/dist/structured-claude-adapter.d.ts +0 -6
  34. package/dist/structured-claude-adapter.js +1 -5
  35. package/dist/structured-provider-common.d.ts +8 -4
  36. package/dist/structured-provider-common.js +14 -15
  37. package/dist/structured-session-manager.d.ts +4 -40
  38. package/dist/structured-session-manager.js +53 -773
  39. package/dist/types.d.ts +12 -27
  40. package/dist/web-ui/content/ai-teams.js +9 -9
  41. package/dist/web-ui/content/scripts.js +70 -70
  42. package/dist/web-ui/content/styles.css +1 -1
  43. package/dist/web-ui/embedded-assets.js +3 -3
  44. package/dist/ws-broadcast.js +19 -7
  45. package/package.json +1 -2
  46. package/dist/claude-sdk-runner.d.ts +0 -59
  47. package/dist/claude-sdk-runner.js +0 -188
  48. package/dist/claude-skills.d.ts +0 -6
  49. package/dist/claude-skills.js +0 -53
  50. package/dist/system-ai.d.ts +0 -27
  51. 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 { resolveSdkClaudeBinary } from "./claude-sdk-runner.js";
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 { buildClaudeSdkThinking, buildSessionSystemPromptParts, ClaudeCliRunner, derivePermissionPolicy, } from "./structured-claude-adapter.js";
19
- import { inferStructuredEscalation, structuredPermissionDenied } from "./structured-permission.js";
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, sdkQueryFactory = sdkQuery, runners = {}, execHost) {
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, this.config.structuredRunner);
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 && session.runner !== "claude-sdk")
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 && this.sessions.get(executionId)?.runner !== "claude-sdk")
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 (!this.disposed && this.sessions.has(id) && shouldAcceptGeneratedSessionTitle(title, blockedTitles)) {
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, this.config.structuredRunner);
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-sdk"
1216
- ? "claude-agent-sdk (stream-json)"
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, skills) {
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, this.config.structuredRunner);
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, { skills });
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
- const sdkAlive = Boolean(sdkQueryHandle || (sdkAbort && !sdkAbort.signal.aborted));
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
- const queuedMessageSkills = session.queuedMessageSkills ?? [];
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, skills);
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, skills);
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: [], queuedMessageSkills: [] };
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 SDK/adapter implementations.
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, { skills: nextSkills });
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, { skills: interruptedSkills }).catch((error) => {
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, { skills: interruptedSkills }).catch((error) => {
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, { skills: interruptedSkills }).catch((error) => {
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, { skills: interruptedSkills }).catch((error) => {
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
  }