@co0ontty/wand 4.88.0 → 4.89.0-beta.gb602808

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": "b1ee94297085a10acc1bd9b7da943e1a12435851",
3
- "builtAt": "2026-10-09T07:15:49.297Z",
4
- "version": "4.88.0",
5
- "channel": "stable"
2
+ "commit": "b60280807f605ce884aea7ec30f202157dbb3047",
3
+ "builtAt": "2026-10-09T11:31:46.251Z",
4
+ "version": "4.89.0-beta.gb602808",
5
+ "channel": "beta"
6
6
  }
@@ -2,5 +2,7 @@
2
2
  export interface DaemonMaintenanceStatus {
3
3
  pending: boolean;
4
4
  phase: "idle" | "waiting" | "updating" | "retrying";
5
+ /** HTTP view only; lifecycle status has no caller-specific permissions. */
6
+ canForceUpdate?: boolean;
5
7
  }
6
8
  export declare function daemonMaintenanceMessage(status: DaemonMaintenanceStatus): string;
@@ -13,6 +13,7 @@ export declare function persistentDaemonTarget(options: {
13
13
  checkpointPath: string;
14
14
  snapshot(includeInventory?: boolean): Promise<OwnerSnapshot>;
15
15
  shutdown(snapshot: OwnerSnapshot): Promise<boolean | void>;
16
+ interrupt?(): Promise<void>;
16
17
  ensure(): Promise<void>;
17
18
  reconnect(): Promise<void>;
18
19
  }): DaemonMaintenanceTarget;
@@ -74,6 +74,16 @@ export function persistentDaemonTarget(options) {
74
74
  unlinkSync(options.checkpointPath);
75
75
  return snapshot;
76
76
  },
77
+ async interrupt(expected) {
78
+ const current = await options.snapshot();
79
+ if (current.identity !== expected.identity)
80
+ throw new Error(`${options.name} owner changed`);
81
+ if (current.running !== 0) {
82
+ if (!options.interrupt)
83
+ throw new Error(`${options.name} cannot interrupt running executions`);
84
+ await options.interrupt();
85
+ }
86
+ },
77
87
  async restart(expected) {
78
88
  const current = await options.snapshot();
79
89
  if (current.identity !== expected.identity || !current.pending || current.running !== 0) {
@@ -120,6 +130,7 @@ export async function createDaemonMaintenanceTargets(configPath, pty, structured
120
130
  pending: shouldUpgradeRenderDaemon(hello.version, version, 0), running: includeInventory ? countRunningDaemonEntries(sessions) : null };
121
131
  },
122
132
  shutdown: () => render.requestShutdownDrain(),
133
+ interrupt: () => render.interruptMaintenanceSessions(),
123
134
  async ensure() { const client = await createRenderTerminalHost(configPath, { binaryPath: binary ?? undefined }); client.disconnect(); },
124
135
  reconnect: () => render.connect(),
125
136
  }));
@@ -157,6 +168,16 @@ export async function createDaemonMaintenanceTargets(configPath, pty, structured
157
168
  process.kill(snapshot.pid, "SIGTERM");
158
169
  }
159
170
  },
171
+ async interrupt() {
172
+ const sessions = await legacy.request("list");
173
+ const runs = await legacy.request("structuredList");
174
+ await Promise.all([
175
+ ...sessions.filter(session => session.status === "running")
176
+ .map(session => legacy.request("kill", { sessionId: session.sessionId, signal: "SIGTERM" })),
177
+ ...runs.filter(run => run.status === "running")
178
+ .map(run => legacy.request("structuredKill", { runId: run.runId, signal: "SIGTERM" })),
179
+ ]);
180
+ },
160
181
  async ensure() { const client = await createTerminalHost(configPath); client.disconnect(); },
161
182
  reconnect: () => legacy.connect(),
162
183
  }));
@@ -175,6 +196,11 @@ export async function createDaemonMaintenanceTargets(configPath, pty, structured
175
196
  running: result ? countRunningDaemonEntries(result.runs) : null };
176
197
  },
177
198
  async shutdown() { await structured.request("shutdown", { mode: "drain" }); },
199
+ async interrupt() {
200
+ const { runs } = await structured.request("list");
201
+ await Promise.all(runs.filter(run => run.status === "running")
202
+ .map(run => structured.request("interrupt", { runId: run.runId, signal: "SIGTERM" })));
203
+ },
178
204
  async ensure() { const client = await startStructuredRenderHost(configPath); client.disconnect(); },
179
205
  reconnect: () => structured.connect(),
180
206
  }));
@@ -23,6 +23,8 @@ export interface DaemonMaintenanceTarget {
23
23
  inspect(includeInventory?: boolean): Promise<DaemonInspection>;
24
24
  /** Must recheck identity and use graceful shutdown, never a force-kill. */
25
25
  restart(expected: DaemonInspection): Promise<void>;
26
+ /** Explicit manual update only: interrupt authenticated live inventory. */
27
+ interrupt?(expected: DaemonInspection): Promise<void>;
26
28
  }
27
29
  export interface DaemonMaintenanceOptions {
28
30
  targets: DaemonMaintenanceTarget[];
@@ -30,6 +32,8 @@ export interface DaemonMaintenanceOptions {
30
32
  /** Includes Core, starting runs, final checkpoints and queued input. */
31
33
  busy(): boolean;
32
34
  beginCoreDrain(): () => void;
35
+ stopExecutions?(): void;
36
+ coreBusy?(): boolean;
33
37
  available(): boolean;
34
38
  log(error: unknown): void;
35
39
  now?: () => number;
@@ -44,10 +48,13 @@ export declare class DaemonMaintenance {
44
48
  private stopped;
45
49
  private idleSince;
46
50
  private failures;
51
+ private forcing;
47
52
  constructor(options: DaemonMaintenanceOptions);
48
53
  status(): DaemonMaintenanceStatus;
49
54
  start(): void;
50
55
  check(): Promise<void>;
51
56
  stop(): Promise<void>;
57
+ /** Never used by polling. The caller must authenticate and confirm interruption. */
58
+ forceUpdate(): Promise<DaemonMaintenanceStatus>;
52
59
  private inspectAndUpdate;
53
60
  }
@@ -61,6 +61,7 @@ export class DaemonMaintenance {
61
61
  stopped = false;
62
62
  idleSince = null;
63
63
  failures = 0;
64
+ forcing = null;
64
65
  constructor(options) {
65
66
  this.options = options;
66
67
  }
@@ -80,6 +81,8 @@ export class DaemonMaintenance {
80
81
  check() {
81
82
  if (this.stopped)
82
83
  return Promise.resolve();
84
+ if (this.forcing)
85
+ return this.forcing.then(() => { }, () => { });
83
86
  if (this.flight)
84
87
  return this.flight;
85
88
  this.flight = this.inspectAndUpdate().catch((error) => {
@@ -99,6 +102,72 @@ export class DaemonMaintenance {
99
102
  clearTimeout(this.timer);
100
103
  this.timer = null;
101
104
  await this.flight;
105
+ await this.forcing?.catch(() => { });
106
+ }
107
+ /** Never used by polling. The caller must authenticate and confirm interruption. */
108
+ forceUpdate() {
109
+ if (this.forcing)
110
+ return this.forcing;
111
+ const previous = this.flight;
112
+ this.forcing = (async () => {
113
+ await previous;
114
+ const o = this.options;
115
+ if (this.stopped || !o.available())
116
+ throw new Error("当前无法更新,请稍后重试。");
117
+ return o.admission.exclusive(async () => {
118
+ if (this.stopped || !o.available())
119
+ throw new Error("当前无法更新,请稍后重试。");
120
+ const releaseCore = o.beginCoreDrain();
121
+ try {
122
+ let fresh = await Promise.all(o.targets.map(target => target.inspect()));
123
+ if (!fresh.some(snapshot => snapshot.pending)) {
124
+ this.state = { pending: false, phase: "idle" };
125
+ return this.status();
126
+ }
127
+ this.state = { pending: true, phase: "waiting" };
128
+ if (fresh.some(snapshot => snapshot.running === null))
129
+ throw new Error("底层组件执行清单不可用,请稍后重试。");
130
+ this.state = { pending: true, phase: "updating" };
131
+ // Keep accepted input queues and history; invalidate active callbacks
132
+ // through their managers before interrupting daemon-owned executions.
133
+ o.stopExecutions?.();
134
+ const interrupted = await Promise.allSettled(o.targets.map((target, i) => target.interrupt?.(fresh[i])));
135
+ for (const result of interrupted)
136
+ if (result.status === "rejected")
137
+ throw result.reason;
138
+ const deadline = Date.now() + 10_000;
139
+ while (fresh.some(snapshot => snapshot.running !== 0) || o.coreBusy?.()) {
140
+ if (Date.now() >= deadline || this.stopped || !o.available()) {
141
+ throw new Error("执行尚未停止,更新未完成。系统会在空闲后自动重试。");
142
+ }
143
+ await new Promise(resolve => setTimeout(resolve, 50));
144
+ fresh = await Promise.all(o.targets.map(target => target.inspect()));
145
+ }
146
+ for (let i = 0; i < fresh.length; i++) {
147
+ if (this.stopped || !o.available())
148
+ throw new Error("更新已暂停,请稍后重试。");
149
+ if (fresh[i].pending)
150
+ await o.targets[i].restart(fresh[i]);
151
+ }
152
+ const after = await Promise.all(o.targets.map(target => target.inspect()));
153
+ if (after.some(snapshot => snapshot.pending))
154
+ throw new Error("底层组件更新未完成,请稍后重试。");
155
+ this.state = { pending: false, phase: "idle" };
156
+ this.failures = 0;
157
+ return this.status();
158
+ }
159
+ catch (error) {
160
+ if (this.state.pending)
161
+ this.state = { pending: true, phase: "retrying" };
162
+ throw error;
163
+ }
164
+ finally {
165
+ this.idleSince = null;
166
+ releaseCore();
167
+ }
168
+ });
169
+ })().finally(() => { this.forcing = null; });
170
+ return this.forcing;
102
171
  }
103
172
  async inspectAndUpdate() {
104
173
  const o = this.options;
@@ -108,6 +108,8 @@ export declare class RenderDaemonClient implements TerminalHost {
108
108
  sessions: TerminalSessionState[] | null;
109
109
  }>;
110
110
  requestShutdownDrain(): Promise<void>;
111
+ /** Explicit confirmed maintenance only; does not shut down the daemon itself. */
112
+ interruptMaintenanceSessions(): Promise<void>;
111
113
  connect(): Promise<void>;
112
114
  private open;
113
115
  /**
@@ -233,6 +233,12 @@ export class RenderDaemonClient {
233
233
  async requestShutdownDrain() {
234
234
  await this.request("shutdown", { mode: "drain" });
235
235
  }
236
+ /** Explicit confirmed maintenance only; does not shut down the daemon itself. */
237
+ async interruptMaintenanceSessions() {
238
+ const sessions = await this.listSessions();
239
+ await Promise.all(sessions.filter(session => session.status === "running")
240
+ .map(session => this.request("kill", { sessionId: session.sessionId, signal: "SIGTERM" })));
241
+ }
236
242
  connect() {
237
243
  if (this.disposed)
238
244
  return Promise.reject(new Error("Render client disposed"));
@@ -0,0 +1,8 @@
1
+ import type { Express, Request, RequestHandler } from "express";
2
+ import type { DaemonMaintenance } from "./daemon-maintenance.js";
3
+ export declare function registerDaemonMaintenanceRoutes(app: Express, options: {
4
+ requireAdmin: RequestHandler;
5
+ canForceUpdate(req: Request): boolean;
6
+ maintenance: Pick<DaemonMaintenance, "status" | "forceUpdate">;
7
+ log(error: unknown): void;
8
+ }): void;
@@ -0,0 +1,19 @@
1
+ export function registerDaemonMaintenanceRoutes(app, options) {
2
+ const { maintenance } = options;
3
+ app.get("/api/daemon-maintenance", (req, res) => {
4
+ res.set("Cache-Control", "no-store").json({ ...maintenance.status(), canForceUpdate: options.canForceUpdate(req) });
5
+ });
6
+ app.post("/api/daemon-maintenance/force-update", options.requireAdmin, async (req, res) => {
7
+ if (req.body?.confirmInterrupt !== true) {
8
+ res.status(400).json({ error: "请先确认中断正在执行的任务。" });
9
+ return;
10
+ }
11
+ try {
12
+ res.set("Cache-Control", "no-store").json(await maintenance.forceUpdate());
13
+ }
14
+ catch (error) {
15
+ options.log(error);
16
+ res.status(409).json({ error: "强制更新未完成,执行可能仍在停止或组件暂不可用。系统会在空闲后自动重试。", ...maintenance.status() });
17
+ }
18
+ });
19
+ }
package/dist/server.js CHANGED
@@ -86,6 +86,7 @@ import { TerminalDaemonClient } from "./terminal-daemon-client.js";
86
86
  import { createUpgradeAwareTerminalHost } from "./render-host.js";
87
87
  import { DaemonAdmission, DaemonMaintenance } from "./daemon-maintenance.js";
88
88
  import { createDaemonMaintenanceTargets } from "./daemon-maintenance-targets.js";
89
+ import { registerDaemonMaintenanceRoutes } from "./server-daemon-maintenance-routes.js";
89
90
  import { terminalDaemonBuildIsCurrent } from "./terminal-daemon-build.js";
90
91
  import { createUpgradeAwareStructuredHost } from "./render-structured-host.js";
91
92
  import { checkPasswordRateLimit, recordFailedPassword, resetPasswordRateLimit } from "./middleware/rate-limit.js";
@@ -467,6 +468,17 @@ export async function startServer(config, configPath, options = {}) {
467
468
  busy: () => structuredSessions.getCoreTurnStatus().hasActiveTurns
468
469
  || [...processes.list(), ...structuredSessions.list()].some((session) => session.status === "running" || session.structuredState?.inFlight === true || (session.queuedMessages?.length ?? 0) > 0),
469
470
  beginCoreDrain: () => structuredSessions.beginCoreRestartDrain(),
471
+ coreBusy: () => structuredSessions.getCoreTurnStatus().hasActiveTurns,
472
+ stopExecutions: () => {
473
+ for (const session of structuredSessions.list()) {
474
+ if (session.status === "running" || session.structuredState?.inFlight)
475
+ structuredSessions.stop(session.id);
476
+ }
477
+ for (const session of processes.list()) {
478
+ if (session.status === "running")
479
+ processes.stop(session.id);
480
+ }
481
+ },
470
482
  available: () => !shuttingDown && !updateState.updateInFlight && !updateState.providerCliUpdateInFlight
471
483
  && terminalDaemonBuildIsCurrent(),
472
484
  log: (error) => wandError("底层组件自动更新暂缓,将稍后重试", getErrorMessage(error)),
@@ -695,9 +707,12 @@ export async function startServer(config, configPath, options = {}) {
695
707
  });
696
708
  registerDecisionRoutes(app, { storage, decisions: decisionExpert, requireAuth, requireSessions });
697
709
  app.use("/api", requireAuth);
698
- app.get("/api/daemon-maintenance", (_req, res) => {
699
- res.set("Cache-Control", "no-store").json(daemonMaintenance.status());
700
- });
710
+ registerDaemonMaintenanceRoutes(app, { requireAdmin, maintenance: daemonMaintenance,
711
+ canForceUpdate: req => {
712
+ const principal = requestPrincipals.get(req);
713
+ return !!principal && principalHasScope(principal, "admin");
714
+ },
715
+ log: error => wandError("底层组件强制更新未完成", getErrorMessage(error)) });
701
716
  // Connected apps receive only the route families used by native clients and
702
717
  // the browser extension. Browser-admin sessions implicitly satisfy all scopes.
703
718
  app.use([