@co0ontty/wand 1.68.0 → 1.68.1-beta.gc44fb34

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,6 +1,6 @@
1
1
  {
2
- "commit": "3982229b08d479f6d7f838b8e4fce20be7c1472f",
3
- "builtAt": "2026-06-16T10:21:53.525Z",
4
- "version": "1.68.0",
5
- "channel": "stable"
2
+ "commit": "c44fb3475cd81a815f54c683f19c3583b599c4d9",
3
+ "builtAt": "2026-06-16T23:05:43.705Z",
4
+ "version": "1.68.1-beta.gc44fb34",
5
+ "channel": "beta"
6
6
  }
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
+ import { isRunningAsRoot } from "./env-utils.js";
6
7
  const DEFAULT_CONFIG_DIR = ".wand";
7
8
  const DEFAULT_CONFIG_FILE = "config.json";
8
9
  /**
@@ -34,7 +35,12 @@ export const defaultConfig = () => ({
34
35
  port: 8443,
35
36
  https: false,
36
37
  password: "change-me",
37
- defaultMode: "default",
38
+ // 非 root 启动时才有资格用 Claude 的 permission-bypass(root 会被 Claude CLI 拒绝),
39
+ // 所以这种环境下把默认执行模式抬到「托管」——开箱即得自动确认权限的全自主体验。
40
+ // root 启动则保守回落到「default」(托管在 root 下也只能降级成 acceptEdits)。
41
+ // 注意:defaultMode 是偏好字段,只存 SQLite、不写 config.json(见 stripPreferenceFields),
42
+ // 这里仅作为「用户从未在设置里显式选过模式」时的回落值——显式选择始终优先。
43
+ defaultMode: isRunningAsRoot() ? "default" : "managed",
38
44
  shell: process.env.SHELL || "/bin/bash",
39
45
  defaultCwd: process.cwd(),
40
46
  startupCommands: [],
@@ -48,6 +48,12 @@ export declare class StructuredSessionManager {
48
48
  */
49
49
  private readonly pendingSdkQueries;
50
50
  private readonly interruptedWith;
51
+ /**
52
+ * 用户主动点了「停止」的会话。异步收尾(claude -p / codex 的 close、SDK 的 abort)
53
+ * 据此跳过"结构化会话执行失败"路径——主动停止不是失败,按正常 idle 收尾、保留历史内容。
54
+ * 用 Set.delete 消费:读取的同时清除,下一轮真失败不会被旧标记误抑制。
55
+ */
56
+ private readonly userStopped;
51
57
  /**
52
58
  * Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
53
59
  * 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
@@ -394,6 +394,12 @@ export class StructuredSessionManager {
394
394
  */
395
395
  pendingSdkQueries = new Map();
396
396
  interruptedWith = new Map();
397
+ /**
398
+ * 用户主动点了「停止」的会话。异步收尾(claude -p / codex 的 close、SDK 的 abort)
399
+ * 据此跳过"结构化会话执行失败"路径——主动停止不是失败,按正常 idle 收尾、保留历史内容。
400
+ * 用 Set.delete 消费:读取的同时清除,下一轮真失败不会被旧标记误抑制。
401
+ */
402
+ userStopped = new Set();
397
403
  /**
398
404
  * Sessions where the current interrupt is a "queue promote" (用户从排队条点了「立即」
399
405
  * 把队首插队到 now)。退出处理三个分支默认会把 queuedMessages 清空——因为常规的
@@ -967,6 +973,9 @@ export class StructuredSessionManager {
967
973
  const session = this.requireSession(id);
968
974
  this.interruptedWith.delete(id);
969
975
  this.preserveQueueOnInterrupt.delete(id);
976
+ // 标记本会话为「用户主动停止」,让被 kill 的子进程 close / SDK abort 收尾时
977
+ // 走正常 idle 收尾而不是失败路径。
978
+ this.userStopped.add(id);
970
979
  const child = this.pendingChildren.get(id);
971
980
  if (child) {
972
981
  child.kill();
@@ -984,22 +993,27 @@ export class StructuredSessionManager {
984
993
  sdkAbort.abort();
985
994
  this.pendingSdkAbort.delete(id);
986
995
  }
987
- const stopped = {
996
+ // 主动停止只是取消「当前回合」,结构化会话本身并没有结束——置为 idle 而非 stopped。
997
+ // 这样前端不会进入"会话已结束/恢复会话"终止态,输入框保持可用,直接展示历史内容。
998
+ const cancelled = {
988
999
  ...session,
989
- status: "stopped",
990
- endedAt: new Date().toISOString(),
1000
+ status: "idle",
1001
+ exitCode: null,
1002
+ endedAt: null,
991
1003
  pendingEscalation: null,
992
1004
  permissionBlocked: false,
993
1005
  structuredState: {
994
1006
  ...(session.structuredState ?? defaultStructuredState(session.provider ?? "claude", session.runner)),
995
1007
  inFlight: false,
996
1008
  activeRequestId: null,
1009
+ lastError: null,
997
1010
  },
998
1011
  };
999
- this.sessions.set(id, stopped);
1000
- this.storage.saveSession(stopped);
1001
- this.emitStructuredSnapshot(stopped, "ended");
1002
- return stopped;
1012
+ this.sessions.set(id, cancelled);
1013
+ this.storage.saveSession(cancelled);
1014
+ // 仍发 "ended" 事件让各端停掉"回复中"指示 / 灵动岛,但携带的 status 是 idle。
1015
+ this.emitStructuredSnapshot(cancelled, "ended");
1016
+ return cancelled;
1003
1017
  }
1004
1018
  delete(id) {
1005
1019
  const child = this.pendingChildren.get(id);
@@ -1148,6 +1162,7 @@ export class StructuredSessionManager {
1148
1162
  // Streaming codex exec --json execution
1149
1163
  // ---------------------------------------------------------------------------
1150
1164
  runCodexStreaming(sessionId, session, prompt) {
1165
+ this.userStopped.delete(sessionId);
1151
1166
  return new Promise((resolve, reject) => {
1152
1167
  const args = this.buildCodexArgs(session);
1153
1168
  const spawnedAt = new Date().toISOString();
@@ -1351,10 +1366,12 @@ export class StructuredSessionManager {
1351
1366
  // 主动中断时(interruptedWith 里有新消息),不走失败路径
1352
1367
  const interruptedByUser = this.interruptedWith.has(sessionId);
1353
1368
  const interruptPrompt = this.interruptedWith.get(sessionId);
1369
+ // 用户点「停止」kill 掉 codex 后会非零退出,但这不是失败——按正常 idle 收尾。
1370
+ const userStopped = this.userStopped.delete(sessionId);
1354
1371
  // codex 把模型/网络/沙箱等错误写到 stdout 的 NDJSON 流(type: error / turn.failed),
1355
1372
  // 而不是 stderr。我们以 turn.failed 的 message 为准,其次是最后一个 error 事件。
1356
1373
  const codexFailed = codexTurnFailed !== null;
1357
- if ((codexFailed || (code !== 0 && code !== null) || signal) && !interruptedByUser) {
1374
+ if ((codexFailed || (code !== 0 && code !== null) || signal) && !interruptedByUser && !userStopped) {
1358
1375
  const errorText = this.formatStructuredExitError("codex exec", code, signal, {
1359
1376
  stderr,
1360
1377
  primary: codexTurnFailed,
@@ -1448,6 +1465,7 @@ export class StructuredSessionManager {
1448
1465
  * outside CWD). stdin is always "ignore" — no ACP bidirectional control.
1449
1466
  */
1450
1467
  runClaudeStreaming(sessionId, session, prompt) {
1468
+ this.userStopped.delete(sessionId);
1451
1469
  return new Promise((resolve, reject) => {
1452
1470
  const args = ["-p", "--verbose", "--output-format", "stream-json"];
1453
1471
  // 权限策略:决策规则与 SDK runner 共享 derivePermissionPolicy(),CLI 这边把
@@ -1909,8 +1927,10 @@ export class StructuredSessionManager {
1909
1927
  // 可能以非零 exit code 退出(内部 handler 调了 exit(1))。这种情况属于正常
1910
1928
  // 中断流程,不应走失败路径——后续 interruptedWith 逻辑会发送新消息。
1911
1929
  const interruptedByUser = this.interruptedWith.has(sessionId);
1930
+ // 用户点「停止」kill 掉 claude -p 后会非零退出,但这不是失败——按正常 idle 收尾。
1931
+ const userStopped = this.userStopped.delete(sessionId);
1912
1932
  const failedExit = (code !== null && code !== 0) || signal !== null;
1913
- if (failedExit && !interruptedByUser) {
1933
+ if (failedExit && !interruptedByUser && !userStopped) {
1914
1934
  const errorText = this.formatStructuredExitError("claude -p", code, signal, {
1915
1935
  stderr,
1916
1936
  // claude -p 没有 codex 那种独立的 turn.failed 事件,所以 primary 留空;
@@ -2060,6 +2080,7 @@ export class StructuredSessionManager {
2060
2080
  * SDKAssistantMessage with the authoritative complete content.
2061
2081
  */
2062
2082
  async runClaudeSdkStreaming(sessionId, session, prompt) {
2083
+ this.userStopped.delete(sessionId);
2063
2084
  const abortController = new AbortController();
2064
2085
  this.pendingSdkAbort.set(sessionId, abortController);
2065
2086
  const isManaged = session.mode === "managed";
@@ -2450,6 +2471,8 @@ export class StructuredSessionManager {
2450
2471
  this.pendingSdkAbort.delete(sessionId);
2451
2472
  this.pendingSdkQueries.delete(sessionId);
2452
2473
  this.lastStreamSaveAt.delete(sessionId);
2474
+ // SDK abort 收尾本就走 idle(不产生失败串),这里只是顺手清掉用户停止标记避免泄漏。
2475
+ this.userStopped.delete(sessionId);
2453
2476
  if (emitTimer)
2454
2477
  clearTimeout(emitTimer);
2455
2478
  flushEmit();