@co0ontty/wand 4.73.0 → 4.75.1-beta.gbf7dafd
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/claude-sdk-runner.d.ts +2 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +28 -0
- package/dist/message-truncator.js +3 -3
- package/dist/models.d.ts +39 -2
- package/dist/models.js +302 -9
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -1
- package/dist/native/darwin-arm64/wand-render.version +1 -1
- package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/darwin-x64/wand-render +0 -0
- package/dist/native/darwin-x64/wand-render.sha256 +1 -1
- package/dist/native/darwin-x64/wand-render.version +1 -1
- package/dist/native/darwin-x64/wand-structured-renderd +0 -0
- package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-arm64/wand-render +0 -0
- package/dist/native/linux-arm64/wand-render.sha256 +1 -1
- package/dist/native/linux-arm64/wand-render.version +1 -1
- package/dist/native/linux-arm64/wand-structured-renderd +0 -0
- package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-x64/wand-render +0 -0
- package/dist/native/linux-x64/wand-render.sha256 +1 -1
- package/dist/native/linux-x64/wand-render.version +1 -1
- package/dist/native/linux-x64/wand-structured-renderd +0 -0
- package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
- package/dist/process-manager.d.ts +2 -1
- package/dist/process-manager.js +15 -3
- package/dist/pty-terminal-state.d.ts +17 -0
- package/dist/pty-terminal-state.js +51 -1
- package/dist/render-daemon-client.d.ts +33 -2
- package/dist/render-daemon-client.js +66 -16
- package/dist/render-host.d.ts +27 -3
- package/dist/render-host.js +156 -17
- package/dist/server-session-routes.js +42 -3
- package/dist/server-task-routes.js +3 -3
- package/dist/server.js +12 -4
- package/dist/session-ai-context.d.ts +1 -1
- package/dist/session-ai-context.js +15 -4
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +44 -1
- package/dist/structured-grok-adapter.js +22 -0
- package/dist/structured-provider-common.d.ts +11 -9
- package/dist/structured-provider-common.js +60 -18
- package/dist/task-types.d.ts +2 -1
- package/dist/terminal-daemon-server.js +27 -4
- package/dist/tui/commands.js +183 -10
- package/dist/types.d.ts +11 -9
- package/dist/wand-task-sync.js +2 -1
- package/dist/web-ui/content/scripts.js +78 -72
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +9 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/thinking-efforts.d.ts +24 -0
- package/dist/web-ui/thinking-efforts.js +81 -0
- package/dist/ws-broadcast.js +11 -4
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1481b8f4688e5d0bfa2e6c1e4b41da288820fcfb43a737b027401e55334bedaf wand-render
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.3
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
f67206b4d171f26172787e8184e684d61170c30646ab7d72a1f4232b737b4738 wand-structured-renderd
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.3
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
e51aee7fde515b0356c1a6919b66812ae4ff679f693847f61e816166e99d8805 wand-render
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.3
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
af342aa8e7dcde0ce2563629921c9080131ed666ba743901ecfa22c31f04bb98 wand-structured-renderd
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.3
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
27c00887d03a24867f120a67852e5d0666cf7ddae18c3d708cbcca9c45a82427 wand-render
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.3
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
52e1ec6dc79695c6af248b4b3bce160b3fb7be2c5f786172c78e13685c89ce39 wand-structured-renderd
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.3
|
|
@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import { WandStorage } from "./storage.js";
|
|
3
3
|
import { ExecutionMode, ProcessEventHandler, SessionProvider, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
|
|
4
4
|
import { type PermissionResolution } from "./claude-pty-bridge.js";
|
|
5
|
-
import { type PtyTerminalSnapshot } from "./pty-terminal-state.js";
|
|
5
|
+
import { type PtyTerminalSnapshot, type PtyHistoryPage } from "./pty-terminal-state.js";
|
|
6
6
|
import { type TerminalHost } from "./terminal-host.js";
|
|
7
7
|
import { type ClaudeHistorySession, type CodexHistorySession, type OpenCodeHistorySession, type QoderHistorySession } from "./provider-history-scanner.js";
|
|
8
8
|
export type { ClaudeHistorySession, CodexHistorySession, OpenCodeHistorySession, QoderHistorySession, } from "./provider-history-scanner.js";
|
|
@@ -119,6 +119,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
119
119
|
private trackPtyInput;
|
|
120
120
|
resize(id: string, cols: number, rows: number): SessionSnapshot;
|
|
121
121
|
getTerminalState(id: string): PtyTerminalSnapshot | null;
|
|
122
|
+
getTerminalHistoryPage(id: string, before: number, revision: number): Promise<PtyHistoryPage | null>;
|
|
122
123
|
/**
|
|
123
124
|
* Finalize provider-only state while leaving the owning PTY shell alive. The
|
|
124
125
|
* session itself remains running so terminal input can continue normally.
|
package/dist/process-manager.js
CHANGED
|
@@ -13,7 +13,7 @@ import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
|
|
|
13
13
|
import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
|
|
14
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
15
15
|
import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
|
|
16
|
-
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort,
|
|
16
|
+
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToPiLevel, thinkingEffortToQoderEffort } from "./structured-provider-common.js";
|
|
17
17
|
import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
18
18
|
import { getErrorMessage } from "./error-utils.js";
|
|
19
19
|
import { recordIterationPrompt } from "./iteration-log.js";
|
|
@@ -1461,7 +1461,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1461
1461
|
return result;
|
|
1462
1462
|
}
|
|
1463
1463
|
getPtyTranscript(id) {
|
|
1464
|
-
return this.logger.
|
|
1464
|
+
return this.logger.readPtyOutputTail(id);
|
|
1465
1465
|
}
|
|
1466
1466
|
/**
|
|
1467
1467
|
* Set the Claude model for an existing PTY session. Persists the selection
|
|
@@ -1622,6 +1622,14 @@ export class ProcessManager extends EventEmitter {
|
|
|
1622
1622
|
}
|
|
1623
1623
|
return record.terminalState.snapshot();
|
|
1624
1624
|
}
|
|
1625
|
+
async getTerminalHistoryPage(id, before, revision) {
|
|
1626
|
+
const record = this.sessions.get(id);
|
|
1627
|
+
if (!record)
|
|
1628
|
+
return null;
|
|
1629
|
+
if (!record.terminalState)
|
|
1630
|
+
this.getTerminalState(id);
|
|
1631
|
+
return record.terminalState?.historyPage(before, revision) ?? null;
|
|
1632
|
+
}
|
|
1625
1633
|
/**
|
|
1626
1634
|
* Finalize provider-only state while leaving the owning PTY shell alive. The
|
|
1627
1635
|
* session itself remains running so terminal input can continue normally.
|
|
@@ -2474,7 +2482,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
2474
2482
|
if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result) && !/(?:^|\s)-m(?:\s|$)/.test(result)) {
|
|
2475
2483
|
result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
|
|
2476
2484
|
}
|
|
2477
|
-
const effort =
|
|
2485
|
+
const effort = thinkingEffortToGrokEffort(thinkingEffort ?? null);
|
|
2478
2486
|
if (effort && !/--(?:reasoning-)?effort(?:\s|=)/.test(result)) {
|
|
2479
2487
|
result += ` --effort '${effort.replace(/'/g, "'\\''")}'`;
|
|
2480
2488
|
}
|
|
@@ -2494,6 +2502,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
2494
2502
|
if (!/--(?:yolo|dangerously-skip-permissions|permission-mode)(?:\s|=|$)/.test(result)) {
|
|
2495
2503
|
result += " --permission-mode bypass_permissions";
|
|
2496
2504
|
}
|
|
2505
|
+
const qoderEffort = thinkingEffortToQoderEffort(thinkingEffort ?? null);
|
|
2506
|
+
if (qoderEffort && !/--reasoning-effort(?:\s|=)/.test(result)) {
|
|
2507
|
+
result += ` --reasoning-effort '${qoderEffort.replace(/'/g, "'\\''")}'`;
|
|
2508
|
+
}
|
|
2497
2509
|
return result;
|
|
2498
2510
|
}
|
|
2499
2511
|
if (provider === "pi") {
|
|
@@ -12,7 +12,19 @@ export interface PtyTerminalSnapshot {
|
|
|
12
12
|
cols: number;
|
|
13
13
|
rows: number;
|
|
14
14
|
pending: PtyTerminalOperation[];
|
|
15
|
+
/** Web scrollback cursor, absent from the Render wire protocol. */
|
|
16
|
+
historyBefore?: number;
|
|
17
|
+
historyRevision?: number;
|
|
15
18
|
}
|
|
19
|
+
export interface PtyHistoryPage {
|
|
20
|
+
data: string;
|
|
21
|
+
separator: string;
|
|
22
|
+
start: number;
|
|
23
|
+
before: number;
|
|
24
|
+
revision: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const PTY_INITIAL_HISTORY_ROWS = 80;
|
|
27
|
+
export declare const PTY_HISTORY_PAGE_ROWS = 80;
|
|
16
28
|
/**
|
|
17
29
|
* Server-side xterm screen model used only for reconnect snapshots. Live PTY
|
|
18
30
|
* bytes still travel unchanged from node-pty to the browser terminal.
|
|
@@ -31,11 +43,16 @@ export declare class PtyTerminalState {
|
|
|
31
43
|
private committedData;
|
|
32
44
|
private committedCols;
|
|
33
45
|
private committedRows;
|
|
46
|
+
private committedHistoryBefore;
|
|
47
|
+
private committedRevision;
|
|
48
|
+
private revision;
|
|
34
49
|
private disposed;
|
|
35
50
|
constructor(cols: number, rows: number, initialData?: string);
|
|
36
51
|
write(data: string): void;
|
|
37
52
|
resize(cols: number, rows: number): void;
|
|
38
53
|
snapshot(): PtyTerminalSnapshot;
|
|
54
|
+
/** A bounded ANSI range preceding the committed baseline, never a cumulative snapshot. */
|
|
55
|
+
historyPage(before: number, revision: number, limit?: number): Promise<PtyHistoryPage | null>;
|
|
39
56
|
dispose(): void;
|
|
40
57
|
private addPending;
|
|
41
58
|
private scheduleFlush;
|
|
@@ -2,6 +2,10 @@ import headless from "@xterm/headless";
|
|
|
2
2
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
3
3
|
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
|
4
4
|
const { Terminal } = headless;
|
|
5
|
+
export const PTY_INITIAL_HISTORY_ROWS = 80;
|
|
6
|
+
const PTY_INITIAL_HISTORY_MAX_BYTES = 256 * 1024;
|
|
7
|
+
export const PTY_HISTORY_PAGE_ROWS = 80;
|
|
8
|
+
const PTY_HISTORY_PAGE_MAX_BYTES = 192 * 1024;
|
|
5
9
|
/**
|
|
6
10
|
* Incoming PTY chunks are coalesced into one xterm write per flush. xterm's
|
|
7
11
|
* write callback costs a timer turn per call, so one write per node-pty chunk
|
|
@@ -63,6 +67,9 @@ export class PtyTerminalState {
|
|
|
63
67
|
committedData = "";
|
|
64
68
|
committedCols;
|
|
65
69
|
committedRows;
|
|
70
|
+
committedHistoryBefore = 0;
|
|
71
|
+
committedRevision = 0;
|
|
72
|
+
revision = 0;
|
|
66
73
|
disposed = false;
|
|
67
74
|
constructor(cols, rows, initialData = "") {
|
|
68
75
|
this.committedCols = cols;
|
|
@@ -77,6 +84,7 @@ export class PtyTerminalState {
|
|
|
77
84
|
write(data) {
|
|
78
85
|
if (this.disposed || !data)
|
|
79
86
|
return;
|
|
87
|
+
this.revision++;
|
|
80
88
|
this.buffered += data;
|
|
81
89
|
if (this.buffered.length >= WRITE_BATCH_MAX_CHARS)
|
|
82
90
|
this.flushBuffered();
|
|
@@ -86,6 +94,7 @@ export class PtyTerminalState {
|
|
|
86
94
|
resize(cols, rows) {
|
|
87
95
|
if (this.disposed)
|
|
88
96
|
return;
|
|
97
|
+
this.revision++;
|
|
89
98
|
// Bytes that arrived before the resize must land before it.
|
|
90
99
|
this.flushBuffered();
|
|
91
100
|
this.addPending({ type: "resize", cols, rows });
|
|
@@ -105,6 +114,37 @@ export class PtyTerminalState {
|
|
|
105
114
|
cols: this.committedCols,
|
|
106
115
|
rows: this.committedRows,
|
|
107
116
|
pending: coalesceOperations(this.pending),
|
|
117
|
+
historyBefore: this.committedHistoryBefore,
|
|
118
|
+
historyRevision: this.committedRevision,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** A bounded ANSI range preceding the committed baseline, never a cumulative snapshot. */
|
|
122
|
+
async historyPage(before, revision, limit = PTY_HISTORY_PAGE_ROWS) {
|
|
123
|
+
if (this.disposed || !Number.isSafeInteger(before) || !Number.isSafeInteger(revision)
|
|
124
|
+
|| !Number.isSafeInteger(limit) || limit < 1 || limit > PTY_HISTORY_PAGE_ROWS)
|
|
125
|
+
return null;
|
|
126
|
+
// Wait for buffered writes/checkpoints. If output or resize changed the row indices,
|
|
127
|
+
// the caller must resync rather than concatenate unrelated lines.
|
|
128
|
+
await this.tail;
|
|
129
|
+
if (this.revision !== revision || before > this.committedHistoryBefore || before <= 0)
|
|
130
|
+
return null;
|
|
131
|
+
const count = Math.min(before, limit);
|
|
132
|
+
let start = before - count;
|
|
133
|
+
let data = "";
|
|
134
|
+
while (start < before) {
|
|
135
|
+
data = this.serializer.serialize({ range: { start, end: before - 1 } });
|
|
136
|
+
if (Buffer.byteLength(data, "utf8") <= PTY_HISTORY_PAGE_MAX_BYTES)
|
|
137
|
+
break;
|
|
138
|
+
if (start === before - 1)
|
|
139
|
+
return null;
|
|
140
|
+
start = Math.min(before - 1, start + Math.max(1, Math.ceil((before - start) / 2)));
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
data,
|
|
144
|
+
separator: this.terminal.buffer.normal.getLine(before)?.isWrapped ? "" : "\r\n",
|
|
145
|
+
start,
|
|
146
|
+
before,
|
|
147
|
+
revision,
|
|
108
148
|
};
|
|
109
149
|
}
|
|
110
150
|
dispose() {
|
|
@@ -186,14 +226,24 @@ export class PtyTerminalState {
|
|
|
186
226
|
const cutoff = this.pending.at(-1)?.id ?? 0;
|
|
187
227
|
if (!cutoff)
|
|
188
228
|
return;
|
|
229
|
+
const revision = this.revision;
|
|
189
230
|
this.checkpointQueued = true;
|
|
190
231
|
this.tail = this.tail.then(() => {
|
|
191
232
|
if (this.disposed)
|
|
192
233
|
return;
|
|
193
234
|
try {
|
|
194
|
-
|
|
235
|
+
let historyRows = PTY_INITIAL_HISTORY_ROWS;
|
|
236
|
+
this.committedData = this.serializer.serialize({ scrollback: historyRows });
|
|
237
|
+
while (historyRows > 0 && Buffer.byteLength(this.committedData) > PTY_INITIAL_HISTORY_MAX_BYTES) {
|
|
238
|
+
historyRows = Math.floor(historyRows / 2);
|
|
239
|
+
this.committedData = this.serializer.serialize({ scrollback: historyRows });
|
|
240
|
+
}
|
|
195
241
|
this.committedCols = this.terminal.cols;
|
|
196
242
|
this.committedRows = this.terminal.rows;
|
|
243
|
+
this.committedHistoryBefore = Math.max(0, this.terminal.buffer.normal.baseY - historyRows);
|
|
244
|
+
// A newer write can arrive while the queued checkpoint drains. Only the
|
|
245
|
+
// operations through cutoff are represented by this baseline.
|
|
246
|
+
this.committedRevision = revision;
|
|
197
247
|
this.pending = this.pending.filter((entry) => entry.id > cutoff);
|
|
198
248
|
}
|
|
199
249
|
catch {
|
|
@@ -10,6 +10,15 @@ import { type TerminalAttachResult, type TerminalHost, type TerminalSessionState
|
|
|
10
10
|
export declare class RenderAuthError extends Error {
|
|
11
11
|
constructor(message: string);
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* 远端 PTY 句柄。Render 可能在这个对象挂上 listener 之前就推来 data/exit
|
|
15
|
+
* (大输出场景几乎必然发生),所以先做有界缓存,挂上后按序补发。
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* 运行时自愈回调:把「端点已消失、且 config 上没有活着的 daemon」的 Render 重新拉起来。
|
|
19
|
+
* 返回 true 表示起了新进程(调用方应立刻重连)。
|
|
20
|
+
*/
|
|
21
|
+
export type RenderDaemonReviver = () => Promise<boolean>;
|
|
13
22
|
/**
|
|
14
23
|
* Render(常驻 PTY 持有者)的 Node 客户端。
|
|
15
24
|
*
|
|
@@ -28,6 +37,11 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
28
37
|
private readonly tokenPath;
|
|
29
38
|
/** Persisted PTY IDs used only if v1 `list` exceeds its aggregate frame limit. */
|
|
30
39
|
private readonly knownSessionIds;
|
|
40
|
+
/**
|
|
41
|
+
* 运行时自愈回调:重连失败时问它能不能把 daemon 拉回来(返回 true = 起了新进程)。
|
|
42
|
+
* 只有「端点已消失 + config 上没有活着的 daemon」时它才动手,见 `render-host.ts`。
|
|
43
|
+
*/
|
|
44
|
+
private readonly reviveDaemon;
|
|
31
45
|
readonly persistent = true;
|
|
32
46
|
private socket;
|
|
33
47
|
private nextRequestId;
|
|
@@ -71,8 +85,21 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
71
85
|
*/
|
|
72
86
|
tokenPath?: string | null,
|
|
73
87
|
/** Persisted PTY IDs used only if v1 `list` exceeds its aggregate frame limit. */
|
|
74
|
-
knownSessionIds?: readonly string[]
|
|
88
|
+
knownSessionIds?: readonly string[],
|
|
89
|
+
/**
|
|
90
|
+
* 运行时自愈回调:重连失败时问它能不能把 daemon 拉回来(返回 true = 起了新进程)。
|
|
91
|
+
* 只有「端点已消失 + config 上没有活着的 daemon」时它才动手,见 `render-host.ts`。
|
|
92
|
+
*/
|
|
93
|
+
reviveDaemon?: RenderDaemonReviver | null);
|
|
75
94
|
get version(): string;
|
|
95
|
+
/** daemon 侧已知的会话数(`inventory` 是 `list`/`attach` 的权威投影)。 */
|
|
96
|
+
get sessionCount(): number;
|
|
97
|
+
/**
|
|
98
|
+
* 让 daemon 干净收摊(`shutdown mode=now`:杀掉所有 PTY 后退出)。
|
|
99
|
+
* **只允许用在「daemon 空闲」的升级场景**(sessionCount === 0);持有活会话时
|
|
100
|
+
* 调用它会杀掉用户的 shell。
|
|
101
|
+
*/
|
|
102
|
+
requestShutdownNow(): Promise<void>;
|
|
76
103
|
connect(): Promise<void>;
|
|
77
104
|
private open;
|
|
78
105
|
/**
|
|
@@ -137,6 +164,10 @@ export declare class RenderDaemonClient implements TerminalHost {
|
|
|
137
164
|
*/
|
|
138
165
|
private scheduleReconnect;
|
|
139
166
|
private tryReconnect;
|
|
167
|
+
private noteReconnectSuccess;
|
|
168
|
+
/** 自愈回调本身绝不能让重连路径抛错;失败按「没拉起」处理。 */
|
|
169
|
+
private reviveDaemonForRetry;
|
|
170
|
+
private noteReconnectFailureAndSchedule;
|
|
140
171
|
/**
|
|
141
172
|
* 重连失败的日志与「同一把废 token」判定。
|
|
142
173
|
*
|
|
@@ -168,4 +199,4 @@ export declare function normalizeRenderSessionState(value: unknown): TerminalSes
|
|
|
168
199
|
* 连接一个已经存在的 Render(socket + token 都在)。**不 spawn**,找不到就返回 null,
|
|
169
200
|
* 这样调用方可以区分「已被别处 adopt」和「需要自己拉起」。
|
|
170
201
|
*/
|
|
171
|
-
export declare function connectExistingRenderClient(configPath: string, knownSessionIds?: readonly string[]): Promise<RenderDaemonClient | null>;
|
|
202
|
+
export declare function connectExistingRenderClient(configPath: string, knownSessionIds?: readonly string[], reviveDaemon?: RenderDaemonReviver | null): Promise<RenderDaemonClient | null>;
|
|
@@ -32,10 +32,6 @@ export class RenderAuthError extends Error {
|
|
|
32
32
|
this.name = "RenderAuthError";
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
* 远端 PTY 句柄。Render 可能在这个对象挂上 listener 之前就推来 data/exit
|
|
37
|
-
* (大输出场景几乎必然发生),所以先做有界缓存,挂上后按序补发。
|
|
38
|
-
*/
|
|
39
35
|
class RemoteRenderProcess {
|
|
40
36
|
sessionId;
|
|
41
37
|
incarnationId;
|
|
@@ -149,6 +145,7 @@ export class RenderDaemonClient {
|
|
|
149
145
|
token;
|
|
150
146
|
tokenPath;
|
|
151
147
|
knownSessionIds;
|
|
148
|
+
reviveDaemon;
|
|
152
149
|
persistent = true;
|
|
153
150
|
socket = null;
|
|
154
151
|
nextRequestId = 1;
|
|
@@ -193,16 +190,34 @@ export class RenderDaemonClient {
|
|
|
193
190
|
*/
|
|
194
191
|
tokenPath = null,
|
|
195
192
|
/** Persisted PTY IDs used only if v1 `list` exceeds its aggregate frame limit. */
|
|
196
|
-
knownSessionIds = []
|
|
193
|
+
knownSessionIds = [],
|
|
194
|
+
/**
|
|
195
|
+
* 运行时自愈回调:重连失败时问它能不能把 daemon 拉回来(返回 true = 起了新进程)。
|
|
196
|
+
* 只有「端点已消失 + config 上没有活着的 daemon」时它才动手,见 `render-host.ts`。
|
|
197
|
+
*/
|
|
198
|
+
reviveDaemon = null) {
|
|
197
199
|
this.socketPath = socketPath;
|
|
198
200
|
this.token = token;
|
|
199
201
|
this.tokenPath = tokenPath;
|
|
200
202
|
this.knownSessionIds = knownSessionIds;
|
|
203
|
+
this.reviveDaemon = reviveDaemon;
|
|
201
204
|
this.lastKnownToken = token;
|
|
202
205
|
}
|
|
203
206
|
get version() {
|
|
204
207
|
return this.daemonVersion;
|
|
205
208
|
}
|
|
209
|
+
/** daemon 侧已知的会话数(`inventory` 是 `list`/`attach` 的权威投影)。 */
|
|
210
|
+
get sessionCount() {
|
|
211
|
+
return this.inventory.size;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 让 daemon 干净收摊(`shutdown mode=now`:杀掉所有 PTY 后退出)。
|
|
215
|
+
* **只允许用在「daemon 空闲」的升级场景**(sessionCount === 0);持有活会话时
|
|
216
|
+
* 调用它会杀掉用户的 shell。
|
|
217
|
+
*/
|
|
218
|
+
async requestShutdownNow() {
|
|
219
|
+
await this.request("shutdown", { mode: "now" });
|
|
220
|
+
}
|
|
206
221
|
connect() {
|
|
207
222
|
if (this.disposed)
|
|
208
223
|
return Promise.reject(new Error("Render client disposed"));
|
|
@@ -789,22 +804,57 @@ export class RenderDaemonClient {
|
|
|
789
804
|
return;
|
|
790
805
|
try {
|
|
791
806
|
await this.connect();
|
|
792
|
-
this.
|
|
793
|
-
|
|
794
|
-
this.authBackoff = false;
|
|
807
|
+
this.noteReconnectSuccess();
|
|
808
|
+
return;
|
|
795
809
|
}
|
|
796
810
|
catch (error) {
|
|
797
811
|
if (this.disposed)
|
|
798
812
|
return;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
813
|
+
// 端点本身没了(socket 文件被临时目录清理器删掉,或 daemon 崩了没留下 socket):
|
|
814
|
+
// 重试同一个路径永远不会成功,先把 daemon 拉回来再连一次。只要 pid 文件里还有
|
|
815
|
+
// 活进程,回调就不会动手 —— 那个 daemon 会自己重建端点(docs/render-protocol.md §6)。
|
|
816
|
+
if (await this.reviveDaemonForRetry()) {
|
|
817
|
+
try {
|
|
818
|
+
await this.connect();
|
|
819
|
+
this.noteReconnectSuccess();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
catch (retryError) {
|
|
823
|
+
this.noteReconnectFailureAndSchedule(retryError);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
804
826
|
}
|
|
805
|
-
this.
|
|
827
|
+
this.noteReconnectFailureAndSchedule(error);
|
|
806
828
|
}
|
|
807
829
|
}
|
|
830
|
+
noteReconnectSuccess() {
|
|
831
|
+
this.reconnectDelayMs = RECONNECT_INITIAL_MS;
|
|
832
|
+
this.reconnectFailureLogged = false;
|
|
833
|
+
this.authBackoff = false;
|
|
834
|
+
}
|
|
835
|
+
/** 自愈回调本身绝不能让重连路径抛错;失败按「没拉起」处理。 */
|
|
836
|
+
async reviveDaemonForRetry() {
|
|
837
|
+
if (!this.reviveDaemon)
|
|
838
|
+
return false;
|
|
839
|
+
try {
|
|
840
|
+
return await this.reviveDaemon();
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
process.stderr.write(`[wand] Render daemon revival failed: ${getErrorMessage(error)}\n`);
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
noteReconnectFailureAndSchedule(error) {
|
|
848
|
+
if (this.disposed)
|
|
849
|
+
return;
|
|
850
|
+
const stalled = this.noteReconnectFailure(error);
|
|
851
|
+
if (stalled && this.reconnectTimer) {
|
|
852
|
+
// 断线路径已经按普通退避排过一次;用 60s 的长退避覆盖它。
|
|
853
|
+
clearTimeout(this.reconnectTimer);
|
|
854
|
+
this.reconnectTimer = null;
|
|
855
|
+
}
|
|
856
|
+
this.scheduleReconnect();
|
|
857
|
+
}
|
|
808
858
|
/**
|
|
809
859
|
* 重连失败的日志与「同一把废 token」判定。
|
|
810
860
|
*
|
|
@@ -1087,7 +1137,7 @@ function asNumber(value, fallback, problems, label) {
|
|
|
1087
1137
|
* 连接一个已经存在的 Render(socket + token 都在)。**不 spawn**,找不到就返回 null,
|
|
1088
1138
|
* 这样调用方可以区分「已被别处 adopt」和「需要自己拉起」。
|
|
1089
1139
|
*/
|
|
1090
|
-
export async function connectExistingRenderClient(configPath, knownSessionIds = []) {
|
|
1140
|
+
export async function connectExistingRenderClient(configPath, knownSessionIds = [], reviveDaemon = null) {
|
|
1091
1141
|
const { socketPath, tokenPath } = renderPaths(configPath);
|
|
1092
1142
|
let token;
|
|
1093
1143
|
try {
|
|
@@ -1099,7 +1149,7 @@ export async function connectExistingRenderClient(configPath, knownSessionIds =
|
|
|
1099
1149
|
if (!token)
|
|
1100
1150
|
return null;
|
|
1101
1151
|
// 传 tokenPath 让重连路径在 daemon 轮换 token 后能自愈(见构造函数注释)。
|
|
1102
|
-
const client = new RenderDaemonClient(socketPath, token, tokenPath, knownSessionIds);
|
|
1152
|
+
const client = new RenderDaemonClient(socketPath, token, tokenPath, knownSessionIds, reviveDaemon);
|
|
1103
1153
|
try {
|
|
1104
1154
|
await client.connect();
|
|
1105
1155
|
return client;
|
package/dist/render-host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RenderDaemonClient } from "./render-daemon-client.js";
|
|
1
|
+
import { type RenderDaemonClient, type RenderDaemonReviver } from "./render-daemon-client.js";
|
|
2
2
|
import { TerminalDaemonClient } from "./terminal-daemon-client.js";
|
|
3
3
|
import { type TerminalAttachResult, type TerminalHost, type TerminalSpawnRequest } from "./terminal-host.js";
|
|
4
4
|
import type { RenderEngine } from "./types.js";
|
|
@@ -10,6 +10,18 @@ export interface RenderHostOptions {
|
|
|
10
10
|
/** DB 中仍在运行的 PTY,用于旧版 Render 的超大 list 失败时逐个 attach。 */
|
|
11
11
|
knownSessionIds?: readonly string[];
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* 运行时自愈:没有活着的 owner、且端点不可用时,起一个 detached Render。
|
|
15
|
+
* 返回 true 表示起了新进程(调用方应立刻重连)。
|
|
16
|
+
*
|
|
17
|
+
* 覆盖两种「重试永远不会好」的形状(都是同一个后果:终端一直断、只能重启整个服务):
|
|
18
|
+
* - daemon 没了,socket 文件也被清掉(临时目录清理器,线上真发生过);
|
|
19
|
+
* - daemon 被 kill -9 / 崩溃,socket 文件作为陈留文件留了下来(没有监听者)。
|
|
20
|
+
*
|
|
21
|
+
* 保守之处:pid 文件里还有活进程就不插手(那个 daemon 会自己重建端点,
|
|
22
|
+
* docs/render-protocol.md §6 single instance);能连上监听者也不插手;同一个窗口只 spawn 一次。
|
|
23
|
+
*/
|
|
24
|
+
export declare function createRenderDaemonReviver(configPath: string, binaryPath: string | null): RenderDaemonReviver;
|
|
13
25
|
export interface UpgradeAwareTerminalHost {
|
|
14
26
|
host: TerminalHost;
|
|
15
27
|
renderHost: TerminalHost | null;
|
|
@@ -60,5 +72,17 @@ export declare function createRenderTerminalHost(configPath: string, options?: {
|
|
|
60
72
|
binaryPath?: string;
|
|
61
73
|
knownSessionIds?: readonly string[];
|
|
62
74
|
}): Promise<RenderDaemonClient>;
|
|
63
|
-
/**
|
|
64
|
-
|
|
75
|
+
/**
|
|
76
|
+
* 该不该把正在跑的 daemon 换成装好的二进制:只有「装好的更新」且「daemon 空闲」才换。
|
|
77
|
+
* 纯决策,便于单测;daemon 持有 PTY 时绝不能动它(会杀掉用户的 shell)。
|
|
78
|
+
*/
|
|
79
|
+
export declare function shouldUpgradeRenderDaemon(daemonVersion: string | null, binaryVersion: string | null, sessionCount: number): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* detached spawn(stdio 忽略、unref),返回子进程 pid。
|
|
82
|
+
*
|
|
83
|
+
* `-c <configPath>` 是按 config 隔离的既定约定(docs/render-protocol.md §6):
|
|
84
|
+
* socket / token / pid / meta 全部由它派生。
|
|
85
|
+
*/
|
|
86
|
+
export declare function spawnDetachedRenderProcess(configPath: string, binaryPath: string): number | null;
|
|
87
|
+
/** spawn 并等 socket + token 就绪(启动路径用)。 */
|
|
88
|
+
export declare function spawnDetachedRender(configPath: string, binaryPath: string, knownSessionIds?: readonly string[], reviveDaemon?: RenderDaemonReviver | null): Promise<RenderDaemonClient>;
|