@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.
@@ -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
- socket.on('connect', () => {
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
- // The RESOLVED name, not just the requested one: a `--new` session the
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 : 0, false);
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
- // The tmux session outlives the link — the tmux client's death only
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) {