@skrr-ai/cli 0.1.78 → 0.1.79
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/lib/dedicated-lease-command.js +1 -0
- package/dist/lib/dedicated-terminal.d.ts +3 -0
- package/dist/lib/dedicated-terminal.js +71 -15
- package/oclif.manifest.json +29593 -29593
- package/package.json +1 -1
|
@@ -129,6 +129,7 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
|
|
|
129
129
|
...(flags.cwd ? { cwd: flags.cwd } : {}),
|
|
130
130
|
...(initialCommand ? { initialCommand } : {}),
|
|
131
131
|
persistent,
|
|
132
|
+
resumeExistingSupported: capabilities.includes('persistent_terminal_resume_v1'),
|
|
132
133
|
...(sessionName ? { sessionName } : {}),
|
|
133
134
|
leaseId,
|
|
134
135
|
input: process.stdin,
|
|
@@ -27,6 +27,7 @@ export interface TerminalOutputFrame {
|
|
|
27
27
|
encoding?: 'utf8' | 'base64';
|
|
28
28
|
exitCode?: number | null;
|
|
29
29
|
signal?: string | null;
|
|
30
|
+
code?: string;
|
|
30
31
|
}
|
|
31
32
|
export interface TerminalStartAck {
|
|
32
33
|
sessionId?: string;
|
|
@@ -66,6 +67,8 @@ export interface DedicatedTerminalOptions {
|
|
|
66
67
|
* capability before asking.
|
|
67
68
|
*/
|
|
68
69
|
persistent?: boolean;
|
|
70
|
+
/** Capability-gated recovery can attach only an existing named shell. */
|
|
71
|
+
resumeExistingSupported?: boolean;
|
|
69
72
|
/**
|
|
70
73
|
* The session suffix (`skrr-term-<name>`). The same name re-attaches the
|
|
71
74
|
* same session; omitted under `persistent`, the machine names the session
|
|
@@ -67,6 +67,9 @@ function describeTerminalStartRefusal(ack) {
|
|
|
67
67
|
if (code === 'DEDICATED_RUNTIME_GENERATION_STALE') {
|
|
68
68
|
return 'The machine was replaced since its daemon registered. Wait a minute and try again.';
|
|
69
69
|
}
|
|
70
|
+
if (code === 'persistent-session-missing') {
|
|
71
|
+
return 'The original terminal no longer exists. Start a new terminal explicitly; no replacement shell was opened.';
|
|
72
|
+
}
|
|
70
73
|
if (code === 'not_found' || code === 'offline') {
|
|
71
74
|
return 'The machine’s daemon is not connected. Wait for it to come online and try again.';
|
|
72
75
|
}
|
|
@@ -94,6 +97,9 @@ function runDedicatedTerminal(options) {
|
|
|
94
97
|
let sessionReady = false;
|
|
95
98
|
let reattaching = false;
|
|
96
99
|
let persistentName = options.sessionName;
|
|
100
|
+
let retryTimer;
|
|
101
|
+
let recoveryTimer;
|
|
102
|
+
let connected = false;
|
|
97
103
|
const socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), {
|
|
98
104
|
path: '/ws/agentic',
|
|
99
105
|
auth: {
|
|
@@ -170,6 +176,10 @@ function runDedicatedTerminal(options) {
|
|
|
170
176
|
const onSigterm = () => finish(143, true);
|
|
171
177
|
const onSighup = () => finish(129, true);
|
|
172
178
|
const onStartTimeout = () => {
|
|
179
|
+
if (reattaching) {
|
|
180
|
+
scheduleRetry();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
173
183
|
fail(new DedicatedTerminalError('The machine did not answer the terminal start in time. Its daemon may be busy or offline.', 'TERMINAL_START_TIMEOUT'));
|
|
174
184
|
};
|
|
175
185
|
let startTimer = setTimeout(onStartTimeout, exports.TERMINAL_START_TIMEOUT_MS);
|
|
@@ -190,6 +200,8 @@ function runDedicatedTerminal(options) {
|
|
|
190
200
|
return;
|
|
191
201
|
finished = true;
|
|
192
202
|
clearTimeout(startTimer);
|
|
203
|
+
clearTimeout(retryTimer);
|
|
204
|
+
clearTimeout(recoveryTimer);
|
|
193
205
|
pending?.close();
|
|
194
206
|
restoreTty();
|
|
195
207
|
detachReconnectFailed();
|
|
@@ -203,6 +215,8 @@ function runDedicatedTerminal(options) {
|
|
|
203
215
|
return;
|
|
204
216
|
finished = true;
|
|
205
217
|
clearTimeout(startTimer);
|
|
218
|
+
clearTimeout(retryTimer);
|
|
219
|
+
clearTimeout(recoveryTimer);
|
|
206
220
|
pending?.close();
|
|
207
221
|
restoreTty();
|
|
208
222
|
detachReconnectFailed();
|
|
@@ -212,15 +226,19 @@ function runDedicatedTerminal(options) {
|
|
|
212
226
|
socket.on('connect_error', (err) => {
|
|
213
227
|
// A failed retry while reattaching is not fatal — the attempt countdown
|
|
214
228
|
// keeps running and `reconnect_failed` ends it if they all miss.
|
|
215
|
-
if (persistent && reattaching)
|
|
229
|
+
if (persistent && reattaching && !err.data?.code)
|
|
216
230
|
return;
|
|
217
231
|
fail(new DedicatedTerminalError(`Could not connect to skrr: ${err.message}`, err.data?.code || 'TERMINAL_CONNECT_FAILED'));
|
|
218
232
|
});
|
|
219
|
-
|
|
220
|
-
if (finished)
|
|
233
|
+
function sendStart() {
|
|
234
|
+
if (finished || !connected)
|
|
221
235
|
return;
|
|
236
|
+
// Clear the previous relay entry too; never replay bytes sent before loss.
|
|
237
|
+
if (sessionId)
|
|
238
|
+
socket.emit(events.stop, { sessionId });
|
|
222
239
|
sessionId = (0, node_crypto_1.randomUUID)();
|
|
223
240
|
clearTimeout(startTimer);
|
|
241
|
+
clearTimeout(retryTimer);
|
|
224
242
|
startTimer = setTimeout(onStartTimeout, exports.TERMINAL_START_TIMEOUT_MS);
|
|
225
243
|
startTimer.unref();
|
|
226
244
|
socket.emit(events.start, {
|
|
@@ -228,13 +246,44 @@ function runDedicatedTerminal(options) {
|
|
|
228
246
|
sessionId,
|
|
229
247
|
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
230
248
|
...(persistent ? { persistent: true } : {}),
|
|
231
|
-
|
|
232
|
-
// machine named itself comes back in the ack, and a re-attach must
|
|
233
|
-
// name it — `tmux -A` without `-s` would open a fresh session instead.
|
|
249
|
+
...(reattaching ? { resumeExisting: true } : {}),
|
|
234
250
|
...(persistentName ? { sessionName: persistentName } : {}),
|
|
235
251
|
...(options.leaseId ? { leaseId: options.leaseId } : {}),
|
|
236
252
|
...size(),
|
|
237
253
|
});
|
|
254
|
+
}
|
|
255
|
+
function scheduleRetry() {
|
|
256
|
+
clearTimeout(startTimer);
|
|
257
|
+
clearTimeout(retryTimer);
|
|
258
|
+
if (finished || !reattaching)
|
|
259
|
+
return;
|
|
260
|
+
retryTimer = setTimeout(sendStart, 2_000);
|
|
261
|
+
retryTimer.unref();
|
|
262
|
+
}
|
|
263
|
+
function recover() {
|
|
264
|
+
if (finished || reattaching)
|
|
265
|
+
return;
|
|
266
|
+
started = false;
|
|
267
|
+
pending?.close(); // The sign-in command is never typed a second time.
|
|
268
|
+
clearTimeout(startTimer);
|
|
269
|
+
if (!persistent || !persistentName || !options.resumeExistingSupported) {
|
|
270
|
+
errorOutput.write(`\r\n[terminal attachment lost; rerun the same command to reattach${persistentName ? ` to '${persistentName}'` : ''}]\r\n`);
|
|
271
|
+
finish(1, false);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
reattaching = true;
|
|
275
|
+
errorOutput.write(`\r\n[lost the terminal attachment — reattaching to session '${persistentName}'…]\r\n`);
|
|
276
|
+
recoveryTimer = setTimeout(() => {
|
|
277
|
+
errorOutput.write(`\r\n[could not reattach within 60 seconds; rerun the same command for session '${persistentName}']\r\n`);
|
|
278
|
+
finish(1, false);
|
|
279
|
+
}, 60_000);
|
|
280
|
+
recoveryTimer.unref();
|
|
281
|
+
if (connected)
|
|
282
|
+
sendStart();
|
|
283
|
+
}
|
|
284
|
+
socket.on('connect', () => {
|
|
285
|
+
connected = true;
|
|
286
|
+
sendStart();
|
|
238
287
|
});
|
|
239
288
|
socket.on(events.startAck, (ack) => {
|
|
240
289
|
if (!ack || ack.sessionId !== sessionId || started || finished)
|
|
@@ -242,6 +291,12 @@ function runDedicatedTerminal(options) {
|
|
|
242
291
|
if (ack.daemonId && ack.daemonId !== options.daemonId)
|
|
243
292
|
return;
|
|
244
293
|
if (!ack.ok) {
|
|
294
|
+
// Only daemon availability refusals are retryable. Authorization,
|
|
295
|
+
// generation fencing and a missing original shell end recovery.
|
|
296
|
+
if (reattaching && (ack.code === 'offline' || ack.code === 'not_found')) {
|
|
297
|
+
scheduleRetry();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
245
300
|
fail(new DedicatedTerminalError(describeTerminalStartRefusal(ack), ack.code || 'TERMINAL_START_REFUSED'));
|
|
246
301
|
return;
|
|
247
302
|
}
|
|
@@ -249,6 +304,8 @@ function runDedicatedTerminal(options) {
|
|
|
249
304
|
started = true;
|
|
250
305
|
reattaching = false;
|
|
251
306
|
clearTimeout(startTimer);
|
|
307
|
+
clearTimeout(retryTimer);
|
|
308
|
+
clearTimeout(recoveryTimer);
|
|
252
309
|
if (typeof ack.persistentName === 'string' && ack.persistentName) {
|
|
253
310
|
persistentName = ack.persistentName;
|
|
254
311
|
}
|
|
@@ -278,8 +335,13 @@ function runDedicatedTerminal(options) {
|
|
|
278
335
|
return;
|
|
279
336
|
if (frame.daemonId && frame.daemonId !== options.daemonId)
|
|
280
337
|
return;
|
|
338
|
+
if (frame.code === 'daemon-disconnected' ||
|
|
339
|
+
(frame.type === 'error' && (frame.code === 'not-found' || frame.code === 'closed'))) {
|
|
340
|
+
recover();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
281
343
|
if (frame.type === 'exit') {
|
|
282
|
-
finish(typeof frame.exitCode === 'number' ? frame.exitCode :
|
|
344
|
+
finish(typeof frame.exitCode === 'number' ? frame.exitCode : 1, false);
|
|
283
345
|
return;
|
|
284
346
|
}
|
|
285
347
|
if (frame.type === 'error' || frame.type === 'info') {
|
|
@@ -318,17 +380,11 @@ function runDedicatedTerminal(options) {
|
|
|
318
380
|
}
|
|
319
381
|
}
|
|
320
382
|
socket.on('disconnect', () => {
|
|
383
|
+
connected = false;
|
|
321
384
|
if (finished)
|
|
322
385
|
return;
|
|
323
386
|
if (persistent) {
|
|
324
|
-
|
|
325
|
-
// detaches. socket.io retries and the next start re-attaches by name.
|
|
326
|
-
started = false;
|
|
327
|
-
if (!reattaching) {
|
|
328
|
-
reattaching = true;
|
|
329
|
-
const named = persistentName ? ` '${persistentName}'` : '';
|
|
330
|
-
errorOutput.write(`\r\n[lost the connection to skrr — reattaching to session${named}…]\r\n`);
|
|
331
|
-
}
|
|
387
|
+
recover();
|
|
332
388
|
return;
|
|
333
389
|
}
|
|
334
390
|
if (!started) {
|