@co0ontty/wand 1.31.0 → 1.31.2
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.
|
@@ -233,10 +233,13 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
233
233
|
app.post("/api/structured-sessions/:id/messages", express.json(), async (req, res) => {
|
|
234
234
|
const input = String(req.body?.input ?? "");
|
|
235
235
|
const interrupt = !!req.body?.interrupt;
|
|
236
|
+
// preserveQueue: 仅在 interrupt 路径有意义。排队条「立即」会带这个 flag,
|
|
237
|
+
// 让退出 handler 不要把剩余 queuedMessages 清空(默认行为是清空)。
|
|
238
|
+
const preserveQueue = !!req.body?.preserveQueue;
|
|
236
239
|
const idempotencyKey = typeof req.body?.idempotencyKey === "string" ? req.body.idempotencyKey : undefined;
|
|
237
|
-
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "idempotencyKey:", idempotencyKey);
|
|
240
|
+
console.log("[WAND] POST /api/structured-sessions/:id/messages id:", req.params.id, "input:", input.substring(0, 50), "interrupt:", interrupt, "preserveQueue:", preserveQueue, "idempotencyKey:", idempotencyKey);
|
|
238
241
|
try {
|
|
239
|
-
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, idempotencyKey });
|
|
242
|
+
const snapshot = await structured.sendMessage(req.params.id, input, { interrupt, preserveQueue, idempotencyKey });
|
|
240
243
|
res.json(snapshot);
|
|
241
244
|
}
|
|
242
245
|
catch (error) {
|
|
@@ -248,6 +251,46 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
248
251
|
});
|
|
249
252
|
}
|
|
250
253
|
});
|
|
254
|
+
// ── Structured queued-messages management ──
|
|
255
|
+
// 三个端点构成"排队消息条"的后端操作面:reorder(拖拽换序)、单条删除、全部清空。
|
|
256
|
+
// 全部走乐观更新模型,失败时前端会回滚到上一次 WS 推送的 queuedMessages。
|
|
257
|
+
app.patch("/api/structured-sessions/:id/queued", express.json(), (req, res) => {
|
|
258
|
+
const rawOrder = req.body?.order;
|
|
259
|
+
if (!Array.isArray(rawOrder)) {
|
|
260
|
+
res.status(400).json({ error: "缺少 order 数组。" });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const snapshot = structured.reorderQueuedMessages(req.params.id, rawOrder.map((v) => Number(v)));
|
|
265
|
+
res.json(snapshot);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
res.status(400).json({ error: getErrorMessage(error, "无法调整排队顺序。") });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
app.delete("/api/structured-sessions/:id/queued/:index", (req, res) => {
|
|
272
|
+
const index = Number(req.params.index);
|
|
273
|
+
if (!Number.isFinite(index)) {
|
|
274
|
+
res.status(400).json({ error: "下标无效。" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const snapshot = structured.deleteQueuedMessage(req.params.id, index);
|
|
279
|
+
res.json(snapshot);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
res.status(400).json({ error: getErrorMessage(error, "无法删除排队消息。") });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
app.delete("/api/structured-sessions/:id/queued", (req, res) => {
|
|
286
|
+
try {
|
|
287
|
+
const snapshot = structured.clearQueuedMessages(req.params.id);
|
|
288
|
+
res.json(snapshot);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
res.status(400).json({ error: getErrorMessage(error, "无法清空排队消息。") });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
251
294
|
// ── Tool content lazy-load endpoint ──
|
|
252
295
|
app.get("/api/sessions/:id/tool-content/:toolUseId", (req, res) => {
|
|
253
296
|
const snapshot = getSessionById(processes, structured, req.params.id);
|
|
@@ -41,6 +41,14 @@ export declare class StructuredSessionManager {
|
|
|
41
41
|
*/
|
|
42
42
|
private readonly pendingSdkQueries;
|
|
43
43
|
private readonly interruptedWith;
|
|
44
|
+
/**
|
|
45
|
+
* Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
|
|
46
|
+
* 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
|
|
47
|
+
* interrupt 语义是"算了,做这个",把队列也作废。但 queue-promote 的语义是
|
|
48
|
+
* "先做这条,剩下的队列还要继续",所以这里打个标记,让退出 handler 保留 queue。
|
|
49
|
+
* 收到后必须 delete 掉,避免下一次普通 interrupt 误带 flag。
|
|
50
|
+
*/
|
|
51
|
+
private readonly preserveQueueOnInterrupt;
|
|
44
52
|
/** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
|
|
45
53
|
private readonly lastStreamSaveAt;
|
|
46
54
|
/**
|
|
@@ -71,11 +79,25 @@ export declare class StructuredSessionManager {
|
|
|
71
79
|
sendMessage(id: string, input: string, opts?: {
|
|
72
80
|
interrupt?: boolean;
|
|
73
81
|
idempotencyKey?: string;
|
|
82
|
+
preserveQueue?: boolean;
|
|
74
83
|
}): Promise<SessionSnapshot>;
|
|
75
84
|
/** Approve a pending permission request. */
|
|
76
85
|
approvePermission(sessionId: string): SessionSnapshot;
|
|
77
86
|
/** Deny a pending permission request. */
|
|
78
87
|
denyPermission(sessionId: string): SessionSnapshot;
|
|
88
|
+
/**
|
|
89
|
+
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
90
|
+
* indices, e.g. `[2, 0, 1]` means "move the third queued message to the front,
|
|
91
|
+
* push the original first to position #2". Throws if the permutation is
|
|
92
|
+
* malformed (length mismatch / duplicate / out-of-range). 不允许在 inFlight
|
|
93
|
+
* 期间改"已经被 flushNextQueuedMessage 拿走的队首",但本方法只动 queue 数组
|
|
94
|
+
* 本身,flushNext 在另一段时序里读 sessions.get(...) 当前快照,已经天然安全。
|
|
95
|
+
*/
|
|
96
|
+
reorderQueuedMessages(sessionId: string, order: number[]): SessionSnapshot;
|
|
97
|
+
/** Remove a single queued message by index. */
|
|
98
|
+
deleteQueuedMessage(sessionId: string, index: number): SessionSnapshot;
|
|
99
|
+
/** Clear all queued messages. No-op when queue is already empty. */
|
|
100
|
+
clearQueuedMessages(sessionId: string): SessionSnapshot;
|
|
79
101
|
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
80
102
|
setSessionModel(sessionId: string, model: string | null): SessionSnapshot;
|
|
81
103
|
/**
|
|
@@ -390,8 +390,8 @@ function buildAppendSystemPromptParts(language, mode) {
|
|
|
390
390
|
}
|
|
391
391
|
if (trimmedLanguage) {
|
|
392
392
|
parts.push(isChinese
|
|
393
|
-
? "
|
|
394
|
-
: `Please respond in ${trimmedLanguage}. Use ${trimmedLanguage} for all your explanations, comments, and conversational text.`);
|
|
393
|
+
? "请使用中文回复。所有解释、注释和对话文本都使用中文。当你通过 Task 工具派发子代理(subagent)时,必须在传给子代理的 prompt 里明确要求它也使用中文回复,确保所有子代理的输出同样遵循该语言偏好。"
|
|
394
|
+
: `Please respond in ${trimmedLanguage}. Use ${trimmedLanguage} for all your explanations, comments, and conversational text. When you dispatch a subagent via the Task tool, you MUST explicitly instruct the subagent in its prompt to also respond in ${trimmedLanguage}, so every subagent's output follows the same language preference.`);
|
|
395
395
|
}
|
|
396
396
|
return parts;
|
|
397
397
|
}
|
|
@@ -434,6 +434,14 @@ export class StructuredSessionManager {
|
|
|
434
434
|
*/
|
|
435
435
|
pendingSdkQueries = new Map();
|
|
436
436
|
interruptedWith = new Map();
|
|
437
|
+
/**
|
|
438
|
+
* Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
|
|
439
|
+
* 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
|
|
440
|
+
* interrupt 语义是"算了,做这个",把队列也作废。但 queue-promote 的语义是
|
|
441
|
+
* "先做这条,剩下的队列还要继续",所以这里打个标记,让退出 handler 保留 queue。
|
|
442
|
+
* 收到后必须 delete 掉,避免下一次普通 interrupt 误带 flag。
|
|
443
|
+
*/
|
|
444
|
+
preserveQueueOnInterrupt = new Set();
|
|
437
445
|
/** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
|
|
438
446
|
lastStreamSaveAt = new Map();
|
|
439
447
|
/**
|
|
@@ -637,6 +645,12 @@ export class StructuredSessionManager {
|
|
|
637
645
|
}
|
|
638
646
|
else if (opts?.interrupt) {
|
|
639
647
|
this.interruptedWith.set(id, prompt);
|
|
648
|
+
if (opts.preserveQueue) {
|
|
649
|
+
this.preserveQueueOnInterrupt.add(id);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
this.preserveQueueOnInterrupt.delete(id);
|
|
653
|
+
}
|
|
640
654
|
try {
|
|
641
655
|
child.kill("SIGTERM");
|
|
642
656
|
}
|
|
@@ -769,6 +783,60 @@ export class StructuredSessionManager {
|
|
|
769
783
|
denyPermission(sessionId) {
|
|
770
784
|
return this.resolvePermission(sessionId, false);
|
|
771
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Reorder the pending queued messages. `order` is a permutation of the current
|
|
788
|
+
* indices, e.g. `[2, 0, 1]` means "move the third queued message to the front,
|
|
789
|
+
* push the original first to position #2". Throws if the permutation is
|
|
790
|
+
* malformed (length mismatch / duplicate / out-of-range). 不允许在 inFlight
|
|
791
|
+
* 期间改"已经被 flushNextQueuedMessage 拿走的队首",但本方法只动 queue 数组
|
|
792
|
+
* 本身,flushNext 在另一段时序里读 sessions.get(...) 当前快照,已经天然安全。
|
|
793
|
+
*/
|
|
794
|
+
reorderQueuedMessages(sessionId, order) {
|
|
795
|
+
const session = this.requireSession(sessionId);
|
|
796
|
+
const queue = session.queuedMessages ?? [];
|
|
797
|
+
if (!Array.isArray(order) || order.length !== queue.length) {
|
|
798
|
+
throw new Error("排序长度与当前队列不一致,请刷新后重试。");
|
|
799
|
+
}
|
|
800
|
+
const seen = new Set();
|
|
801
|
+
for (const idx of order) {
|
|
802
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= queue.length || seen.has(idx)) {
|
|
803
|
+
throw new Error("排序参数无效。");
|
|
804
|
+
}
|
|
805
|
+
seen.add(idx);
|
|
806
|
+
}
|
|
807
|
+
const reordered = order.map((idx) => queue[idx]);
|
|
808
|
+
const updated = { ...session, queuedMessages: reordered };
|
|
809
|
+
this.sessions.set(sessionId, updated);
|
|
810
|
+
this.storage.saveSession(updated);
|
|
811
|
+
this.emitStructuredSnapshot(updated);
|
|
812
|
+
return updated;
|
|
813
|
+
}
|
|
814
|
+
/** Remove a single queued message by index. */
|
|
815
|
+
deleteQueuedMessage(sessionId, index) {
|
|
816
|
+
const session = this.requireSession(sessionId);
|
|
817
|
+
const queue = session.queuedMessages ?? [];
|
|
818
|
+
if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
|
|
819
|
+
throw new Error("队列中没有该条消息(可能已被处理)。");
|
|
820
|
+
}
|
|
821
|
+
const next = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
822
|
+
const updated = { ...session, queuedMessages: next };
|
|
823
|
+
this.sessions.set(sessionId, updated);
|
|
824
|
+
this.storage.saveSession(updated);
|
|
825
|
+
this.emitStructuredSnapshot(updated);
|
|
826
|
+
return updated;
|
|
827
|
+
}
|
|
828
|
+
/** Clear all queued messages. No-op when queue is already empty. */
|
|
829
|
+
clearQueuedMessages(sessionId) {
|
|
830
|
+
const session = this.requireSession(sessionId);
|
|
831
|
+
if (!session.queuedMessages || session.queuedMessages.length === 0) {
|
|
832
|
+
return session;
|
|
833
|
+
}
|
|
834
|
+
const updated = { ...session, queuedMessages: [] };
|
|
835
|
+
this.sessions.set(sessionId, updated);
|
|
836
|
+
this.storage.saveSession(updated);
|
|
837
|
+
this.emitStructuredSnapshot(updated);
|
|
838
|
+
return updated;
|
|
839
|
+
}
|
|
772
840
|
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
773
841
|
setSessionModel(sessionId, model) {
|
|
774
842
|
const session = this.requireSession(sessionId);
|
|
@@ -850,6 +918,7 @@ export class StructuredSessionManager {
|
|
|
850
918
|
stop(id) {
|
|
851
919
|
const session = this.requireSession(id);
|
|
852
920
|
this.interruptedWith.delete(id);
|
|
921
|
+
this.preserveQueueOnInterrupt.delete(id);
|
|
853
922
|
const child = this.pendingChildren.get(id);
|
|
854
923
|
if (child) {
|
|
855
924
|
child.kill();
|
|
@@ -902,6 +971,8 @@ export class StructuredSessionManager {
|
|
|
902
971
|
}
|
|
903
972
|
this.sessions.delete(id);
|
|
904
973
|
this.lastStreamSaveAt.delete(id);
|
|
974
|
+
this.interruptedWith.delete(id);
|
|
975
|
+
this.preserveQueueOnInterrupt.delete(id);
|
|
905
976
|
this.storage.deleteSession(id);
|
|
906
977
|
this.logger?.deleteSession(id);
|
|
907
978
|
}
|
|
@@ -1292,7 +1363,7 @@ export class StructuredSessionManager {
|
|
|
1292
1363
|
output: turnState.result,
|
|
1293
1364
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
1294
1365
|
messages: msgs,
|
|
1295
|
-
queuedMessages: interruptPrompt ? [] : current.queuedMessages,
|
|
1366
|
+
queuedMessages: interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId) ? [] : current.queuedMessages,
|
|
1296
1367
|
pendingEscalation: null,
|
|
1297
1368
|
permissionBlocked: false,
|
|
1298
1369
|
structuredState: {
|
|
@@ -1311,6 +1382,11 @@ export class StructuredSessionManager {
|
|
|
1311
1382
|
}
|
|
1312
1383
|
if (interruptPrompt) {
|
|
1313
1384
|
this.interruptedWith.delete(sessionId);
|
|
1385
|
+
// 把"保留队列"标记一并清掉——不属于本次 interrupt 的后续轮次会按
|
|
1386
|
+
// 默认(清空 queue)行为走,避免 stale flag 影响下一次普通 interrupt。
|
|
1387
|
+
// 注意:被保留的 queuedMessages 不需要在这里主动 flush,重发的
|
|
1388
|
+
// interruptPrompt 跑完会自然触发 flushNextQueuedMessage。
|
|
1389
|
+
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
1314
1390
|
resolve();
|
|
1315
1391
|
setImmediate(() => {
|
|
1316
1392
|
this.sendMessage(sessionId, interruptPrompt).catch((err) => {
|
|
@@ -1803,7 +1879,7 @@ export class StructuredSessionManager {
|
|
|
1803
1879
|
output: turnState.result,
|
|
1804
1880
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
1805
1881
|
messages: msgs,
|
|
1806
|
-
queuedMessages: interruptPrompt ? [] : current.queuedMessages,
|
|
1882
|
+
queuedMessages: interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId) ? [] : current.queuedMessages,
|
|
1807
1883
|
pendingEscalation: null,
|
|
1808
1884
|
permissionBlocked: false,
|
|
1809
1885
|
structuredState: {
|
|
@@ -1828,6 +1904,11 @@ export class StructuredSessionManager {
|
|
|
1828
1904
|
// 用户中断当前回复:保存部分回复后立即发送新消息。
|
|
1829
1905
|
if (interruptPrompt) {
|
|
1830
1906
|
this.interruptedWith.delete(sessionId);
|
|
1907
|
+
// 把"保留队列"标记一并清掉——不属于本次 interrupt 的后续轮次会按
|
|
1908
|
+
// 默认(清空 queue)行为走,避免 stale flag 影响下一次普通 interrupt。
|
|
1909
|
+
// 注意:被保留的 queuedMessages 不需要在这里主动 flush,重发的
|
|
1910
|
+
// interruptPrompt 跑完会自然触发 flushNextQueuedMessage。
|
|
1911
|
+
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
1831
1912
|
resolve();
|
|
1832
1913
|
setImmediate(() => {
|
|
1833
1914
|
this.sendMessage(sessionId, interruptPrompt).catch((err) => {
|