@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) => {