@co0ontty/wand 4.80.1 → 4.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { PtyInputDeliveryError, SessionInputError } from "./process-manager.js";
3
3
  import { getDefaultModelForProvider, isExecutionMode } from "./config.js";
4
- import { alignedBlockStart, blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, visibleBlockCount, windowMessagesForTransport } from "./message-truncator.js";
4
+ import { alignedBlockStart, blockWindowMessagesForTransport, compactToolMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, visibleBlockCount, windowMessagesForTransport } from "./message-truncator.js";
5
5
  import { toSessionDetailDTO, toSessionListItemDTO } from "./session-transport.js";
6
6
  import { checkSessionWorktreeMergeabilityAsync, cleanupSessionWorktreeAsync, getWorktreeMergeErrorCode, mergeSessionWorktreeAsync, WorktreeMergeError, } from "./git-worktree.js";
7
7
  import { resolveSessionCwd } from "./session-cwd.js";
@@ -347,8 +347,10 @@ function isMergeActionAllowed(snapshot) {
347
347
  return snapshot.status !== "running";
348
348
  }
349
349
  export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated) {
350
- const sessionResponseDTO = (snapshot) => {
351
- const windowed = windowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id), config.cardDefaults ?? {});
350
+ const wantsCompactTools = (req) => req.get("X-Wand-Tool-Projection") === "compact" || req.query.compactTools === "1";
351
+ const sessionResponseDTO = (snapshot, req) => {
352
+ const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
353
+ const windowed = windowMessagesForTransport(wantsCompactTools(req) ? compactToolMessagesForTransport(enriched) : enriched, config.cardDefaults ?? {});
352
354
  return toSessionDetailDTO(snapshot, {
353
355
  messages: windowed.messages,
354
356
  messageOffset: windowed.messageOffset,
@@ -507,14 +509,14 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
507
509
  const accepted = structured.get(snapshot.id);
508
510
  if (!accepted)
509
511
  throw new Error("未找到该结构化会话。");
510
- res.status(201).json(sessionResponseDTO(accepted));
512
+ res.status(201).json(sessionResponseDTO(accepted, req));
511
513
  return;
512
514
  }
513
515
  const finished = await structured.sendMessage(snapshot.id, prompt);
514
- res.status(201).json(sessionResponseDTO(finished));
516
+ res.status(201).json(sessionResponseDTO(finished, req));
515
517
  return;
516
518
  }
517
- res.status(201).json(sessionResponseDTO(snapshot));
519
+ res.status(201).json(sessionResponseDTO(snapshot, req));
518
520
  }
519
521
  catch (error) {
520
522
  sendRouteError(res, error, "无法启动结构化会话。");
@@ -530,7 +532,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
530
532
  res.status(404).json({ error: "未找到该会话。" });
531
533
  return;
532
534
  }
533
- res.json(updated);
535
+ res.json(wantsCompactTools(req) ? sessionResponseDTO(updated, req) : updated);
534
536
  }
535
537
  catch (error) {
536
538
  sendRouteError(res, error, "切换模型失败。");
@@ -548,7 +550,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
548
550
  res.status(404).json({ error: "未找到该会话。" });
549
551
  return;
550
552
  }
551
- res.json(updated);
553
+ res.json(wantsCompactTools(req) ? sessionResponseDTO(updated, req) : updated);
552
554
  }
553
555
  catch (error) {
554
556
  sendRouteError(res, error, "切换思考深度失败。");
@@ -577,7 +579,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
577
579
  return;
578
580
  }
579
581
  const effective = (snapshot.provider ?? "claude") === "codex" ? "full-access" : mode;
580
- res.json(sessions.setSessionMode(id, effective));
582
+ const updated = sessions.setSessionMode(id, effective);
583
+ res.json(updated && wantsCompactTools(req) ? sessionResponseDTO(updated, req) : updated);
581
584
  }
582
585
  catch (error) {
583
586
  sendRouteError(res, error, "切换模式失败。");
@@ -589,7 +592,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
589
592
  res.status(404).json({ error: "未找到该结构化会话。" });
590
593
  return;
591
594
  }
592
- const all = snapshot.messages ?? [];
595
+ const raw = snapshot.messages ?? [];
596
+ const all = wantsCompactTools(req)
597
+ ? compactToolMessagesForTransport(enrichStructuredMessages(raw, snapshot.id))
598
+ : raw;
593
599
  const offset = parseBoundedInteger(req.query.offset, Math.max(0, all.length - 40), 0, all.length);
594
600
  const limit = parseBoundedInteger(req.query.limit, 40, 1, 200);
595
601
  res.json({
@@ -617,7 +623,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
617
623
  preserveQueue,
618
624
  idempotencyKey,
619
625
  });
620
- res.json(sessionResponseDTO(snapshot));
626
+ res.json(sessionResponseDTO(snapshot, req));
621
627
  }
622
628
  catch (error) {
623
629
  const errorCode = error?.code;
@@ -639,7 +645,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
639
645
  }
640
646
  try {
641
647
  const snapshot = structured.reorderQueuedMessages(req.params.id, rawOrder.map((v) => Number(v)));
642
- res.json(sessionResponseDTO(snapshot));
648
+ res.json(sessionResponseDTO(snapshot, req));
643
649
  }
644
650
  catch (error) {
645
651
  sendRouteError(res, error, "无法调整排队顺序。");
@@ -652,7 +658,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
652
658
  return;
653
659
  }
654
660
  try {
655
- res.json(sessionResponseDTO(structured.editQueuedMessage(req.params.id, Number(req.params.index), expectedText, text)));
661
+ res.json(sessionResponseDTO(structured.editQueuedMessage(req.params.id, Number(req.params.index), expectedText, text), req));
656
662
  }
657
663
  catch (error) {
658
664
  sendRouteError(res, error, "无法编辑排队消息。");
@@ -667,7 +673,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
667
673
  const expectedText = typeof req.body?.expectedText === "string" ? req.body.expectedText : undefined;
668
674
  try {
669
675
  const snapshot = structured.deleteQueuedMessage(req.params.id, index, expectedText);
670
- res.json(sessionResponseDTO(snapshot));
676
+ res.json(sessionResponseDTO(snapshot, req));
671
677
  }
672
678
  catch (error) {
673
679
  sendRouteError(res, error, "无法删除排队消息。");
@@ -683,7 +689,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
683
689
  const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
684
690
  try {
685
691
  const snapshot = await structured.promoteQueuedMessage(req.params.id, index, expectedText, idempotencyKey);
686
- res.json(sessionResponseDTO(snapshot));
692
+ res.json(sessionResponseDTO(snapshot, req));
687
693
  }
688
694
  catch (error) {
689
695
  const errorCode = error?.code;
@@ -697,7 +703,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
697
703
  app.delete("/api/structured-sessions/:id/queued", (req, res) => {
698
704
  try {
699
705
  const snapshot = structured.clearQueuedMessages(req.params.id);
700
- res.json(sessionResponseDTO(snapshot));
706
+ res.json(sessionResponseDTO(snapshot, req));
701
707
  }
702
708
  catch (error) {
703
709
  sendRouteError(res, error, "无法清空排队消息。");
@@ -712,19 +718,28 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
712
718
  }
713
719
  const toolUseId = req.params.toolUseId;
714
720
  const messages = snapshot.messages ?? [];
721
+ let toolUse;
722
+ let toolResult;
715
723
  for (const turn of messages) {
716
724
  for (const block of turn.content) {
717
- if (block.type === "tool_result" && block.tool_use_id === toolUseId) {
718
- res.json({
719
- tool_use_id: block.tool_use_id,
720
- content: block.content,
721
- is_error: block.is_error || false,
722
- });
723
- return;
724
- }
725
+ if (block.type === "tool_use" && block.id === toolUseId)
726
+ toolUse = block;
727
+ if (block.type === "tool_result" && block.tool_use_id === toolUseId)
728
+ toolResult = block;
725
729
  }
726
730
  }
727
- res.status(404).json({ error: "未找到该工具结果。" });
731
+ if (!toolUse && !toolResult) {
732
+ res.status(404).json({ error: "未找到该工具调用。" });
733
+ return;
734
+ }
735
+ res.json({
736
+ tool_use_id: toolUseId,
737
+ input: toolUse?.input ?? {},
738
+ content: toolResult?.content ?? "",
739
+ is_error: toolResult?.is_error ?? false,
740
+ pending: !toolResult,
741
+ resultAvailable: !!toolResult,
742
+ });
728
743
  });
729
744
  // ── Inline tool image endpoint ──
730
745
  // tool_result 里内联的 base64 图片在传输层被改写为这个 URL(避免把整张图的 base64
@@ -784,7 +799,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
784
799
  conflict: result.hasConflicts,
785
800
  lastError: result.ok ? undefined : result.reason,
786
801
  });
787
- res.json({ session: updated, result });
802
+ res.json({ session: wantsCompactTools(req) ? sessionResponseDTO(updated, req) : updated, result });
788
803
  }
789
804
  catch (error) {
790
805
  res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法检查 worktree 合并状态。"));
@@ -814,7 +829,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
814
829
  lastError: undefined,
815
830
  conflict: false,
816
831
  });
817
- res.json({ session: updated, result });
832
+ res.json({ session: wantsCompactTools(req) ? sessionResponseDTO(updated, req) : updated, result });
818
833
  }
819
834
  catch (error) {
820
835
  const current = sessions.getLatest(req.params.id);
@@ -1058,7 +1073,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1058
1073
  lastError: undefined,
1059
1074
  conflict: false,
1060
1075
  });
1061
- res.json({ session: updated, ok: true });
1076
+ res.json({ session: wantsCompactTools(req) ? sessionResponseDTO(updated, req) : updated, ok: true });
1062
1077
  }
1063
1078
  catch (error) {
1064
1079
  res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法清理 worktree。"));
@@ -1117,13 +1132,15 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1117
1132
  const transcriptOutput = (snapshot.sessionKind ?? "pty") === "pty"
1118
1133
  ? processes.getPtyTranscript(snapshot.id) ?? snapshot.output
1119
1134
  : snapshot.output;
1135
+ const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
1136
+ const messages = wantsCompactTools(req) ? compactToolMessagesForTransport(enriched) : enriched;
1120
1137
  if (req.query.format === "chat") {
1121
1138
  // 客户端带 blockBudget(Web/iOS)走块级窗口:只回最近 N 个块(必要时切掉最旧 turn 的头部),
1122
1139
  // 根治「单条 turn 上百块/1MB」的长任务打开慢;未启用的客户端保持 turn 级窗口。
1123
1140
  const rawBudget = req.query.blockBudget;
1124
1141
  if (typeof rawBudget === "string" && /^\d+$/.test(rawBudget) && Number(rawBudget) > 0) {
1125
1142
  const blockBudget = parseBoundedInteger(rawBudget, 1, 1, 2_000);
1126
- const windowed = blockWindowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id), config.cardDefaults ?? {}, blockBudget);
1143
+ const windowed = blockWindowMessagesForTransport(messages, config.cardDefaults ?? {}, blockBudget);
1127
1144
  res.json(toSessionDetailDTO(snapshot, {
1128
1145
  output: transcriptOutput,
1129
1146
  messages: windowed.messages,
@@ -1136,7 +1153,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1136
1153
  return;
1137
1154
  }
1138
1155
  // 与 WS init 对齐:只回最近一窗 turn + offset/total,更早的走 /messages 翻页。
1139
- const windowed = windowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id), config.cardDefaults ?? {});
1156
+ const windowed = windowMessagesForTransport(messages, config.cardDefaults ?? {});
1140
1157
  res.json(toSessionDetailDTO(snapshot, {
1141
1158
  output: transcriptOutput,
1142
1159
  messages: windowed.messages,
@@ -1156,7 +1173,8 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1156
1173
  res.status(404).json({ error: "未找到该会话,可能已被删除。" });
1157
1174
  return;
1158
1175
  }
1159
- const all = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
1176
+ const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
1177
+ const all = wantsCompactTools(req) ? compactToolMessagesForTransport(enriched) : enriched;
1160
1178
  const total = all.length;
1161
1179
  // Web history: fetch at most one block window *before* the currently
1162
1180
  // displayed turn. A 40-turn page can contain a single multi-MB assistant
@@ -1226,7 +1244,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1226
1244
  return;
1227
1245
  }
1228
1246
  const newSnapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, body);
1229
- res.status(201).json(sessionResponseDTO(newSnapshot));
1247
+ res.status(201).json(sessionResponseDTO(newSnapshot, req));
1230
1248
  }
1231
1249
  catch (error) {
1232
1250
  sendRouteError(res, error, "无法恢复会话。");
@@ -1278,7 +1296,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1278
1296
  workspaceTaskId: existingSession.workspaceTaskId,
1279
1297
  ...(requestedOrigin ?? {}),
1280
1298
  });
1281
- res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
1299
+ res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot, req) });
1282
1300
  }
1283
1301
  else {
1284
1302
  const cwd = body.cwd?.trim();
@@ -1296,7 +1314,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1296
1314
  workspaceId: resolveWorkspaceIdForNewSession(storage, cwd),
1297
1315
  ...(requestedOrigin ?? {}),
1298
1316
  });
1299
- res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot) });
1317
+ res.status(201).json({ resumedClaudeSessionId: claudeSessionId, ...sessionResponseDTO(newSnapshot, req) });
1300
1318
  }
1301
1319
  }
1302
1320
  catch (error) {
@@ -1328,11 +1346,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1328
1346
  if (!accepted) {
1329
1347
  throw new Error("未找到该结构化会话。");
1330
1348
  }
1331
- res.status(202).json(sessionResponseDTO(accepted));
1349
+ res.status(202).json(sessionResponseDTO(accepted, req));
1332
1350
  return;
1333
1351
  }
1334
1352
  const snapshot = await completion;
1335
- res.json(sessionResponseDTO(snapshot));
1353
+ res.json(sessionResponseDTO(snapshot, req));
1336
1354
  return;
1337
1355
  }
1338
1356
  const existingSession = processes.get(sessionId) || storage.getSession(sessionId);
@@ -1346,7 +1364,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1346
1364
  res.status(202).json({ accepted: true, deliveryConfirmed: false });
1347
1365
  return;
1348
1366
  }
1349
- res.json(sessionResponseDTO(snapshot));
1367
+ res.json(sessionResponseDTO(snapshot, req));
1350
1368
  return;
1351
1369
  }
1352
1370
  const snapshot = await processes.sendInputConfirmed(sessionId, input, view, shortcutKey);
@@ -1354,7 +1372,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1354
1372
  res.status(202).json({ accepted: true });
1355
1373
  return;
1356
1374
  }
1357
- res.json(sessionResponseDTO(snapshot));
1375
+ res.json(sessionResponseDTO(snapshot, req));
1358
1376
  }
1359
1377
  catch (error) {
1360
1378
  const response = getInputErrorResponse(error, sessionId);
@@ -1382,7 +1400,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1382
1400
  return;
1383
1401
  }
1384
1402
  const snapshot = processes.resize(req.params.id, body.cols ?? 0, body.rows ?? 0);
1385
- res.json(sessionResponseDTO(snapshot));
1403
+ res.json(sessionResponseDTO(snapshot, req));
1386
1404
  }
1387
1405
  catch (error) {
1388
1406
  sendRouteError(res, error, "无法调整终端大小。");
@@ -1391,7 +1409,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1391
1409
  app.post("/api/sessions/:id/approve-permission", (req, res) => {
1392
1410
  try {
1393
1411
  if (sessions.ownerOf(req.params.id) === "structured") {
1394
- res.json(sessionResponseDTO(structured.approvePermission(req.params.id)));
1412
+ res.json(sessionResponseDTO(structured.approvePermission(req.params.id), req));
1395
1413
  return;
1396
1414
  }
1397
1415
  const snapshot = sessions.get(req.params.id);
@@ -1399,7 +1417,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1399
1417
  res.status(400).json({ error: "Codex provider 不支持权限批准操作。" });
1400
1418
  return;
1401
1419
  }
1402
- res.json(sessionResponseDTO(processes.approvePermission(req.params.id)));
1420
+ res.json(sessionResponseDTO(processes.approvePermission(req.params.id), req));
1403
1421
  }
1404
1422
  catch (error) {
1405
1423
  sendRouteError(res, error, "无法批准该授权请求。");
@@ -1408,7 +1426,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1408
1426
  app.post("/api/sessions/:id/deny-permission", (req, res) => {
1409
1427
  try {
1410
1428
  if (sessions.ownerOf(req.params.id) === "structured") {
1411
- res.json(sessionResponseDTO(structured.denyPermission(req.params.id)));
1429
+ res.json(sessionResponseDTO(structured.denyPermission(req.params.id), req));
1412
1430
  return;
1413
1431
  }
1414
1432
  const snapshot = sessions.get(req.params.id);
@@ -1416,7 +1434,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1416
1434
  res.status(400).json({ error: "Codex provider 不支持权限拒绝操作。" });
1417
1435
  return;
1418
1436
  }
1419
- res.json(sessionResponseDTO(processes.denyPermission(req.params.id)));
1437
+ res.json(sessionResponseDTO(processes.denyPermission(req.params.id), req));
1420
1438
  }
1421
1439
  catch (error) {
1422
1440
  sendRouteError(res, error, "无法拒绝该授权请求。");
@@ -1425,7 +1443,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1425
1443
  app.post("/api/sessions/:id/toggle-auto-approve", (req, res) => {
1426
1444
  try {
1427
1445
  if (sessions.ownerOf(req.params.id) === "structured") {
1428
- res.json(sessionResponseDTO(structured.toggleAutoApprove(req.params.id)));
1446
+ res.json(sessionResponseDTO(structured.toggleAutoApprove(req.params.id), req));
1429
1447
  return;
1430
1448
  }
1431
1449
  const snapshot = sessions.get(req.params.id);
@@ -1433,7 +1451,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1433
1451
  res.status(400).json({ error: "Codex provider 不支持自动批准切换。" });
1434
1452
  return;
1435
1453
  }
1436
- res.json(sessionResponseDTO(processes.toggleAutoApprove(req.params.id)));
1454
+ res.json(sessionResponseDTO(processes.toggleAutoApprove(req.params.id), req));
1437
1455
  }
1438
1456
  catch (error) {
1439
1457
  sendRouteError(res, error, "无法切换自动批准状态。");
@@ -1449,10 +1467,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1449
1467
  return;
1450
1468
  }
1451
1469
  if (sessions.ownerOf(req.params.id) === "structured") {
1452
- res.json(sessionResponseDTO(structured.resolveEscalation(req.params.id, requestId, resolution)));
1470
+ res.json(sessionResponseDTO(structured.resolveEscalation(req.params.id, requestId, resolution), req));
1453
1471
  return;
1454
1472
  }
1455
- res.json(sessionResponseDTO(processes.resolveEscalation(req.params.id, requestId, resolution)));
1473
+ res.json(sessionResponseDTO(processes.resolveEscalation(req.params.id, requestId, resolution), req));
1456
1474
  }
1457
1475
  catch (error) {
1458
1476
  sendRouteError(res, error, "无法处理该授权请求。");
@@ -1461,10 +1479,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1461
1479
  app.post("/api/sessions/:id/stop", (req, res) => {
1462
1480
  try {
1463
1481
  if (sessions.ownerOf(req.params.id) === "structured") {
1464
- res.json(sessionResponseDTO(structured.stop(req.params.id)));
1482
+ res.json(sessionResponseDTO(structured.stop(req.params.id), req));
1465
1483
  return;
1466
1484
  }
1467
- res.json(sessionResponseDTO(processes.stop(req.params.id)));
1485
+ res.json(sessionResponseDTO(processes.stop(req.params.id), req));
1468
1486
  }
1469
1487
  catch (error) {
1470
1488
  sendRouteError(res, error, "无法停止会话。");
package/dist/server.js CHANGED
@@ -48,6 +48,8 @@ import { RuntimeConfigState } from "./runtime-config.js";
48
48
  import { safeServiceInstalled } from "./tui/runtime-utils.js";
49
49
  import { checkManagedServiceUpdatePreflight, } from "./update-helper.js";
50
50
  import { toSessionDetailDTO } from "./session-transport.js";
51
+ import { compactToolMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
52
+ import { enrichStructuredMessages } from "./structured-client-protocol.js";
51
53
  import { registerUploadRoutes } from "./upload-routes.js";
52
54
  import { optimizePrompt, PromptOptimizeError } from "./prompt-optimizer.js";
53
55
  import { resolveDatabasePath, WandStorage } from "./storage.js";
@@ -837,7 +839,16 @@ export async function startServer(config, configPath, options = {}) {
837
839
  ...origin,
838
840
  }));
839
841
  recordRecentPath(storage, snapshot.cwd);
840
- res.status(201).json(toSessionDetailDTO(snapshot));
842
+ const compactTools = req.get("X-Wand-Tool-Projection") === "compact"
843
+ || req.query.compactTools === "1";
844
+ if (compactTools) {
845
+ const messages = compactToolMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id));
846
+ const windowed = windowMessagesForTransport(messages, config.cardDefaults ?? {});
847
+ res.status(201).json(toSessionDetailDTO(snapshot, windowed));
848
+ }
849
+ else {
850
+ res.status(201).json(toSessionDetailDTO(snapshot));
851
+ }
841
852
  }
842
853
  catch (error) {
843
854
  sendRouteError(res, error, "无法启动命令。请检查命令是否安装。");
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { prepareSessionWorktree } from "./git-worktree.js";
3
3
  import { truncateMessagesForTransport } from "./message-truncator.js";
4
+ import { stampNewToolUseTimes } from "./tool-use-timestamps.js";
4
5
  import { buildChildEnv } from "./env-utils.js";
5
6
  import { getErrorMessage } from "./error-utils.js";
6
7
  import { getDefaultModelForProvider } from "./config.js";
@@ -372,13 +373,15 @@ class TruncatedReplayView {
372
373
  function appendNoticeTurn(messages, turn) {
373
374
  return [...(messages ?? []), { ...turn, createdAt: isoNow(), completedAt: isoNow() }];
374
375
  }
375
- function upsertAssistantMessage(messages, turn, complete = false) {
376
+ function upsertAssistantMessage(messages, turn, complete = false, stampNewTools = true) {
376
377
  const msgs = [...(messages ?? [])];
377
378
  const last = msgs[msgs.length - 1];
378
- const createdAt = (last?.role === "assistant" ? last.createdAt : undefined) ?? turn.createdAt ?? isoNow();
379
+ const observedAt = isoNow();
380
+ const createdAt = (last?.role === "assistant" ? last.createdAt : undefined) ?? turn.createdAt ?? observedAt;
379
381
  const next = {
380
382
  ...turn,
381
383
  createdAt,
384
+ content: stampNewToolUseTimes(turn.content, last?.role === "assistant" ? last.content : undefined, observedAt, stampNewTools),
382
385
  };
383
386
  if (complete)
384
387
  next.completedAt = isoNow();
@@ -725,6 +728,7 @@ export class StructuredSessionManager {
725
728
  // 不能用它覆盖 messages/output,也不能就此冻结前端。
726
729
  const replayTruncated = initialState.stdoutTruncated;
727
730
  const replayView = new TruncatedReplayView(resumed.messages);
731
+ let replayingHistory = true;
728
732
  const syncTurn = (turnState) => {
729
733
  const current = this.currentSessionForRequest(sessionId, requestId);
730
734
  if (!current)
@@ -750,7 +754,7 @@ export class StructuredSessionManager {
750
754
  };
751
755
  const patched = {
752
756
  ...current,
753
- messages: upsertAssistantMessage(current.messages, merged),
757
+ messages: upsertAssistantMessage(current.messages, merged, false, !replayingHistory),
754
758
  structuredState,
755
759
  };
756
760
  this.sessions.set(sessionId, patched);
@@ -762,7 +766,7 @@ export class StructuredSessionManager {
762
766
  content: this.compactContentBlocks([...turnState.blocks], turnState.result),
763
767
  usage: turnState.usage,
764
768
  };
765
- const messages = upsertAssistantMessage(current.messages, turn);
769
+ const messages = upsertAssistantMessage(current.messages, turn, false, !replayingHistory);
766
770
  const patched = {
767
771
  ...current,
768
772
  claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
@@ -853,6 +857,7 @@ export class StructuredSessionManager {
853
857
  }
854
858
  // 首轮 feed + attach 完成后再定锚点,之后 onStream 的事件都走合并视图。
855
859
  replayView.prime([...processor.state.blocks]);
860
+ replayingHistory = false;
856
861
  runningHandle.onStream((event) => {
857
862
  // Events may be buffered while structuredAttach is in flight. Sequence
858
863
  // watermarks distinguish overlap with the snapshot from genuinely new
@@ -920,7 +925,7 @@ export class StructuredSessionManager {
920
925
  const keepRunning = interruptedForQuestion;
921
926
  const messages = replayTruncated
922
927
  ? this.mergeTruncatedReplayTurn(current, processor, replayView, !keepRunning)
923
- : this.buildCompletedAssistantMessages(current, processor.state);
928
+ : this.buildCompletedAssistantMessages(current, processor.state, false);
924
929
  const finished = {
925
930
  ...current,
926
931
  status: keepRunning ? "running" : "idle",
@@ -2669,13 +2674,13 @@ export class StructuredSessionManager {
2669
2674
  }
2670
2675
  return compacted;
2671
2676
  }
2672
- buildCompletedAssistantMessages(current, turnState) {
2677
+ buildCompletedAssistantMessages(current, turnState, stampNewTools = true) {
2673
2678
  const assistantTurn = {
2674
2679
  role: "assistant",
2675
2680
  content: this.compactContentBlocks([...turnState.blocks], turnState.result),
2676
2681
  usage: turnState.usage,
2677
2682
  };
2678
- return upsertAssistantMessage(current.messages, assistantTurn, true);
2683
+ return upsertAssistantMessage(current.messages, assistantTurn, true, stampNewTools);
2679
2684
  }
2680
2685
  /**
2681
2686
  * Rebuild the trailing assistant turn when the replayed log was truncated.
@@ -2692,7 +2697,7 @@ export class StructuredSessionManager {
2692
2697
  content: this.compactContentBlocks(view.content([...processor.state.blocks]), processor.state.result),
2693
2698
  usage: processor.state.usage,
2694
2699
  };
2695
- return upsertAssistantMessage([...(current.messages ?? [])], turn, complete);
2700
+ return upsertAssistantMessage([...(current.messages ?? [])], turn, complete, false);
2696
2701
  }
2697
2702
  resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt) {
2698
2703
  if (interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId))
@@ -27,8 +27,6 @@ export interface SystemEmployeeSeed {
27
27
  /** 首次创建内置员工时使用的候选:优先沿用用户已有的系统 AI 工具与模型。 */
28
28
  export declare function systemEmployeeSeedAgents(seed?: SystemEmployeeSeed): WandTaskAgent[];
29
29
  export declare function systemEmployeeDefinition(agents: WandTaskAgent[], now: string, existing?: SiliconEmployee | null): SiliconEmployee;
30
- /** 内置员工只保留结构化候选(会话与内部 AI 调用都要求结构化执行)。 */
31
- export declare function systemEmployeeAgentsOrNull(value: unknown): WandTaskAgent[] | null;
32
30
  /**
33
31
  * 内部 AI 调用的 CLI 降级链:按用户设置的顺序,模型为空 / "default" 时跟随 provider 默认值。
34
32
  */
@@ -56,12 +56,6 @@ export function systemEmployeeDefinition(agents, now, existing) {
56
56
  updatedAt: now,
57
57
  };
58
58
  }
59
- /** 内置员工只保留结构化候选(会话与内部 AI 调用都要求结构化执行)。 */
60
- export function systemEmployeeAgentsOrNull(value) {
61
- if (!Array.isArray(value) || value.length === 0)
62
- return null;
63
- return value;
64
- }
65
59
  /**
66
60
  * 内部 AI 调用的 CLI 降级链:按用户设置的顺序,模型为空 / "default" 时跟随 provider 默认值。
67
61
  */
@@ -35,8 +35,6 @@ export declare function normalizeWandTaskAgentMode(provider: WandTaskAgentProvid
35
35
  * - pty:原始 CLI 终端,与「新建工作窗口」里的 PTY 一致。
36
36
  */
37
37
  export type WandTaskAgentKind = "structured" | "pty";
38
- /** 合法会话形态清单;顺序即下拉顺序。 */
39
- export declare const WAND_TASK_AGENT_KINDS: readonly WandTaskAgentKind[];
40
38
  /** 历史行 / 老客户端没带 kind 时的兜底:结构化对话,与旧派发行为一致。 */
41
39
  export declare const DEFAULT_WAND_TASK_AGENT_KIND: WandTaskAgentKind;
42
40
  export declare function isWandTaskAgentKind(value: unknown): value is WandTaskAgentKind;
@@ -30,8 +30,6 @@ export function normalizeWandTaskAgentMode(provider, mode) {
30
30
  ? DEFAULT_WAND_TASK_AGENT_MODE
31
31
  : supported[0];
32
32
  }
33
- /** 合法会话形态清单;顺序即下拉顺序。 */
34
- export const WAND_TASK_AGENT_KINDS = ["structured", "pty"];
35
33
  /** 历史行 / 老客户端没带 kind 时的兜底:结构化对话,与旧派发行为一致。 */
36
34
  export const DEFAULT_WAND_TASK_AGENT_KIND = "structured";
37
35
  export function isWandTaskAgentKind(value) {
@@ -0,0 +1,3 @@
1
+ import type { ContentBlock } from "./types.js";
2
+ /** Preserve first-observed tool times across streaming replacements of an assistant turn. */
3
+ export declare function stampNewToolUseTimes(content: ContentBlock[], previous: ContentBlock[] | undefined, observedAt: string, stampNew?: boolean): ContentBlock[];
@@ -0,0 +1,27 @@
1
+ /** Preserve first-observed tool times across streaming replacements of an assistant turn. */
2
+ export function stampNewToolUseTimes(content, previous, observedAt, stampNew = true) {
3
+ const previousById = new Map();
4
+ for (const block of previous ?? []) {
5
+ if (block.type === "tool_use")
6
+ previousById.set(block.id, block);
7
+ }
8
+ return content.map((block) => {
9
+ if (block.type !== "tool_use")
10
+ return block;
11
+ const prior = previousById.get(block.id);
12
+ if (prior) {
13
+ // A persisted legacy invocation has no trustworthy per-tool time. Keep it absent.
14
+ if (!prior.occurredAt) {
15
+ if (!block.occurredAt)
16
+ return block;
17
+ const { occurredAt: _ignored, ...withoutTime } = block;
18
+ return withoutTime;
19
+ }
20
+ return block.occurredAt === prior.occurredAt
21
+ ? block : { ...block, occurredAt: prior.occurredAt };
22
+ }
23
+ if (block.occurredAt || !stampNew)
24
+ return block;
25
+ return { ...block, occurredAt: observedAt };
26
+ });
27
+ }
package/dist/types.d.ts CHANGED
@@ -500,6 +500,18 @@ export interface ToolUseBlock {
500
500
  name: string;
501
501
  description?: string;
502
502
  input: Record<string, unknown>;
503
+ /** ISO time when the server first observed this tool invocation; absent for older transcripts. */
504
+ occurredAt?: string;
505
+ /** Small transport-only label; the complete input is fetched when a tool is opened. */
506
+ activity?: {
507
+ kind: "edit_file" | "read_file" | "run_command" | "other";
508
+ label: string;
509
+ /** Opaque, stable identifier for counting repeated accesses to one file. */
510
+ fileKey?: string;
511
+ hasImage?: boolean;
512
+ /** ISO time of the original invocation, never the snapshot/render time. */
513
+ occurredAt?: string;
514
+ };
503
515
  semantic?: ToolUseSemantic;
504
516
  __subagent?: SubagentMeta;
505
517
  }