@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.
- package/dist/build-info.json +4 -4
- package/dist/daemon-maintenance-state.d.ts +2 -0
- package/dist/daemon-maintenance-targets.d.ts +1 -0
- package/dist/daemon-maintenance-targets.js +26 -0
- package/dist/daemon-maintenance.d.ts +7 -0
- package/dist/daemon-maintenance.js +69 -0
- package/dist/render-daemon-client.d.ts +2 -0
- package/dist/render-daemon-client.js +6 -0
- package/dist/server-daemon-maintenance-routes.d.ts +8 -0
- package/dist/server-daemon-maintenance-routes.js +19 -0
- package/dist/server.js +18 -3
- package/dist/web-ui/content/ai-teams.js +12 -12
- package/dist/web-ui/content/scripts.js +193 -193
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-10-
|
|
4
|
-
"version": "4.
|
|
5
|
-
"channel": "
|
|
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
|
|
699
|
-
|
|
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([
|