farming-code 2.2.7 → 2.2.11
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/README.md +110 -199
- package/README.zh_cn.md +106 -237
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1496 -737
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-iA2BU9P6.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-DpYuBonn.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const net = require('net');
|
|
5
6
|
const os = require('os');
|
|
@@ -20,6 +21,30 @@ const {
|
|
|
20
21
|
} = require('./local-session-engine');
|
|
21
22
|
const { deriveTerminalStatus } = require('./terminal-status');
|
|
22
23
|
const { probeUnixSocket } = require('./terminal-runtime-cleanup');
|
|
24
|
+
const { nativePtyHostRuntimeIdentity } = require('./native-pty-host-identity');
|
|
25
|
+
const {
|
|
26
|
+
allocateNativePtyRuntimeGeneration,
|
|
27
|
+
formatNativePtyRuntimeEpoch,
|
|
28
|
+
} = require('./native-pty-controller-generation');
|
|
29
|
+
const {
|
|
30
|
+
acknowledgeTerminalReducerData,
|
|
31
|
+
createTerminalReducerFlowControl,
|
|
32
|
+
ensureTerminalReducerFlowControl,
|
|
33
|
+
enqueueTerminalReducerData,
|
|
34
|
+
resetTerminalReducerFlowControl,
|
|
35
|
+
setTerminalExternalFlowControlBlocked,
|
|
36
|
+
} = require('./terminal-reducer-flow-control');
|
|
37
|
+
const {
|
|
38
|
+
normalizeTerminalStateEntry,
|
|
39
|
+
serializeTerminalState,
|
|
40
|
+
} = require('./terminal-state-serialization');
|
|
41
|
+
const {
|
|
42
|
+
captureTerminalAttachCheckpoint,
|
|
43
|
+
} = require('./terminal-attach-checkpoint');
|
|
44
|
+
const {
|
|
45
|
+
acceptTerminalExitData,
|
|
46
|
+
waitForTerminalExitDataQuiescence,
|
|
47
|
+
} = require('./terminal-exit-quiescence');
|
|
23
48
|
|
|
24
49
|
const OUTPUT_LIMIT = 10000;
|
|
25
50
|
const DEFAULT_COLS = 80;
|
|
@@ -27,6 +52,11 @@ const DEFAULT_ROWS = 30;
|
|
|
27
52
|
const OWNER_CHECK_INTERVAL_MS = 1000;
|
|
28
53
|
const DISPOSE_PTY_GRACE_MS = 800;
|
|
29
54
|
const DEFAULT_IDLE_EXIT_MS = 60000;
|
|
55
|
+
const DEFAULT_CLIENT_MAX_BUFFERED_BYTES = 16 * 1024 * 1024;
|
|
56
|
+
const DEFAULT_CLIENT_MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
57
|
+
const HOST_RUNTIME_IDENTITY = nativePtyHostRuntimeIdentity();
|
|
58
|
+
const CONTROLLER_MUTATION_ADMISSION = Symbol('controllerMutationAdmission');
|
|
59
|
+
const TERMINAL_HISTORY_RESTORED_MESSAGE = '\r\n\x1b[7m History restored \x1b[0m\r\n';
|
|
30
60
|
|
|
31
61
|
function trimOutput(output) {
|
|
32
62
|
const text = typeof output === 'string' ? output : '';
|
|
@@ -52,11 +82,32 @@ class NativePtyHost {
|
|
|
52
82
|
this.configDir = options.configDir || process.env.FARMING_CONFIG_DIR || path.join(os.homedir(), '.farming');
|
|
53
83
|
this.socketPath = options.socketPath || process.env.FARMING_NATIVE_PTY_HOST_SOCKET || nativePtyHostSocketPath(this.configDir);
|
|
54
84
|
this.ownerPid = Number(options.ownerPid || process.env.FARMING_NATIVE_PTY_HOST_OWNER_PID || 0);
|
|
85
|
+
this.runtimeIdentity = options.runtimeIdentity || HOST_RUNTIME_IDENTITY;
|
|
86
|
+
this.exitOnShutdown = options.exitOnShutdown !== false;
|
|
55
87
|
this.sessions = new Map();
|
|
56
88
|
this.clients = new Set();
|
|
57
89
|
this.ownerCheckTimer = null;
|
|
58
90
|
this.idleExitTimer = null;
|
|
59
91
|
this.hasAcceptedClient = false;
|
|
92
|
+
this.activeControllerClient = null;
|
|
93
|
+
this.activeControllerIdentity = null;
|
|
94
|
+
this.sessionMutationQueues = new Map();
|
|
95
|
+
this.activeControllerMutations = new Set();
|
|
96
|
+
this.controllerRegistrationQueue = Promise.resolve();
|
|
97
|
+
this.controllerHandoff = null;
|
|
98
|
+
this.rotationPreparation = null;
|
|
99
|
+
this.clientMaxBufferedBytes = normalizePositiveInteger(
|
|
100
|
+
options.clientMaxBufferedBytes,
|
|
101
|
+
DEFAULT_CLIENT_MAX_BUFFERED_BYTES,
|
|
102
|
+
1024,
|
|
103
|
+
256 * 1024 * 1024,
|
|
104
|
+
);
|
|
105
|
+
this.clientMaxRequestBytes = normalizePositiveInteger(
|
|
106
|
+
options.clientMaxRequestBytes,
|
|
107
|
+
DEFAULT_CLIENT_MAX_REQUEST_BYTES,
|
|
108
|
+
1024,
|
|
109
|
+
256 * 1024 * 1024,
|
|
110
|
+
);
|
|
60
111
|
this.idleExitMs = normalizePositiveInteger(
|
|
61
112
|
options.idleExitMs ?? process.env.FARMING_NATIVE_PTY_HOST_IDLE_EXIT_MS,
|
|
62
113
|
DEFAULT_IDLE_EXIT_MS,
|
|
@@ -125,7 +176,7 @@ class NativePtyHost {
|
|
|
125
176
|
}
|
|
126
177
|
|
|
127
178
|
handleConnection(socket) {
|
|
128
|
-
const client = { socket, buffer: '' };
|
|
179
|
+
const client = { socket, buffer: '', disconnected: false };
|
|
129
180
|
this.hasAcceptedClient = true;
|
|
130
181
|
this.cancelIdleExit();
|
|
131
182
|
this.clients.add(client);
|
|
@@ -135,8 +186,43 @@ class NativePtyHost {
|
|
|
135
186
|
}
|
|
136
187
|
|
|
137
188
|
removeClient(client) {
|
|
189
|
+
if (!client || client.disconnected === true) return Promise.resolve();
|
|
190
|
+
client.disconnected = true;
|
|
138
191
|
this.clients.delete(client);
|
|
192
|
+
let retirement = Promise.resolve();
|
|
193
|
+
if (
|
|
194
|
+
this.activeControllerClient === client
|
|
195
|
+
|| this.controllerHandoff?.fromClient === client
|
|
196
|
+
|| this.controllerHandoff?.toClient === client
|
|
197
|
+
) {
|
|
198
|
+
retirement = this.controllerRegistrationQueue
|
|
199
|
+
.catch(() => {})
|
|
200
|
+
.then(() => this.retireControllerClient(client));
|
|
201
|
+
this.controllerRegistrationQueue = retirement;
|
|
202
|
+
}
|
|
139
203
|
this.scheduleIdleExitIfUnused();
|
|
204
|
+
return retirement;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async retireControllerClient(client) {
|
|
208
|
+
if (this.activeControllerClient !== client) return;
|
|
209
|
+
const handoff = {
|
|
210
|
+
fromClient: client,
|
|
211
|
+
fromIdentity: this.activeControllerIdentity,
|
|
212
|
+
toClient: null,
|
|
213
|
+
toIdentity: null,
|
|
214
|
+
};
|
|
215
|
+
this.controllerHandoff = handoff;
|
|
216
|
+
try {
|
|
217
|
+
// A disconnect closes admission immediately, but an operation that was
|
|
218
|
+
// already admitted owns its linearization point until it completes.
|
|
219
|
+
// Retire the server client only after that exact set has drained.
|
|
220
|
+
await Promise.allSettled([...this.activeControllerMutations]);
|
|
221
|
+
if (this.activeControllerClient !== client) return;
|
|
222
|
+
this.activeControllerClient = null;
|
|
223
|
+
} finally {
|
|
224
|
+
if (this.controllerHandoff === handoff) this.controllerHandoff = null;
|
|
225
|
+
}
|
|
140
226
|
}
|
|
141
227
|
|
|
142
228
|
hasLiveSessions() {
|
|
@@ -186,6 +272,10 @@ class NativePtyHost {
|
|
|
186
272
|
client.buffer += chunk.toString('utf8');
|
|
187
273
|
let newline = client.buffer.indexOf('\n');
|
|
188
274
|
while (newline >= 0) {
|
|
275
|
+
if (newline > this.clientMaxRequestBytes) {
|
|
276
|
+
this.disconnectSlowClient(client, 'native pty request exceeded limit');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
189
279
|
const line = client.buffer.slice(0, newline);
|
|
190
280
|
client.buffer = client.buffer.slice(newline + 1);
|
|
191
281
|
if (line.trim()) {
|
|
@@ -193,6 +283,9 @@ class NativePtyHost {
|
|
|
193
283
|
}
|
|
194
284
|
newline = client.buffer.indexOf('\n');
|
|
195
285
|
}
|
|
286
|
+
if (client.buffer.length > this.clientMaxRequestBytes) {
|
|
287
|
+
this.disconnectSlowClient(client, 'native pty request exceeded limit');
|
|
288
|
+
}
|
|
196
289
|
}
|
|
197
290
|
|
|
198
291
|
async handleClientMessage(client, line) {
|
|
@@ -207,7 +300,7 @@ class NativePtyHost {
|
|
|
207
300
|
if (!id) return;
|
|
208
301
|
|
|
209
302
|
try {
|
|
210
|
-
const result = await this.dispatch(message.method, message.params || {});
|
|
303
|
+
const result = await this.dispatch(message.method, message.params || {}, client);
|
|
211
304
|
this.send(client, { id, ok: true, result });
|
|
212
305
|
} catch (error) {
|
|
213
306
|
this.send(client, {
|
|
@@ -218,45 +311,146 @@ class NativePtyHost {
|
|
|
218
311
|
}
|
|
219
312
|
}
|
|
220
313
|
|
|
221
|
-
dispatch(method, params) {
|
|
314
|
+
dispatch(method, params, client) {
|
|
222
315
|
switch (method) {
|
|
223
316
|
case 'ping':
|
|
224
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
ok: true,
|
|
319
|
+
pid: process.pid,
|
|
320
|
+
runtimeIdentity: this.runtimeIdentity,
|
|
321
|
+
};
|
|
322
|
+
case 'registerController':
|
|
323
|
+
return this.registerController(client, params.identity || {});
|
|
225
324
|
case 'createSession':
|
|
226
|
-
return this.
|
|
325
|
+
return this.enqueueControllerMutation(
|
|
326
|
+
params.options?.agentId,
|
|
327
|
+
client,
|
|
328
|
+
() => this.createSession(params.options || {}),
|
|
329
|
+
);
|
|
227
330
|
case 'sendInput':
|
|
228
|
-
return this.
|
|
331
|
+
return this.enqueueControllerMutation(
|
|
332
|
+
params.sessionId,
|
|
333
|
+
client,
|
|
334
|
+
() => this.sendInput(params.sessionId, params.input, params.expectedRuntimeEpoch || ''),
|
|
335
|
+
);
|
|
229
336
|
case 'resizeSession':
|
|
230
|
-
return this.
|
|
337
|
+
return this.enqueueControllerMutation(
|
|
338
|
+
params.sessionId,
|
|
339
|
+
client,
|
|
340
|
+
() => this.resizeSession(params.sessionId, params.cols, params.rows),
|
|
341
|
+
);
|
|
231
342
|
case 'clearBuffer':
|
|
232
|
-
return this.
|
|
343
|
+
return this.enqueueControllerMutation(
|
|
344
|
+
params.sessionId,
|
|
345
|
+
client,
|
|
346
|
+
() => this.clearBuffer(params.sessionId, params.expectedRuntimeEpoch || ''),
|
|
347
|
+
);
|
|
233
348
|
case 'killSession':
|
|
234
|
-
return this.
|
|
349
|
+
return this.enqueueControllerMutation(
|
|
350
|
+
params.sessionId,
|
|
351
|
+
client,
|
|
352
|
+
() => this.killSession(params.sessionId),
|
|
353
|
+
);
|
|
235
354
|
case 'getSessionState':
|
|
236
355
|
return this.getSessionState(params.sessionId);
|
|
356
|
+
case 'getSessionAttachCheckpoint':
|
|
357
|
+
return this.getSessionAttachCheckpoint(params.sessionId, client);
|
|
237
358
|
case 'getSessionPreview':
|
|
238
359
|
return this.getSessionPreview(params.sessionId);
|
|
239
360
|
case 'recoverSessions':
|
|
240
361
|
return this.recoverSessions();
|
|
362
|
+
case 'serializeTerminalState':
|
|
363
|
+
return this.serializeTerminalState(client);
|
|
364
|
+
case 'resumeTerminalState':
|
|
365
|
+
return this.resumeTerminalState(client, params.preparationToken || '');
|
|
241
366
|
case 'updateSessionMetadata':
|
|
242
|
-
return this.
|
|
367
|
+
return this.enqueueControllerMutation(
|
|
368
|
+
params.sessionId,
|
|
369
|
+
client,
|
|
370
|
+
() => this.updateSessionMetadata(params.sessionId, params.patch || {}),
|
|
371
|
+
);
|
|
243
372
|
case 'shutdownHost':
|
|
244
|
-
return this.shutdownHost(
|
|
373
|
+
return this.shutdownHost(
|
|
374
|
+
client,
|
|
375
|
+
params.controller || {},
|
|
376
|
+
params.preparationToken || '',
|
|
377
|
+
);
|
|
245
378
|
default:
|
|
246
379
|
throw new Error(`Unknown native pty host method: ${method}`);
|
|
247
380
|
}
|
|
248
381
|
}
|
|
249
382
|
|
|
383
|
+
enqueueControllerMutation(sessionId, client, operation) {
|
|
384
|
+
// Admission is fenced before queueing. Once a newer controller starts its
|
|
385
|
+
// handoff, no additional work from the retiring controller may enter the
|
|
386
|
+
// mutation set; work admitted before that cut is allowed to finish.
|
|
387
|
+
try {
|
|
388
|
+
this.assertActiveController(client);
|
|
389
|
+
if (this.controllerHandoff) {
|
|
390
|
+
throw new Error('Native pty controller handoff is in progress');
|
|
391
|
+
}
|
|
392
|
+
if (this.rotationPreparation) {
|
|
393
|
+
throw new Error('Native pty host is frozen for runtime rotation');
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return Promise.reject(error);
|
|
397
|
+
}
|
|
398
|
+
const admission = Object.freeze({
|
|
399
|
+
[CONTROLLER_MUTATION_ADMISSION]: true,
|
|
400
|
+
client,
|
|
401
|
+
controllerId: client.controllerId,
|
|
402
|
+
controllerGeneration: client.controllerGeneration,
|
|
403
|
+
});
|
|
404
|
+
const key = typeof sessionId === 'string' && sessionId ? sessionId : '__host__';
|
|
405
|
+
const previous = this.sessionMutationQueues.get(key) || Promise.resolve();
|
|
406
|
+
const next = previous
|
|
407
|
+
.catch(() => {})
|
|
408
|
+
.then(() => {
|
|
409
|
+
if (this.rotationPreparation) {
|
|
410
|
+
throw new Error('Native pty host is frozen for runtime rotation');
|
|
411
|
+
}
|
|
412
|
+
return operation(admission);
|
|
413
|
+
});
|
|
414
|
+
this.activeControllerMutations.add(next);
|
|
415
|
+
this.sessionMutationQueues.set(key, next);
|
|
416
|
+
const cleanup = () => {
|
|
417
|
+
this.activeControllerMutations.delete(next);
|
|
418
|
+
if (this.sessionMutationQueues.get(key) === next) {
|
|
419
|
+
this.sessionMutationQueues.delete(key);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
next.then(cleanup, cleanup);
|
|
423
|
+
return next;
|
|
424
|
+
}
|
|
425
|
+
|
|
250
426
|
send(client, message) {
|
|
251
|
-
|
|
252
|
-
|
|
427
|
+
this.writeClientMessage(client, `${JSON.stringify(message)}\n`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
disconnectSlowClient(client, reason) {
|
|
431
|
+
if (!client || !client.socket || client.socket.destroyed) return false;
|
|
432
|
+
client.socket.destroy(new Error(reason));
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
writeClientMessage(client, message) {
|
|
437
|
+
if (!client || !client.socket || client.socket.destroyed) return false;
|
|
438
|
+
if (client.socket.writableLength > this.clientMaxBufferedBytes) {
|
|
439
|
+
this.disconnectSlowClient(client, 'native pty client backpressure');
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
const accepted = client.socket.write(message);
|
|
443
|
+
if (!accepted && client.socket.writableLength > this.clientMaxBufferedBytes) {
|
|
444
|
+
this.disconnectSlowClient(client, 'native pty client backpressure');
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
253
448
|
}
|
|
254
449
|
|
|
255
450
|
broadcast(event, payload) {
|
|
256
451
|
const message = `${JSON.stringify({ event, payload })}\n`;
|
|
257
452
|
for (const client of this.clients) {
|
|
258
|
-
|
|
259
|
-
client.socket.write(message);
|
|
453
|
+
this.writeClientMessage(client, message);
|
|
260
454
|
}
|
|
261
455
|
}
|
|
262
456
|
|
|
@@ -264,7 +458,88 @@ class NativePtyHost {
|
|
|
264
458
|
this.broadcast(event, payload);
|
|
265
459
|
}
|
|
266
460
|
|
|
461
|
+
registerController(client, rawIdentity) {
|
|
462
|
+
if (!client || client.disconnected === true) {
|
|
463
|
+
throw new Error('Native pty controller client is disconnected');
|
|
464
|
+
}
|
|
465
|
+
const identity = {
|
|
466
|
+
id: typeof rawIdentity.id === 'string' ? rawIdentity.id : '',
|
|
467
|
+
generation: Math.floor(Number(rawIdentity.generation)),
|
|
468
|
+
};
|
|
469
|
+
if (!identity.id || !Number.isFinite(identity.generation) || identity.generation <= 0) {
|
|
470
|
+
throw new Error('Invalid native pty controller identity');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const registration = this.controllerRegistrationQueue
|
|
474
|
+
.catch(() => {})
|
|
475
|
+
.then(async () => {
|
|
476
|
+
const current = this.activeControllerIdentity;
|
|
477
|
+
if (
|
|
478
|
+
current &&
|
|
479
|
+
(identity.generation < current.generation ||
|
|
480
|
+
(identity.generation === current.generation && current.id !== identity.id))
|
|
481
|
+
) {
|
|
482
|
+
throw new Error('Stale native pty controller');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const replacesController = !current
|
|
486
|
+
|| current.id !== identity.id
|
|
487
|
+
|| current.generation !== identity.generation
|
|
488
|
+
|| this.activeControllerClient !== client;
|
|
489
|
+
if (replacesController && current) {
|
|
490
|
+
const handoff = {
|
|
491
|
+
fromClient: this.activeControllerClient,
|
|
492
|
+
fromIdentity: current,
|
|
493
|
+
toClient: client,
|
|
494
|
+
toIdentity: identity,
|
|
495
|
+
};
|
|
496
|
+
this.controllerHandoff = handoff;
|
|
497
|
+
try {
|
|
498
|
+
// New admissions from the retiring controller are now closed.
|
|
499
|
+
// Drain the exact set admitted before that cut before publishing
|
|
500
|
+
// the new generation as active.
|
|
501
|
+
await Promise.allSettled([...this.activeControllerMutations]);
|
|
502
|
+
} finally {
|
|
503
|
+
if (this.controllerHandoff === handoff) this.controllerHandoff = null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (client.disconnected === true) {
|
|
508
|
+
throw new Error('Native pty controller client is disconnected');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
this.activeControllerIdentity = identity;
|
|
512
|
+
this.activeControllerClient = client;
|
|
513
|
+
client.controllerId = identity.id;
|
|
514
|
+
client.controllerGeneration = identity.generation;
|
|
515
|
+
return {
|
|
516
|
+
registered: true,
|
|
517
|
+
controllerId: identity.id,
|
|
518
|
+
controllerGeneration: identity.generation,
|
|
519
|
+
};
|
|
520
|
+
});
|
|
521
|
+
this.controllerRegistrationQueue = registration;
|
|
522
|
+
return registration;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
assertActiveController(client) {
|
|
526
|
+
if (client?.[CONTROLLER_MUTATION_ADMISSION] === true) return;
|
|
527
|
+
if (
|
|
528
|
+
!client ||
|
|
529
|
+
client.disconnected === true ||
|
|
530
|
+
client !== this.activeControllerClient ||
|
|
531
|
+
!this.activeControllerIdentity ||
|
|
532
|
+
client.controllerId !== this.activeControllerIdentity.id ||
|
|
533
|
+
client.controllerGeneration !== this.activeControllerIdentity.generation
|
|
534
|
+
) {
|
|
535
|
+
throw new Error('Native pty mutation requires the active controller');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
267
539
|
async createSession(options) {
|
|
540
|
+
if (this.rotationPreparation) {
|
|
541
|
+
throw new Error('Native pty host is frozen for runtime rotation');
|
|
542
|
+
}
|
|
268
543
|
this.cancelIdleExit();
|
|
269
544
|
const normalized = options.shellIntegrationPrepared === true
|
|
270
545
|
? {
|
|
@@ -286,9 +561,31 @@ class NativePtyHost {
|
|
|
286
561
|
return { sessionId: agentId, status: existing.status };
|
|
287
562
|
}
|
|
288
563
|
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
564
|
+
const reviveState = options.reviveState
|
|
565
|
+
? normalizeTerminalStateEntry(options.reviveState)
|
|
566
|
+
: null;
|
|
567
|
+
const reviveEvent = reviveState?.replayEvent?.events?.[0] || null;
|
|
568
|
+
const cols = reviveEvent?.cols || normalized.cols || DEFAULT_COLS;
|
|
569
|
+
const rows = reviveEvent?.rows || normalized.rows || DEFAULT_ROWS;
|
|
570
|
+
const runtimeGeneration = await allocateNativePtyRuntimeGeneration(this.configDir);
|
|
571
|
+
const runtimeEpoch = formatNativePtyRuntimeEpoch(runtimeGeneration);
|
|
572
|
+
const screenWorker = await this.screenWorkerPool.acquire({ cols, rows, runtimeEpoch });
|
|
573
|
+
const replayText = reviveEvent?.data || '';
|
|
574
|
+
const restoredOutput = reviveState
|
|
575
|
+
? `${replayText}${TERMINAL_HISTORY_RESTORED_MESSAGE}`
|
|
576
|
+
: '';
|
|
577
|
+
let restoredScreenState = null;
|
|
578
|
+
if (restoredOutput) {
|
|
579
|
+
try {
|
|
580
|
+
await screenWorker.append(restoredOutput, 1, 1);
|
|
581
|
+
restoredScreenState = await screenWorker.getState();
|
|
582
|
+
} catch (error) {
|
|
583
|
+
await screenWorker.dispose().catch(() => {});
|
|
584
|
+
throw new Error(`Failed to restore serialized terminal history: ${error.message}`, {
|
|
585
|
+
cause: error,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
292
589
|
const metadata = {
|
|
293
590
|
...(normalized.metadata || {}),
|
|
294
591
|
protocolVersion: 1,
|
|
@@ -321,14 +618,20 @@ class NativePtyHost {
|
|
|
321
618
|
cwd: normalized.cwd,
|
|
322
619
|
metadata,
|
|
323
620
|
process: ptyProcess,
|
|
324
|
-
output:
|
|
325
|
-
outputSeq: 0,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
621
|
+
output: trimOutput(restoredOutput),
|
|
622
|
+
outputSeq: restoredOutput ? 1 : 0,
|
|
623
|
+
stateRevision: restoredOutput ? 1 : 0,
|
|
624
|
+
runtimeEpoch,
|
|
625
|
+
runtimeGeneration,
|
|
626
|
+
stateProofAvailable: true,
|
|
627
|
+
reducerFlowControl: createTerminalReducerFlowControl(),
|
|
628
|
+
reducerCommitQueue: Promise.resolve(),
|
|
629
|
+
renderOutput: restoredScreenState?.renderOutput || restoredOutput,
|
|
630
|
+
previewText: restoredScreenState?.previewText || restoredOutput,
|
|
631
|
+
previewSnapshot: restoredScreenState?.previewSnapshot || null,
|
|
632
|
+
previewCols: restoredScreenState?.cols || cols,
|
|
633
|
+
previewRows: restoredScreenState?.rows || rows,
|
|
634
|
+
title: restoredScreenState?.title || '',
|
|
332
635
|
status: 'running',
|
|
333
636
|
terminalBusy: null,
|
|
334
637
|
shellCwd: '',
|
|
@@ -355,7 +658,24 @@ class NativePtyHost {
|
|
|
355
658
|
sessionId: agentId,
|
|
356
659
|
status: session.status,
|
|
357
660
|
startedAt: session.startedAt,
|
|
661
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
662
|
+
outputSeq: session.outputSeq,
|
|
663
|
+
stateRevision: session.stateRevision,
|
|
358
664
|
});
|
|
665
|
+
if (restoredOutput) {
|
|
666
|
+
this.emitSessionEvent('session-sync', {
|
|
667
|
+
sessionId: agentId,
|
|
668
|
+
output: session.renderOutput,
|
|
669
|
+
textOutput: session.output,
|
|
670
|
+
replaceLive: true,
|
|
671
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
672
|
+
outputSeq: session.outputSeq,
|
|
673
|
+
stateRevision: session.stateRevision,
|
|
674
|
+
cols: session.previewCols,
|
|
675
|
+
rows: session.previewRows,
|
|
676
|
+
revived: true,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
359
679
|
|
|
360
680
|
return { sessionId: agentId, status: session.status };
|
|
361
681
|
}
|
|
@@ -363,7 +683,7 @@ class NativePtyHost {
|
|
|
363
683
|
bindScreenWorker(session) {
|
|
364
684
|
session.screenWorker.on('preview', ({ previewText, title, cols, rows, previewSnapshot }) => {
|
|
365
685
|
const current = this.sessions.get(session.id);
|
|
366
|
-
if (
|
|
686
|
+
if (current !== session) return;
|
|
367
687
|
|
|
368
688
|
current.previewText = previewText || '';
|
|
369
689
|
current.previewSnapshot = previewSnapshot || null;
|
|
@@ -375,6 +695,7 @@ class NativePtyHost {
|
|
|
375
695
|
this.emitSessionEvent('session-title', {
|
|
376
696
|
sessionId: current.id,
|
|
377
697
|
title: current.title,
|
|
698
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
378
699
|
});
|
|
379
700
|
}
|
|
380
701
|
|
|
@@ -384,34 +705,60 @@ class NativePtyHost {
|
|
|
384
705
|
cols: current.previewCols,
|
|
385
706
|
rows: current.previewRows,
|
|
386
707
|
previewSnapshot: current.previewSnapshot,
|
|
708
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
387
709
|
});
|
|
388
710
|
});
|
|
389
711
|
|
|
390
712
|
session.screenWorker.on('error', error => {
|
|
391
|
-
this.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
713
|
+
this.failSessionScreenState(session, error);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
failSessionScreenState(session, error) {
|
|
718
|
+
const current = this.sessions.get(session.id);
|
|
719
|
+
if (current !== session || current.stateProofAvailable === false) return;
|
|
720
|
+
current.stateProofAvailable = false;
|
|
721
|
+
const message = error instanceof Error ? error.message : String(error || 'unknown reducer failure');
|
|
722
|
+
this.emitSessionEvent('session-error', {
|
|
723
|
+
sessionId: current.id,
|
|
724
|
+
error: `Terminal state reducer failed: ${message}`,
|
|
725
|
+
fatal: true,
|
|
726
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
396
727
|
});
|
|
728
|
+
try {
|
|
729
|
+
current.process?.kill();
|
|
730
|
+
} catch {
|
|
731
|
+
// The session is already unusable because its authoritative reducer failed.
|
|
732
|
+
}
|
|
397
733
|
}
|
|
398
734
|
|
|
399
735
|
bindPty(session) {
|
|
400
|
-
session.process.onData(data => this.handleSessionData(session.id, data));
|
|
736
|
+
session.process.onData(data => this.handleSessionData(session.id, data, session));
|
|
401
737
|
session.process.onExit(({ code }) => {
|
|
402
|
-
this.handleSessionExit(session.id, code).catch(error => {
|
|
738
|
+
this.handleSessionExit(session.id, code, session).catch(error => {
|
|
739
|
+
if (this.sessions.get(session.id) !== session) return;
|
|
403
740
|
this.emitSessionEvent('session-error', {
|
|
404
741
|
sessionId: session.id,
|
|
405
742
|
error: error.message,
|
|
406
743
|
fatal: false,
|
|
744
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
407
745
|
});
|
|
408
746
|
});
|
|
409
747
|
});
|
|
410
748
|
}
|
|
411
749
|
|
|
412
|
-
handleSessionData(sessionId, rawData) {
|
|
750
|
+
handleSessionData(sessionId, rawData, expectedSession = null) {
|
|
413
751
|
const current = this.sessions.get(sessionId);
|
|
414
|
-
if (
|
|
752
|
+
if (
|
|
753
|
+
!current ||
|
|
754
|
+
(expectedSession && current !== expectedSession) ||
|
|
755
|
+
current.stateProofAvailable === false
|
|
756
|
+
) return;
|
|
757
|
+
if (!acceptTerminalExitData(current)) {
|
|
758
|
+
current.finalCheckpoint = null;
|
|
759
|
+
this.failSessionScreenState(current, new Error('PTY emitted data after its final checkpoint cut'));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
415
762
|
|
|
416
763
|
const busyState = parseShellBusyMarkers(rawData, current.terminalBusy, current.shellBusyMarkerPending);
|
|
417
764
|
current.shellBusyMarkerPending = busyState.pending;
|
|
@@ -460,13 +807,26 @@ class NativePtyHost {
|
|
|
460
807
|
shellLastCommandDurationMs: current.shellLastCommandDurationMs,
|
|
461
808
|
statusMarkerSeen: busyState.statusMarkerSeen,
|
|
462
809
|
busyMarkerSeen: busyState.busyMarkerSeen,
|
|
810
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
463
811
|
});
|
|
464
812
|
}
|
|
465
813
|
|
|
466
814
|
const data = busyState.data;
|
|
467
815
|
if (!data) return;
|
|
468
816
|
|
|
817
|
+
const reducerDelivery = enqueueTerminalReducerData(
|
|
818
|
+
ensureTerminalReducerFlowControl(current),
|
|
819
|
+
current.process,
|
|
820
|
+
data,
|
|
821
|
+
);
|
|
822
|
+
if (reducerDelivery.error) {
|
|
823
|
+
this.failSessionScreenState(current, reducerDelivery.error);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
469
826
|
current.outputSeq += 1;
|
|
827
|
+
current.stateRevision += 1;
|
|
828
|
+
const outputSeq = current.outputSeq;
|
|
829
|
+
const stateRevision = current.stateRevision;
|
|
470
830
|
current.output = trimOutput(current.output + data);
|
|
471
831
|
current.lastActivityAt = Date.now();
|
|
472
832
|
|
|
@@ -476,46 +836,85 @@ class NativePtyHost {
|
|
|
476
836
|
this.emitSessionEvent('session-title', {
|
|
477
837
|
sessionId,
|
|
478
838
|
title: current.title,
|
|
839
|
+
runtimeEpoch: current.runtimeEpoch,
|
|
479
840
|
});
|
|
480
841
|
}
|
|
481
842
|
|
|
482
|
-
this.emitSessionEvent('session-output', {
|
|
483
|
-
sessionId,
|
|
484
|
-
data,
|
|
485
|
-
outputSeq: current.outputSeq,
|
|
486
|
-
});
|
|
487
|
-
this.emitSessionEvent('session-activity', {
|
|
488
|
-
sessionId,
|
|
489
|
-
lastActivityAt: current.lastActivityAt,
|
|
490
|
-
});
|
|
491
|
-
|
|
492
843
|
if (current.screenWorker) {
|
|
493
|
-
current.screenWorker.append(data);
|
|
844
|
+
const commit = current.screenWorker.append(data, stateRevision, outputSeq);
|
|
845
|
+
current.reducerCommitQueue = current.reducerCommitQueue.then(() => commit).then(() => {
|
|
846
|
+
const latest = this.sessions.get(sessionId);
|
|
847
|
+
if (latest !== current || latest.stateProofAvailable === false) return;
|
|
848
|
+
const flowError = acknowledgeTerminalReducerData(
|
|
849
|
+
ensureTerminalReducerFlowControl(latest),
|
|
850
|
+
latest.process,
|
|
851
|
+
reducerDelivery.bytes,
|
|
852
|
+
);
|
|
853
|
+
if (flowError) {
|
|
854
|
+
this.failSessionScreenState(latest, flowError);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
this.emitSessionEvent('session-output', {
|
|
858
|
+
sessionId,
|
|
859
|
+
data,
|
|
860
|
+
runtimeEpoch: latest.runtimeEpoch,
|
|
861
|
+
outputSeq,
|
|
862
|
+
stateRevision,
|
|
863
|
+
});
|
|
864
|
+
this.emitSessionEvent('session-activity', {
|
|
865
|
+
sessionId,
|
|
866
|
+
lastActivityAt: latest.lastActivityAt,
|
|
867
|
+
runtimeEpoch: latest.runtimeEpoch,
|
|
868
|
+
});
|
|
869
|
+
}).catch(error => this.failSessionScreenState(current, error));
|
|
494
870
|
}
|
|
495
871
|
}
|
|
496
872
|
|
|
497
|
-
|
|
873
|
+
handleSessionExit(sessionId, code, expectedSession = null) {
|
|
498
874
|
const session = this.sessions.get(sessionId);
|
|
499
|
-
if (
|
|
875
|
+
if (
|
|
876
|
+
!session ||
|
|
877
|
+
(expectedSession && session !== expectedSession) ||
|
|
878
|
+
session.status === 'exited'
|
|
879
|
+
) return Promise.resolve();
|
|
880
|
+
if (session.exitFinalizationPromise) return session.exitFinalizationPromise;
|
|
881
|
+
const finalization = this.finalizeSessionExit(sessionId, code, session);
|
|
882
|
+
session.exitFinalizationPromise = finalization;
|
|
883
|
+
return finalization;
|
|
884
|
+
}
|
|
500
885
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
886
|
+
async finalizeSessionExit(sessionId, code, session) {
|
|
887
|
+
const quiesced = await waitForTerminalExitDataQuiescence(session, {
|
|
888
|
+
flushMs: this.terminalExitDataFlushMs,
|
|
889
|
+
isCurrent: () => this.sessions.get(sessionId) === session,
|
|
890
|
+
});
|
|
891
|
+
if (!quiesced) return;
|
|
892
|
+
|
|
893
|
+
await Promise.resolve(session.reducerCommitQueue).catch(() => {});
|
|
894
|
+
if (this.sessions.get(sessionId) !== session) return;
|
|
895
|
+
const finalCheckpoint = await captureTerminalAttachCheckpoint(session, { requireCurrentCut: true });
|
|
896
|
+
if (this.sessions.get(sessionId) !== session) return;
|
|
897
|
+
if (finalCheckpoint) {
|
|
898
|
+
session.finalCheckpoint = Object.freeze({ ...finalCheckpoint });
|
|
899
|
+
session.renderOutput = finalCheckpoint.renderOutput;
|
|
900
|
+
session.previewText = finalCheckpoint.previewText;
|
|
901
|
+
session.previewSnapshot = finalCheckpoint.previewSnapshot;
|
|
902
|
+
session.previewCols = finalCheckpoint.cols;
|
|
903
|
+
session.previewRows = finalCheckpoint.rows;
|
|
904
|
+
session.title = finalCheckpoint.title || session.title;
|
|
905
|
+
} else {
|
|
906
|
+
this.failSessionScreenState(session, new Error('Unable to capture the exact final Terminal checkpoint'));
|
|
512
907
|
}
|
|
513
908
|
|
|
514
909
|
session.status = 'exited';
|
|
515
910
|
session.exitedAt = Date.now();
|
|
911
|
+
resetTerminalReducerFlowControl(
|
|
912
|
+
ensureTerminalReducerFlowControl(session),
|
|
913
|
+
session.process,
|
|
914
|
+
);
|
|
516
915
|
cleanupShellBusyIntegration(session.shellBusyIntegration);
|
|
517
916
|
if (session.screenWorker) {
|
|
518
|
-
session.screenWorker.dispose().catch(() => {});
|
|
917
|
+
await session.screenWorker.dispose().catch(() => {});
|
|
519
918
|
session.screenWorker = null;
|
|
520
919
|
}
|
|
521
920
|
|
|
@@ -523,107 +922,226 @@ class NativePtyHost {
|
|
|
523
922
|
sessionId,
|
|
524
923
|
code: code == null ? 'unknown' : code,
|
|
525
924
|
exitedAt: session.exitedAt,
|
|
925
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
926
|
+
outputSeq: session.outputSeq,
|
|
927
|
+
stateRevision: session.stateRevision,
|
|
928
|
+
stateProofAvailable: session.stateProofAvailable !== false,
|
|
526
929
|
});
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
930
|
+
if (finalCheckpoint) {
|
|
931
|
+
this.emitSessionEvent('session-preview', {
|
|
932
|
+
sessionId,
|
|
933
|
+
previewText: session.previewText,
|
|
934
|
+
cols: session.previewCols,
|
|
935
|
+
rows: session.previewRows,
|
|
936
|
+
previewSnapshot: session.previewSnapshot,
|
|
937
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
534
940
|
this.scheduleIdleExitIfUnused();
|
|
535
941
|
}
|
|
536
942
|
|
|
537
|
-
async sendInput(sessionId, input) {
|
|
943
|
+
async sendInput(sessionId, input, expectedRuntimeEpoch = '') {
|
|
538
944
|
const session = this.sessions.get(sessionId);
|
|
539
|
-
if (!session || !session.process || session.status === 'exited') {
|
|
945
|
+
if (!session || !session.process || session.status === 'exited' || session.exitFinalizing === true) {
|
|
540
946
|
throw new Error('Session not available');
|
|
541
947
|
}
|
|
948
|
+
if (session.rotationFrozen === true) {
|
|
949
|
+
throw new Error('Terminal session is frozen for native PTY host rotation');
|
|
950
|
+
}
|
|
951
|
+
if (expectedRuntimeEpoch && expectedRuntimeEpoch !== session.runtimeEpoch) {
|
|
952
|
+
return { status: 'input-rejected', reason: 'runtime-epoch-mismatch' };
|
|
953
|
+
}
|
|
542
954
|
session.process.write(terminalInputToPtyString(input));
|
|
543
955
|
session.lastActivityAt = Date.now();
|
|
544
956
|
this.emitSessionEvent('session-activity', {
|
|
545
957
|
sessionId,
|
|
546
958
|
lastActivityAt: session.lastActivityAt,
|
|
959
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
547
960
|
});
|
|
548
961
|
return { sent: true };
|
|
549
962
|
}
|
|
550
963
|
|
|
964
|
+
async getSessionAttachCheckpoint(sessionId, client) {
|
|
965
|
+
this.assertActiveController(client);
|
|
966
|
+
const session = this.sessions.get(sessionId);
|
|
967
|
+
if (!session) return null;
|
|
968
|
+
const checkpoint = await captureTerminalAttachCheckpoint(session);
|
|
969
|
+
return this.sessions.get(sessionId) === session ? checkpoint : null;
|
|
970
|
+
}
|
|
971
|
+
|
|
551
972
|
async resizeSession(sessionId, cols, rows) {
|
|
552
973
|
const session = this.sessions.get(sessionId);
|
|
553
|
-
if (!session || !session.process || session.status === 'exited') {
|
|
554
|
-
return { resized: false };
|
|
974
|
+
if (!session || !session.process || session.status === 'exited' || session.exitFinalizing === true) {
|
|
975
|
+
return { status: 'resize-rejected', reason: 'session-unavailable', resized: false };
|
|
976
|
+
}
|
|
977
|
+
if (session.rotationFrozen === true) {
|
|
978
|
+
return { status: 'resize-rejected', reason: 'runtime-rotation', resized: false };
|
|
555
979
|
}
|
|
556
980
|
|
|
557
981
|
const nextCols = normalizePositiveInteger(cols, session.previewCols || DEFAULT_COLS, 1, 1000);
|
|
558
982
|
const nextRows = normalizePositiveInteger(rows, session.previewRows || DEFAULT_ROWS, 1, 1000);
|
|
559
|
-
|
|
983
|
+
if (nextCols === session.previewCols && nextRows === session.previewRows) {
|
|
984
|
+
return {
|
|
985
|
+
status: 'resize-committed',
|
|
986
|
+
resized: true,
|
|
987
|
+
unchanged: true,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
session.process.resize(nextCols, nextRows);
|
|
993
|
+
} catch (error) {
|
|
994
|
+
return {
|
|
995
|
+
status: 'resize-rejected',
|
|
996
|
+
reason: 'pty-resize-failed',
|
|
997
|
+
resized: false,
|
|
998
|
+
error: error instanceof Error ? error.message : String(error || 'unknown PTY resize failure'),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
session.stateRevision += 1;
|
|
1002
|
+
const stateRevision = session.stateRevision;
|
|
1003
|
+
const outputSeq = session.outputSeq;
|
|
1004
|
+
const runtimeEpoch = session.runtimeEpoch;
|
|
560
1005
|
session.previewCols = nextCols;
|
|
561
1006
|
session.previewRows = nextRows;
|
|
562
1007
|
|
|
563
1008
|
if (session.screenWorker) {
|
|
564
|
-
|
|
565
|
-
|
|
1009
|
+
const reducerCommit = session.screenWorker.resize(nextCols, nextRows, stateRevision);
|
|
1010
|
+
const publishedCommit = session.reducerCommitQueue.then(() => reducerCommit).then((screenState) => {
|
|
1011
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
1012
|
+
throw new Error('Terminal session was replaced during resize');
|
|
1013
|
+
}
|
|
1014
|
+
if (
|
|
1015
|
+
screenState.runtimeEpoch !== runtimeEpoch ||
|
|
1016
|
+
screenState.outputSeq !== outputSeq ||
|
|
1017
|
+
screenState.stateRevision !== stateRevision
|
|
1018
|
+
) {
|
|
1019
|
+
throw new Error('Terminal screen resize returned a non-authoritative state cut');
|
|
1020
|
+
}
|
|
566
1021
|
session.previewText = screenState.previewText || '';
|
|
567
1022
|
session.previewSnapshot = screenState.previewSnapshot || session.previewSnapshot;
|
|
568
|
-
session.renderOutput = screenState.renderOutput
|
|
1023
|
+
session.renderOutput = typeof screenState.renderOutput === 'string'
|
|
1024
|
+
? screenState.renderOutput
|
|
1025
|
+
: session.renderOutput;
|
|
569
1026
|
session.previewCols = screenState.cols || nextCols;
|
|
570
1027
|
session.previewRows = screenState.rows || nextRows;
|
|
571
1028
|
session.title = screenState.title || session.title;
|
|
572
|
-
|
|
573
|
-
this.emitSessionEvent('session-error', {
|
|
1029
|
+
this.emitSessionEvent('session-transition', {
|
|
574
1030
|
sessionId,
|
|
575
|
-
|
|
576
|
-
|
|
1031
|
+
kind: 'resize',
|
|
1032
|
+
data: '',
|
|
1033
|
+
runtimeEpoch: screenState.runtimeEpoch,
|
|
1034
|
+
outputSeq: screenState.outputSeq,
|
|
1035
|
+
stateRevision: screenState.stateRevision,
|
|
1036
|
+
cols: screenState.cols,
|
|
1037
|
+
rows: screenState.rows,
|
|
577
1038
|
});
|
|
1039
|
+
return screenState;
|
|
1040
|
+
});
|
|
1041
|
+
session.reducerCommitQueue = publishedCommit.catch(error => {
|
|
1042
|
+
this.failSessionScreenState(session, error);
|
|
1043
|
+
});
|
|
1044
|
+
try {
|
|
1045
|
+
await publishedCommit;
|
|
1046
|
+
} catch {
|
|
1047
|
+
return { status: 'resize-rejected', reason: 'screen-reducer-failed', resized: false };
|
|
578
1048
|
}
|
|
579
1049
|
}
|
|
1050
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
1051
|
+
return { status: 'resize-rejected', reason: 'session-replaced', resized: false };
|
|
1052
|
+
}
|
|
580
1053
|
|
|
581
|
-
return {
|
|
1054
|
+
return {
|
|
1055
|
+
status: 'resize-committed',
|
|
1056
|
+
resized: true,
|
|
1057
|
+
unchanged: false,
|
|
1058
|
+
runtimeEpoch,
|
|
1059
|
+
outputSeq,
|
|
1060
|
+
stateRevision,
|
|
1061
|
+
cols: nextCols,
|
|
1062
|
+
rows: nextRows,
|
|
1063
|
+
};
|
|
582
1064
|
}
|
|
583
1065
|
|
|
584
|
-
async clearBuffer(sessionId) {
|
|
1066
|
+
async clearBuffer(sessionId, expectedRuntimeEpoch = '') {
|
|
585
1067
|
const session = this.sessions.get(sessionId);
|
|
586
|
-
if (!session) {
|
|
1068
|
+
if (!session || session.status === 'exited' || session.exitFinalizing === true) {
|
|
587
1069
|
return { cleared: false };
|
|
588
1070
|
}
|
|
1071
|
+
if (session.rotationFrozen === true) {
|
|
1072
|
+
return { cleared: false, reason: 'runtime-rotation' };
|
|
1073
|
+
}
|
|
1074
|
+
if (expectedRuntimeEpoch && expectedRuntimeEpoch !== session.runtimeEpoch) {
|
|
1075
|
+
return { cleared: false, reason: 'runtime-epoch-mismatch' };
|
|
1076
|
+
}
|
|
589
1077
|
|
|
590
1078
|
session.output = '';
|
|
591
|
-
session.
|
|
1079
|
+
session.stateRevision += 1;
|
|
1080
|
+
const stateRevision = session.stateRevision;
|
|
1081
|
+
const outputSeq = session.outputSeq;
|
|
1082
|
+
const runtimeEpoch = session.runtimeEpoch;
|
|
592
1083
|
session.renderOutput = '';
|
|
593
1084
|
session.previewText = '';
|
|
594
1085
|
session.previewSnapshot = null;
|
|
595
1086
|
session.lastActivityAt = Date.now();
|
|
596
1087
|
|
|
1088
|
+
let exactState = {
|
|
1089
|
+
renderOutput: '',
|
|
1090
|
+
runtimeEpoch,
|
|
1091
|
+
outputSeq,
|
|
1092
|
+
stateRevision,
|
|
1093
|
+
cols: session.previewCols,
|
|
1094
|
+
rows: session.previewRows,
|
|
1095
|
+
};
|
|
597
1096
|
if (session.screenWorker) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1097
|
+
const reducerCommit = session.screenWorker.clear(stateRevision, outputSeq);
|
|
1098
|
+
const publishedCommit = session.reducerCommitQueue.then(() => reducerCommit).then((screenState) => {
|
|
1099
|
+
if (this.sessions.get(sessionId) !== session) {
|
|
1100
|
+
throw new Error('Terminal session was replaced during clear');
|
|
1101
|
+
}
|
|
1102
|
+
if (
|
|
1103
|
+
screenState.runtimeEpoch !== runtimeEpoch ||
|
|
1104
|
+
screenState.outputSeq !== outputSeq ||
|
|
1105
|
+
screenState.stateRevision !== stateRevision
|
|
1106
|
+
) {
|
|
1107
|
+
throw new Error('Terminal screen clear returned a non-authoritative state cut');
|
|
1108
|
+
}
|
|
1109
|
+
session.renderOutput = typeof screenState.renderOutput === 'string' ? screenState.renderOutput : '';
|
|
601
1110
|
session.previewText = screenState.previewText || '';
|
|
602
1111
|
session.previewSnapshot = screenState.previewSnapshot || null;
|
|
603
1112
|
session.previewCols = screenState.cols || session.previewCols;
|
|
604
1113
|
session.previewRows = screenState.rows || session.previewRows;
|
|
1114
|
+
exactState = screenState;
|
|
605
1115
|
if (screenState.title && screenState.title !== session.title) {
|
|
606
1116
|
session.title = screenState.title;
|
|
607
1117
|
this.emitSessionEvent('session-title', {
|
|
608
1118
|
sessionId,
|
|
609
1119
|
title: session.title,
|
|
1120
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
610
1121
|
});
|
|
611
1122
|
}
|
|
612
|
-
|
|
613
|
-
this.emitSessionEvent('session-error', {
|
|
1123
|
+
this.emitSessionEvent('session-transition', {
|
|
614
1124
|
sessionId,
|
|
615
|
-
|
|
616
|
-
|
|
1125
|
+
kind: 'clear',
|
|
1126
|
+
data: '\x1b[2J\x1b[3J\x1b[H',
|
|
1127
|
+
runtimeEpoch: screenState.runtimeEpoch,
|
|
1128
|
+
outputSeq: screenState.outputSeq,
|
|
1129
|
+
stateRevision: screenState.stateRevision,
|
|
1130
|
+
cols: screenState.cols,
|
|
1131
|
+
rows: screenState.rows,
|
|
617
1132
|
});
|
|
1133
|
+
return screenState;
|
|
1134
|
+
});
|
|
1135
|
+
session.reducerCommitQueue = publishedCommit.catch(error => {
|
|
1136
|
+
this.failSessionScreenState(session, error);
|
|
1137
|
+
});
|
|
1138
|
+
try {
|
|
1139
|
+
await publishedCommit;
|
|
1140
|
+
} catch {
|
|
1141
|
+
return { cleared: false };
|
|
618
1142
|
}
|
|
619
1143
|
}
|
|
620
|
-
|
|
621
|
-
this.emitSessionEvent('session-sync', {
|
|
622
|
-
sessionId,
|
|
623
|
-
output: session.renderOutput || session.output,
|
|
624
|
-
outputSeq: session.outputSeq,
|
|
625
|
-
replaceLive: true,
|
|
626
|
-
});
|
|
1144
|
+
if (this.sessions.get(sessionId) !== session) return { cleared: false, reason: 'session-replaced' };
|
|
627
1145
|
this.emitSessionEvent('session-preview', {
|
|
628
1146
|
sessionId,
|
|
629
1147
|
previewText: session.previewText,
|
|
@@ -631,12 +1149,21 @@ class NativePtyHost {
|
|
|
631
1149
|
rows: session.previewRows,
|
|
632
1150
|
previewSnapshot: session.previewSnapshot,
|
|
633
1151
|
title: session.title,
|
|
1152
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
634
1153
|
});
|
|
635
1154
|
this.emitSessionEvent('session-activity', {
|
|
636
1155
|
sessionId,
|
|
637
1156
|
lastActivityAt: session.lastActivityAt,
|
|
1157
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
638
1158
|
});
|
|
639
|
-
return {
|
|
1159
|
+
return {
|
|
1160
|
+
cleared: true,
|
|
1161
|
+
runtimeEpoch: exactState.runtimeEpoch,
|
|
1162
|
+
outputSeq: exactState.outputSeq,
|
|
1163
|
+
stateRevision: exactState.stateRevision,
|
|
1164
|
+
cols: exactState.cols,
|
|
1165
|
+
rows: exactState.rows,
|
|
1166
|
+
};
|
|
640
1167
|
}
|
|
641
1168
|
|
|
642
1169
|
async killSession(sessionId) {
|
|
@@ -648,7 +1175,7 @@ class NativePtyHost {
|
|
|
648
1175
|
session.process.kill('SIGTERM');
|
|
649
1176
|
const timer = setTimeout(() => {
|
|
650
1177
|
const latest = this.sessions.get(sessionId);
|
|
651
|
-
if (
|
|
1178
|
+
if (latest !== session || latest.status === 'exited' || !latest.process) return;
|
|
652
1179
|
latest.process.kill('SIGKILL');
|
|
653
1180
|
}, 1500);
|
|
654
1181
|
if (typeof timer.unref === 'function') timer.unref();
|
|
@@ -658,23 +1185,34 @@ class NativePtyHost {
|
|
|
658
1185
|
async getSessionState(sessionId) {
|
|
659
1186
|
const session = this.sessions.get(sessionId);
|
|
660
1187
|
if (!session) return null;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
const
|
|
1188
|
+
// Cut the raw output before the async worker request. The screen worker
|
|
1189
|
+
// receives every append with its output sequence and reports the exact
|
|
1190
|
+
// sequence represented by its serialized terminal state.
|
|
1191
|
+
const snapshotOutput = session.output;
|
|
1192
|
+
const fallbackPreviewText = session.previewText || snapshotOutput.slice(-2000);
|
|
1193
|
+
const fallbackPreviewSnapshot = session.previewSnapshot;
|
|
1194
|
+
const fallbackPreviewCols = session.previewCols;
|
|
1195
|
+
const fallbackPreviewRows = session.previewRows;
|
|
1196
|
+
const fallbackTitle = session.title;
|
|
1197
|
+
const stateProofAvailable = session.stateProofAvailable !== false;
|
|
1198
|
+
const checkpoint = stateProofAvailable ? await captureTerminalAttachCheckpoint(session) : null;
|
|
1199
|
+
if (this.sessions.get(sessionId) !== session) return null;
|
|
1200
|
+
const title = checkpoint ? checkpoint.title : fallbackTitle;
|
|
1201
|
+
const previewText = checkpoint ? checkpoint.previewText : (stateProofAvailable ? fallbackPreviewText : '');
|
|
667
1202
|
|
|
668
1203
|
return {
|
|
669
1204
|
sessionId: session.id,
|
|
670
1205
|
status: session.status,
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1206
|
+
runtimeEpoch: session.runtimeEpoch,
|
|
1207
|
+
output: snapshotOutput,
|
|
1208
|
+
outputSeq: checkpoint?.outputSeq ?? null,
|
|
1209
|
+
stateRevision: checkpoint?.stateRevision ?? null,
|
|
1210
|
+
stateProofAvailable,
|
|
1211
|
+
renderOutput: checkpoint ? checkpoint.renderOutput : (stateProofAvailable ? snapshotOutput : ''),
|
|
674
1212
|
previewText,
|
|
675
|
-
previewSnapshot:
|
|
676
|
-
previewCols:
|
|
677
|
-
previewRows:
|
|
1213
|
+
previewSnapshot: checkpoint ? checkpoint.previewSnapshot : (stateProofAvailable ? fallbackPreviewSnapshot : null),
|
|
1214
|
+
previewCols: checkpoint ? checkpoint.cols : fallbackPreviewCols,
|
|
1215
|
+
previewRows: checkpoint ? checkpoint.rows : fallbackPreviewRows,
|
|
678
1216
|
title,
|
|
679
1217
|
lastActivityAt: session.lastActivityAt,
|
|
680
1218
|
startedAt: session.startedAt,
|
|
@@ -723,6 +1261,137 @@ class NativePtyHost {
|
|
|
723
1261
|
return recovered;
|
|
724
1262
|
}
|
|
725
1263
|
|
|
1264
|
+
serializeTerminalState(client = null) {
|
|
1265
|
+
if (client) this.assertActiveController(client);
|
|
1266
|
+
if (this.rotationPreparation) {
|
|
1267
|
+
if (
|
|
1268
|
+
client &&
|
|
1269
|
+
this.rotationPreparation.controllerClient &&
|
|
1270
|
+
this.rotationPreparation.controllerClient !== client
|
|
1271
|
+
) {
|
|
1272
|
+
throw new Error('Native pty host rotation is owned by another controller');
|
|
1273
|
+
}
|
|
1274
|
+
return this.rotationPreparation.promise;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const preparation = {
|
|
1278
|
+
token: crypto.randomUUID(),
|
|
1279
|
+
controllerClient: client,
|
|
1280
|
+
phase: 'preparing',
|
|
1281
|
+
promise: null,
|
|
1282
|
+
serializedTerminalState: '',
|
|
1283
|
+
};
|
|
1284
|
+
this.rotationPreparation = preparation;
|
|
1285
|
+
preparation.promise = this.prepareSerializedTerminalState(preparation);
|
|
1286
|
+
return preparation.promise;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async prepareSerializedTerminalState(preparation) {
|
|
1290
|
+
const entries = [];
|
|
1291
|
+
try {
|
|
1292
|
+
await Promise.allSettled([...this.activeControllerMutations]);
|
|
1293
|
+
if (this.rotationPreparation !== preparation) {
|
|
1294
|
+
throw new Error('Native pty host rotation preparation was cancelled');
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const liveSessions = [...this.sessions.values()]
|
|
1298
|
+
.filter(session => session.status === 'running' && session.stateProofAvailable !== false);
|
|
1299
|
+
for (const session of liveSessions) {
|
|
1300
|
+
session.rotationFrozen = true;
|
|
1301
|
+
const flowError = setTerminalExternalFlowControlBlocked(
|
|
1302
|
+
session.reducerFlowControl,
|
|
1303
|
+
session.process,
|
|
1304
|
+
true,
|
|
1305
|
+
);
|
|
1306
|
+
if (flowError) throw flowError;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
for (const session of liveSessions) {
|
|
1310
|
+
await session.reducerCommitQueue;
|
|
1311
|
+
const state = await this.getSessionState(session.id);
|
|
1312
|
+
const current = this.sessions.get(session.id);
|
|
1313
|
+
if (
|
|
1314
|
+
current !== session ||
|
|
1315
|
+
session.status !== 'running' ||
|
|
1316
|
+
state?.status !== 'running'
|
|
1317
|
+
) {
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (
|
|
1321
|
+
!state ||
|
|
1322
|
+
typeof state.renderOutput !== 'string' ||
|
|
1323
|
+
!Number.isFinite(state.outputSeq) ||
|
|
1324
|
+
!Number.isFinite(state.stateRevision)
|
|
1325
|
+
) {
|
|
1326
|
+
throw new Error(`Cannot serialize terminal ${session.id} without an exact reducer checkpoint`);
|
|
1327
|
+
}
|
|
1328
|
+
entries.push({
|
|
1329
|
+
id: session.id,
|
|
1330
|
+
metadata: session.metadata || {},
|
|
1331
|
+
processDetails: {
|
|
1332
|
+
cwd: session.shellCwd || session.cwd || '',
|
|
1333
|
+
title: state.title || session.title || '',
|
|
1334
|
+
},
|
|
1335
|
+
processLaunchConfig: {
|
|
1336
|
+
command: session.command || '',
|
|
1337
|
+
args: session.args || [],
|
|
1338
|
+
category: session.metadata?.category || '',
|
|
1339
|
+
},
|
|
1340
|
+
replayEvent: {
|
|
1341
|
+
events: [{
|
|
1342
|
+
data: state.renderOutput,
|
|
1343
|
+
cols: state.previewCols || session.previewCols || DEFAULT_COLS,
|
|
1344
|
+
rows: state.previewRows || session.previewRows || DEFAULT_ROWS,
|
|
1345
|
+
}],
|
|
1346
|
+
},
|
|
1347
|
+
timestamp: Date.now(),
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
const serializedTerminalState = serializeTerminalState(entries);
|
|
1351
|
+
preparation.phase = 'prepared';
|
|
1352
|
+
preparation.serializedTerminalState = serializedTerminalState;
|
|
1353
|
+
return {
|
|
1354
|
+
preparationToken: preparation.token,
|
|
1355
|
+
serializedTerminalState,
|
|
1356
|
+
};
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
this.resumePreparedTerminalState(preparation.token);
|
|
1359
|
+
throw error;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
resumeTerminalState(client = null, preparationToken = '') {
|
|
1364
|
+
if (client) this.assertActiveController(client);
|
|
1365
|
+
return this.resumePreparedTerminalState(preparationToken);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
resumePreparedTerminalState(preparationToken = '') {
|
|
1369
|
+
const preparation = this.rotationPreparation;
|
|
1370
|
+
if (!preparation) return { resumed: 0 };
|
|
1371
|
+
if (preparationToken && preparation.token !== preparationToken) {
|
|
1372
|
+
throw new Error('Native pty rotation preparation token does not match');
|
|
1373
|
+
}
|
|
1374
|
+
let resumed = 0;
|
|
1375
|
+
for (const session of this.sessions.values()) {
|
|
1376
|
+
if (session.rotationFrozen !== true) continue;
|
|
1377
|
+
session.rotationFrozen = false;
|
|
1378
|
+
const flowError = setTerminalExternalFlowControlBlocked(
|
|
1379
|
+
session.reducerFlowControl,
|
|
1380
|
+
session.process,
|
|
1381
|
+
false,
|
|
1382
|
+
);
|
|
1383
|
+
if (flowError) {
|
|
1384
|
+
this.failSessionScreenState(session, flowError);
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
resumed += 1;
|
|
1388
|
+
}
|
|
1389
|
+
if (this.rotationPreparation === preparation) {
|
|
1390
|
+
this.rotationPreparation = null;
|
|
1391
|
+
}
|
|
1392
|
+
return { resumed };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
726
1395
|
async updateSessionMetadata(sessionId, patch) {
|
|
727
1396
|
const session = this.sessions.get(sessionId);
|
|
728
1397
|
if (!session) return null;
|
|
@@ -735,9 +1404,39 @@ class NativePtyHost {
|
|
|
735
1404
|
return session.metadata;
|
|
736
1405
|
}
|
|
737
1406
|
|
|
738
|
-
shutdownHost() {
|
|
1407
|
+
shutdownHost(client, rawIdentity = {}, preparationToken = '') {
|
|
1408
|
+
const requestedGeneration = Math.floor(Number(rawIdentity.generation));
|
|
1409
|
+
const requestedId = typeof rawIdentity.id === 'string' ? rawIdentity.id : '';
|
|
1410
|
+
const active = this.activeControllerIdentity;
|
|
1411
|
+
const activeClient = client
|
|
1412
|
+
&& client === this.activeControllerClient
|
|
1413
|
+
&& active
|
|
1414
|
+
&& client.controllerId === active.id
|
|
1415
|
+
&& client.controllerGeneration === active.generation;
|
|
1416
|
+
const newerController = requestedId
|
|
1417
|
+
&& Number.isFinite(requestedGeneration)
|
|
1418
|
+
&& requestedGeneration > 0
|
|
1419
|
+
&& (!active || requestedGeneration > active.generation);
|
|
1420
|
+
if (!activeClient && !newerController) {
|
|
1421
|
+
throw new Error('Native pty shutdown requires the active or a newer controller');
|
|
1422
|
+
}
|
|
1423
|
+
const preparation = this.rotationPreparation;
|
|
1424
|
+
if (preparation) {
|
|
1425
|
+
if (
|
|
1426
|
+
preparation.phase !== 'prepared' ||
|
|
1427
|
+
!preparationToken ||
|
|
1428
|
+
preparationToken !== preparation.token
|
|
1429
|
+
) {
|
|
1430
|
+
throw new Error('Native pty shutdown requires the prepared rotation token');
|
|
1431
|
+
}
|
|
1432
|
+
preparation.phase = 'committing';
|
|
1433
|
+
} else if (this.hasLiveSessions()) {
|
|
1434
|
+
throw new Error('Native pty shutdown requires a prepared terminal checkpoint');
|
|
1435
|
+
}
|
|
739
1436
|
setImmediate(() => {
|
|
740
|
-
this.dispose().finally(() =>
|
|
1437
|
+
this.dispose().finally(() => {
|
|
1438
|
+
if (this.exitOnShutdown) process.exit(0);
|
|
1439
|
+
});
|
|
741
1440
|
});
|
|
742
1441
|
return { shuttingDown: true };
|
|
743
1442
|
}
|