@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.
- package/dist/build-info.json +3 -3
- package/dist/iteration-log.d.ts +0 -4
- package/dist/iteration-log.js +1 -5
- package/dist/message-truncator.d.ts +6 -12
- package/dist/message-truncator.js +116 -31
- package/dist/server-resume-routes.d.ts +2 -2
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.js +67 -49
- package/dist/server.js +12 -1
- package/dist/structured-session-manager.js +13 -8
- package/dist/system-employee.d.ts +0 -2
- package/dist/system-employee.js +0 -6
- package/dist/task-types.d.ts +0 -2
- package/dist/task-types.js +0 -2
- package/dist/tool-use-timestamps.d.ts +3 -0
- package/dist/tool-use-timestamps.js +27 -0
- package/dist/types.d.ts +12 -0
- package/dist/web-ui/content/ai-teams.js +1 -1
- package/dist/web-ui/content/scripts.js +70 -68
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/ws-broadcast.d.ts +1 -0
- package/dist/ws-broadcast.js +39 -4
- package/package.json +1 -1
|
@@ -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
|
|
351
|
-
|
|
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
|
-
|
|
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
|
|
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 === "
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
*/
|
package/dist/system-employee.js
CHANGED
|
@@ -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
|
*/
|
package/dist/task-types.d.ts
CHANGED
|
@@ -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;
|
package/dist/task-types.js
CHANGED
|
@@ -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
|
}
|