@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.
Files changed (62) hide show
  1. package/dist/build-info.json +4 -4
  2. package/dist/claude-sdk-runner.d.ts +2 -3
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +28 -0
  5. package/dist/message-truncator.js +3 -3
  6. package/dist/models.d.ts +39 -2
  7. package/dist/models.js +302 -9
  8. package/dist/native/darwin-arm64/wand-render +0 -0
  9. package/dist/native/darwin-arm64/wand-render.sha256 +1 -1
  10. package/dist/native/darwin-arm64/wand-render.version +1 -1
  11. package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
  12. package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
  13. package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
  14. package/dist/native/darwin-x64/wand-render +0 -0
  15. package/dist/native/darwin-x64/wand-render.sha256 +1 -1
  16. package/dist/native/darwin-x64/wand-render.version +1 -1
  17. package/dist/native/darwin-x64/wand-structured-renderd +0 -0
  18. package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
  19. package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
  20. package/dist/native/linux-arm64/wand-render +0 -0
  21. package/dist/native/linux-arm64/wand-render.sha256 +1 -1
  22. package/dist/native/linux-arm64/wand-render.version +1 -1
  23. package/dist/native/linux-arm64/wand-structured-renderd +0 -0
  24. package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
  25. package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
  26. package/dist/native/linux-x64/wand-render +0 -0
  27. package/dist/native/linux-x64/wand-render.sha256 +1 -1
  28. package/dist/native/linux-x64/wand-render.version +1 -1
  29. package/dist/native/linux-x64/wand-structured-renderd +0 -0
  30. package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
  31. package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
  32. package/dist/process-manager.d.ts +2 -1
  33. package/dist/process-manager.js +15 -3
  34. package/dist/pty-terminal-state.d.ts +17 -0
  35. package/dist/pty-terminal-state.js +51 -1
  36. package/dist/render-daemon-client.d.ts +33 -2
  37. package/dist/render-daemon-client.js +66 -16
  38. package/dist/render-host.d.ts +27 -3
  39. package/dist/render-host.js +156 -17
  40. package/dist/server-session-routes.js +42 -3
  41. package/dist/server-task-routes.js +3 -3
  42. package/dist/server.js +12 -4
  43. package/dist/session-ai-context.d.ts +1 -1
  44. package/dist/session-ai-context.js +15 -4
  45. package/dist/session-logger.d.ts +2 -0
  46. package/dist/session-logger.js +44 -1
  47. package/dist/structured-grok-adapter.js +22 -0
  48. package/dist/structured-provider-common.d.ts +11 -9
  49. package/dist/structured-provider-common.js +60 -18
  50. package/dist/task-types.d.ts +2 -1
  51. package/dist/terminal-daemon-server.js +27 -4
  52. package/dist/tui/commands.js +183 -10
  53. package/dist/types.d.ts +11 -9
  54. package/dist/wand-task-sync.js +2 -1
  55. package/dist/web-ui/content/scripts.js +78 -72
  56. package/dist/web-ui/content/styles.css +1 -1
  57. package/dist/web-ui/content/tailwind.css +9 -0
  58. package/dist/web-ui/embedded-assets.js +2 -2
  59. package/dist/web-ui/thinking-efforts.d.ts +24 -0
  60. package/dist/web-ui/thinking-efforts.js +81 -0
  61. package/dist/ws-broadcast.js +11 -4
  62. package/package.json +1 -1
@@ -1 +1 @@
1
- 783bd070dad27f2ec879ed5853f813f7ffdb3914d719d3ed947ec86af12a7b88 wand-render
1
+ 1481b8f4688e5d0bfa2e6c1e4b41da288820fcfb43a737b027401e55334bedaf wand-render
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.3
@@ -0,0 +1 @@
1
+ f67206b4d171f26172787e8184e684d61170c30646ab7d72a1f4232b737b4738 wand-structured-renderd
Binary file
@@ -1 +1 @@
1
- 7acb6e92e8f049ac87d8159905115dabe5f9a33f9d310e3c98f38b798c4302e5 wand-render
1
+ e51aee7fde515b0356c1a6919b66812ae4ff679f693847f61e816166e99d8805 wand-render
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.3
@@ -0,0 +1 @@
1
+ af342aa8e7dcde0ce2563629921c9080131ed666ba743901ecfa22c31f04bb98 wand-structured-renderd
Binary file
@@ -1 +1 @@
1
- 7814590a02ae2606fedc395ee1708921aec3b16332edb46fa2369e43e9a589f5 wand-render
1
+ 27c00887d03a24867f120a67852e5d0666cf7ddae18c3d708cbcca9c45a82427 wand-render
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.3
@@ -0,0 +1 @@
1
+ 52e1ec6dc79695c6af248b4b3bce160b3fb7be2c5f786172c78e13685c89ce39 wand-structured-renderd
@@ -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.
@@ -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, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
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.readPtyOutput(id);
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 = thinkingEffortToOpenCodeVariant(thinkingEffort ?? null);
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
- this.committedData = this.serializer.serialize({ scrollback: 5000 });
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.reconnectDelayMs = RECONNECT_INITIAL_MS;
793
- this.reconnectFailureLogged = false;
794
- this.authBackoff = false;
807
+ this.noteReconnectSuccess();
808
+ return;
795
809
  }
796
810
  catch (error) {
797
811
  if (this.disposed)
798
812
  return;
799
- const stalled = this.noteReconnectFailure(error);
800
- if (stalled && this.reconnectTimer) {
801
- // 断线路径已经按普通退避排过一次;用 60s 的长退避覆盖它。
802
- clearTimeout(this.reconnectTimer);
803
- this.reconnectTimer = null;
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.scheduleReconnect();
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;
@@ -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
- /** detached spawn(stdio 忽略、unref),并等 socket + token 就绪。 */
64
- export declare function spawnDetachedRender(configPath: string, binaryPath: string, knownSessionIds?: readonly string[]): Promise<RenderDaemonClient>;
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>;