claude-usage-limits 1.26.0 → 1.39.1

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.
@@ -27,7 +27,7 @@
27
27
  const fs = require('fs');
28
28
  const os = require('os');
29
29
  const path = require('path');
30
- const { spawnSync } = require('child_process');
30
+ const { spawn, spawnSync } = require('child_process');
31
31
 
32
32
  const relay = require('./relay.js');
33
33
  const net = require('./net.js');
@@ -143,8 +143,21 @@ function userIsPresent(cli) {
143
143
  // None of this is restored by --resume: a headless resume starts in the
144
144
  // permission mode a fresh -p run would, so a session that was running with
145
145
  // edits accepted comes back asking a person who is not there.
146
+ // The prompt is NOT in here. It travels on stdin - see deliverClaude.
147
+ //
148
+ // It used to be the argument after -p, and on 2026-09-14 a relay booked for
149
+ // 09:55 fired on time, three times, and died three times with the one line
150
+ // "The command line is too long." The continuation had grown to 7.5 KB and
151
+ // Windows caps a command line at 8,191 characters. Every retry hit the same
152
+ // wall, because the failure was deterministic, and the work sat on disk until
153
+ // somebody came home at 2:52 PM and found nothing done.
154
+ //
155
+ // claude -p reads the prompt from stdin when none is given as an argument and
156
+ // stdin is not a terminal, with a 10 MB cap. A 12 KB prompt was fed through
157
+ // that way and answered before this change was made. The prompt parameter is
158
+ // kept in the signature so callers and tests do not change shape.
146
159
  function claudeArgs(record, prompt, config, fallback) {
147
- const args = fallback ? ['--continue', '-p', prompt] : ['--resume', record.id, '-p', prompt];
160
+ const args = fallback ? ['--continue', '-p'] : ['--resume', record.id, '-p'];
148
161
  if (config.permissionMode) args.push('--permission-mode', config.permissionMode);
149
162
  // Nobody is awake to answer a prompt. Deny it rather than stall on it: a
150
163
  // resumed run that sits waiting for a keystroke burns its whole timeout and
@@ -179,14 +192,272 @@ function spawnOptionsFor(record, config, cli) {
179
192
  encoding: 'utf8',
180
193
  cwd: record.cwd,
181
194
  timeout: RESUME_TIMEOUT_MS,
182
- windowsHide: config && config.show === false,
195
+ // Always. The seen run is the interactive window (deliverVisible); this
196
+ // headless one shared the wake's console once and died with it.
197
+ windowsHide: true,
183
198
  shell: process.platform === 'win32' && /\.(cmd|bat)$/i.test(cli),
199
+ // The visible launcher sets this in its own script; the headless run
200
+ // inherited the wake's environment and could start an update at launch,
201
+ // with nobody there to watch it. Only when nothing has set it already.
202
+ env: Object.assign({}, process.env, process.env.DISABLE_AUTOUPDATER === undefined ? { DISABLE_AUTOUPDATER: '1' } : {}),
184
203
  };
185
204
  }
186
205
 
187
- function deliverClaude(record, prompt, config, cli, runs) {
206
+ // The window that says "Claude" and nothing else.
207
+ //
208
+ // On 2026-09-14 the 8:05 PM relay fired, resumed the session and worked - in
209
+ // a console window that showed only its title. `claude -p` prints its result
210
+ // when it has finished and nothing before, the wake was capturing both
211
+ // streams for the run log anyway, and a headless run does not register with
212
+ // Remote Control, so the phone showed nothing either. The user's words: "just
213
+ // a blank terminal screen that says Claude".
214
+ //
215
+ // So when the run is meant to be seen (relay show, the default) it is a real
216
+ // interactive session in its own window: resumed by id, model and permission
217
+ // mode set, Remote Control on under a name that says what it is, and the
218
+ // first prompt given as a short argument that points at the hand-off on
219
+ // disk. The hand-off itself cannot ride on argv (8,191 characters) and an
220
+ // interactive session's stdin is its keyboard, so the file is the one
221
+ // channel that fits every length. The session reads it with one tool call
222
+ // and carries on, and from then on it is a session like any other: on
223
+ // screen, on claude.ai/code, and typed into.
224
+ //
225
+ // The launcher is a .cmd file rather than a command line, so the only
226
+ // quoting rules that have to hold are cmd's own, applied to text this code
227
+ // wrote. `call` matters: claude.cmd is itself a batch file, and a batch file
228
+ // run without call never returns to the lines below it - the exit code
229
+ // would never be written and a failure would close the window unread.
230
+ const LAUNCH_GRACE_MS = 20000;
231
+
232
+ function wakePromptFile(id) {
233
+ return path.join(relay.configDir(), 'relay-wake-' + id + '.md');
234
+ }
235
+
236
+ function launcherFile(id) {
237
+ return path.join(relay.configDir(), 'relay-wake-' + id + (process.platform === 'win32' ? '.cmd' : '.sh'));
238
+ }
239
+
240
+ function exitFile(id) {
241
+ return path.join(relay.configDir(), 'relay-wake-' + id + '.exit');
242
+ }
243
+
244
+ // Short, plain, and free of quotes: it goes through cmd and then argv.
245
+ function pointerPrompt(file) {
246
+ return (
247
+ 'The usage window has reset and this is the plugin picking the work back up, not a new request. ' +
248
+ 'The full hand-off is in the file ' + file + ' - read it with the Read tool now and carry on from it ' +
249
+ 'at full quality, without re-asking what to do. If the working directory is not the project folder, ' +
250
+ 'it was resumed from a trusted folder on purpose: use absolute paths.'
251
+ );
252
+ }
253
+
254
+ // Doubled percent signs are the one escape cmd needs inside double quotes;
255
+ // a double quote itself has no escape there, so it is dropped.
256
+ function cmdArg(text) {
257
+ return '"' + String(text).replace(/%/g, '%%').replace(/"/g, '') + '"';
258
+ }
259
+
260
+ function visibleArgs(record, config, promptFile) {
261
+ const args = ['--resume', record.id];
262
+ if (record.launchCwd) args.push('--add-dir', record.cwd);
263
+ if (config && config.permissionMode) args.push('--permission-mode', config.permissionMode);
264
+ if (config && config.model) args.push('--model', config.model);
265
+ args.push('--remote-control', 'usage-limits relay ' + (record.project || path.basename(record.cwd)));
266
+ args.push(pointerPrompt(promptFile));
267
+ return args;
268
+ }
269
+
270
+ function launcherScript(record, config, cli, promptFile, exitPath) {
271
+ const name = String(record.project || path.basename(record.cwd)).replace(/[&|<>^%"]/g, '');
272
+ return [
273
+ '@echo off',
274
+ 'title Claude relay - ' + name,
275
+ 'cd /d ' + cmdArg(record.launchCwd || record.cwd),
276
+ 'set DISABLE_AUTOUPDATER=1',
277
+ // Seen live on 2026-09-14: launched from inside a session, the window
278
+ // inherited that session's markers - transcript saving was off (the
279
+ // child-session marker) and the hooks took it for the parent (its id).
280
+ // The resumed session is nobody's child, whoever opened the window.
281
+ 'set CLAUDE_CODE_CHILD_SESSION=',
282
+ 'set CLAUDE_CODE_SESSION_ID=',
283
+ 'call ' + cmdArg(cli) + ' ' + visibleArgs(record, config, promptFile).map(cmdArg).join(' '),
284
+ 'set CODE=%ERRORLEVEL%',
285
+ '> ' + cmdArg(exitPath) + ' echo %CODE%',
286
+ 'if not "%CODE%"=="0" (',
287
+ ' echo.',
288
+ ' echo [usage-limits relay] claude exited with code %CODE%. The window stays open so the message above can be read; the plan is still on disk.',
289
+ ' pause >nul',
290
+ ')',
291
+ '',
292
+ ].join('\r\n');
293
+ }
294
+
295
+ // `claude --resume` finds a session by its transcript, one folder per project
296
+ // under the config dir. Checking first means a session that is gone gets the
297
+ // same permanent answer the headless path gives, without opening a window
298
+ // whose only content would be the error.
299
+ function transcriptExists(id) {
300
+ const root = path.join(relay.configDir(), 'projects');
301
+ try {
302
+ for (const dir of fs.readdirSync(root)) {
303
+ if (fs.existsSync(path.join(root, dir, id + '.jsonl'))) return true;
304
+ }
305
+ } catch (err) {
306
+ // No projects folder at all.
307
+ }
308
+ return false;
309
+ }
310
+
311
+ function readExit(file) {
312
+ try {
313
+ const code = parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
314
+ return Number.isFinite(code) ? code : null;
315
+ } catch (err) {
316
+ return null;
317
+ }
318
+ }
319
+
320
+ function sleepMs(ms) {
321
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
322
+ }
323
+
324
+ // `start` returns as soon as the window exists; the launcher runs on in it.
325
+ // Pre-quoted arguments go through verbatim, because Node's own quoting is
326
+ // for programs that parse like C, and cmd does not.
327
+ function openWindow(launcher, exitPath, record) {
328
+ const run = spawnSync('cmd.exe', ['/d', '/c', 'start', '"Claude relay"', '/D', cmdArg(record.launchCwd || record.cwd), cmdArg(launcher)], {
329
+ encoding: 'utf8',
330
+ timeout: 30000,
331
+ windowsHide: true,
332
+ windowsVerbatimArguments: true,
333
+ });
334
+ if (run.status !== 0) throw new Error(((run.stderr || run.stdout || '') + '').trim().split('\n')[0] || 'start exited ' + run.status);
335
+ }
336
+
337
+ // The same window on macOS and Linux. UNVERIFIED on a real Mac or Linux box:
338
+ // written from the documented behaviour of osascript and the common terminal
339
+ // emulators, with the launcher script itself under test. The shell quoting
340
+ // is the POSIX one: close the quote, escape one, reopen.
341
+ const Q = String.fromCharCode(39);
342
+ function shQuote(text) {
343
+ return Q + String(text).split(Q).join(Q + String.fromCharCode(92) + Q + Q) + Q;
344
+ }
345
+
346
+ function launcherScriptPosix(record, config, cli, promptFile, exitPath) {
347
+ return [
348
+ '#!/bin/sh',
349
+ 'cd ' + shQuote(record.cwd) + ' || exit 1',
350
+ 'unset CLAUDE_CODE_CHILD_SESSION CLAUDE_CODE_SESSION_ID',
351
+ shQuote(cli) + ' ' + visibleArgs(record, config, promptFile).map(shQuote).join(' '),
352
+ 'code=$?',
353
+ 'echo "$code" > ' + shQuote(exitPath),
354
+ 'if [ "$code" -ne 0 ]; then',
355
+ ' echo',
356
+ ' echo "[usage-limits relay] claude exited with code $code. This window stays open so the message above can be read; the plan is still on disk."',
357
+ ' read -r _',
358
+ 'fi',
359
+ '',
360
+ ].join(String.fromCharCode(10));
361
+ }
362
+
363
+ function hasCommand(name) {
364
+ const run = spawnSync('sh', ['-c', 'command -v ' + shQuote(name)], { encoding: 'utf8', timeout: 10000 });
365
+ return run.status === 0;
366
+ }
367
+
368
+ function openWindowPosix(launcher) {
369
+ fs.chmodSync(launcher, 0o755);
370
+ if (process.platform === 'darwin') {
371
+ const run = spawnSync('osascript', ['-e', 'tell application "Terminal" to do script ' + JSON.stringify('sh ' + shQuote(launcher))], { encoding: 'utf8', timeout: 30000 });
372
+ if (run.status !== 0) throw new Error(((run.stderr || run.stdout || '') + '').trim().split(String.fromCharCode(10))[0] || 'osascript exited ' + run.status);
373
+ return;
374
+ }
375
+ if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) throw new Error('no DISPLAY or WAYLAND_DISPLAY: not a graphical session');
376
+ const terminals = [
377
+ ['x-terminal-emulator', ['-e', 'sh', launcher]],
378
+ ['gnome-terminal', ['--', 'sh', launcher]],
379
+ ['konsole', ['-e', 'sh', launcher]],
380
+ ['xterm', ['-e', 'sh', launcher]],
381
+ ];
382
+ for (const [bin, args] of terminals) {
383
+ if (!hasCommand(bin)) continue;
384
+ const child = spawn(bin, args, { detached: true, stdio: 'ignore' });
385
+ child.unref();
386
+ return;
387
+ }
388
+ throw new Error('no terminal emulator found (tried x-terminal-emulator, gnome-terminal, konsole, xterm)');
389
+ }
390
+
391
+ // `io` exists so the window path can be tested without a window: open, sleep
392
+ // and now are the three things it does to the world.
393
+ function deliverVisible(record, prompt, config, cli, runs, io) {
394
+ const ops = Object.assign({ open: process.platform === 'win32' ? openWindow : openWindowPosix, sleep: sleepMs, now: Date.now }, io || null);
395
+ if (!transcriptExists(record.id)) {
396
+ return {
397
+ ok: false,
398
+ permanent: true,
399
+ error:
400
+ 'the session ' + String(record.id).slice(0, 8) + ' no longer exists, so there was nothing to resume. ' +
401
+ 'The plan is still on disk: run "claude" in ' + record.cwd + ' and paste it in.',
402
+ };
403
+ }
404
+ const promptFile = wakePromptFile(record.id);
405
+ const launcher = launcherFile(record.id);
406
+ const exitPath = exitFile(record.id);
407
+ try {
408
+ fs.mkdirSync(relay.configDir(), { recursive: true });
409
+ fs.writeFileSync(promptFile, String(prompt == null ? '' : prompt) + '\n');
410
+ fs.writeFileSync(launcher, process.platform === 'win32'
411
+ ? launcherScript(record, config, cli, promptFile, exitPath)
412
+ : launcherScriptPosix(record, config, cli, promptFile, exitPath));
413
+ try {
414
+ fs.unlinkSync(exitPath);
415
+ } catch (err) {
416
+ // None left from last time.
417
+ }
418
+ } catch (err) {
419
+ return { ok: false, error: 'could not write the launcher: ' + err.message };
420
+ }
421
+ try {
422
+ ops.open(launcher, exitPath, record);
423
+ } catch (err) {
424
+ appendRun(runs, 'claude --resume (window)', { status: 1, stdout: '', stderr: err.message });
425
+ return { ok: false, openFailed: true, error: 'could not open a window: ' + err.message };
426
+ }
427
+ const started = ops.now();
428
+ while (ops.now() - started < LAUNCH_GRACE_MS) {
429
+ ops.sleep(1000);
430
+ const code = readExit(exitPath);
431
+ if (code !== null) {
432
+ const after = Math.round((ops.now() - started) / 1000);
433
+ appendRun(runs, 'claude --resume (window)', {
434
+ status: code,
435
+ stdout: '',
436
+ stderr: 'exited within ' + after + 's; its output is in the window, which stays open when the exit is not 0',
437
+ });
438
+ if (code === 0) return { ok: true, how: 'claude --resume in a window' };
439
+ return { ok: false, error: 'claude exited ' + code + ' in the window it opened (left open so the message can be read)' };
440
+ }
441
+ }
442
+ appendRun(runs, 'claude --resume (window)', {
443
+ status: 0,
444
+ stdout:
445
+ 'still running after ' + Math.round(LAUNCH_GRACE_MS / 1000) + 's: an interactive session in its own window, Remote Control on. ' +
446
+ 'Its output is on screen and in the session transcript, not in this log.',
447
+ stderr: '',
448
+ });
449
+ return { ok: true, how: 'claude --resume in a window, Remote Control on' };
450
+ }
451
+ function deliverClaude(record, prompt, config, cli, runs, io) {
452
+ // Seen by default. relay show off keeps the headless run, and so does a
453
+ // machine where no window could be opened (no terminal emulator found).
454
+ if (config && config.show !== false) {
455
+ const visible = deliverVisible(record, prompt, config, cli, runs, io);
456
+ if (!visible.openFailed) return visible;
457
+ }
188
458
  const args = claudeArgs(record, prompt, config, false);
189
- const options = spawnOptionsFor(record, config, cli);
459
+ // stdin, never argv. See claudeArgs for the 7.5 KB note that proved why.
460
+ const options = Object.assign(spawnOptionsFor(record, config, cli), { input: String(prompt == null ? '' : prompt) });
190
461
  const run = appendRun(runs, 'claude --resume', spawnSync(cli, args, options));
191
462
  if (run.status === 0) return { ok: true, how: 'claude --resume' };
192
463
  const detail = ((run.stderr || run.stdout || '') + '').trim().split('\n')[0];
@@ -221,16 +492,29 @@ function deliverCodex(record, prompt, cli, runs) {
221
492
  // Codex keeps interactive sessions on a local app server, and queue is the
222
493
  // supported way to put a message into one from outside. If the thread is
223
494
  // gone, exec resume does the same work in a fresh process.
224
- const queued = spawnSync(cli, ['queue', '--thread', record.id, '--message', prompt], {
225
- encoding: 'utf8',
226
- cwd: record.cwd,
227
- timeout: 60000,
228
- windowsHide: true,
229
- });
495
+ // Same wall as the Claude path: Windows caps a command line at 8,191
496
+ // characters and a continuation can be longer than that. `queue --message`
497
+ // has no stdin form, so past the safe length it is skipped rather than
498
+ // attempted - a call that is known to fail is not worth the time it takes to
499
+ // fail. `exec` reads its prompt from stdin when none is given, so that path
500
+ // carries the long ones. UNVERIFIED against a live Codex: this is the Claude
501
+ // fix applied by analogy to the documented exec behaviour, not a measured
502
+ // run, because measuring it would spend the user's ChatGPT quota.
503
+ const text = String(prompt == null ? '' : prompt);
504
+ const fitsArgv = text.length <= 6000;
505
+ const queued = fitsArgv
506
+ ? spawnSync(cli, ['queue', '--thread', record.id, '--message', text], {
507
+ encoding: 'utf8',
508
+ cwd: record.cwd,
509
+ timeout: 60000,
510
+ windowsHide: true,
511
+ })
512
+ : { status: 1, stdout: '', stderr: 'continuation is ' + text.length + ' chars, too long for argv; going straight to exec' };
230
513
  appendRun(runs, 'codex queue', queued);
231
514
  if (queued.status === 0) return { ok: true, how: 'codex queue' };
232
- const run = spawnSync(cli, ['exec', 'resume', record.id, prompt, '--skip-git-repo-check'], {
515
+ const run = spawnSync(cli, ['exec', 'resume', record.id, '--skip-git-repo-check'], {
233
516
  encoding: 'utf8',
517
+ input: text,
234
518
  cwd: record.cwd,
235
519
  timeout: RESUME_TIMEOUT_MS,
236
520
  windowsHide: true,
@@ -245,7 +529,14 @@ function finish(state, record, outcome, detail, now) {
245
529
  Object.assign({}, record, { endedAt: now, outcome, detail: detail || null })
246
530
  );
247
531
  state.history = state.history.slice(-10);
248
- state.armed = null;
532
+ if (outcome === 'resumed') {
533
+ try {
534
+ fs.unlinkSync(relay.planFile(record.id));
535
+ } catch (err) {
536
+ // No note, or already gone.
537
+ }
538
+ }
539
+ relay.dropRecord(state, record.id);
249
540
  relay.write(state);
250
541
  relay.note('wake ' + record.id + ': ' + outcome + (detail ? ' - ' + detail : ''), now);
251
542
  // Last, deliberately: this deletes the task this process is running under,
@@ -262,6 +553,11 @@ function finish(state, record, outcome, detail, now) {
262
553
  // `overrides` exists so the retry path can be tested without spawning a CLI or
263
554
  // registering a real scheduled task. Production passes nothing and gets the
264
555
  // real functions; only the names listed here can be swapped.
556
+ // The record this wake owns, wherever the state keeps it.
557
+ function mine(held, record) {
558
+ return relay.armedFor(held, record.id);
559
+ }
560
+
265
561
  async function run(now, argv, overrides) {
266
562
  const deps = Object.assign(
267
563
  { windowReopened, deliverClaude, deliverCodex, toast, userIsPresent, arm: relay.arm, capabilities: relay.capabilities },
@@ -269,9 +565,12 @@ async function run(now, argv, overrides) {
269
565
  );
270
566
  const id = argOf(argv, '--id');
271
567
  const state = relay.read();
272
- const record = state.armed;
273
- if (!record) return { outcome: 'nothing-armed' };
274
- if (id && record.id !== id) return { outcome: 'superseded' };
568
+ const record = id ? relay.armedFor(state, id) : state.armed;
569
+ if (!record) return { outcome: relay.records(state).length ? 'superseded' : 'nothing-armed' };
570
+ // Marked before anything else, so a wake that dies part-way (the 8:05 PM
571
+ // one was killed with its console) is told apart from one that never ran.
572
+ record.wokeAt = now;
573
+ relay.write(state);
275
574
 
276
575
  const config = relay.settings(state);
277
576
  const capabilities = deps.capabilities();
@@ -299,8 +598,8 @@ async function run(now, argv, overrides) {
299
598
  });
300
599
  if (again.ok) {
301
600
  const held = relay.read();
302
- held.armed.attempt = attempt;
303
- held.armed.continuation = record.continuation;
601
+ mine(held, record).attempt = attempt;
602
+ mine(held, record).continuation = record.continuation;
304
603
  relay.write(held);
305
604
  }
306
605
  relay.note('wake ' + record.id + ': window still at ' + Math.round(reopened.percent) + '%, retry ' + attempt, now);
@@ -339,10 +638,10 @@ async function run(now, argv, overrides) {
339
638
  });
340
639
  if (again.ok) {
341
640
  const held = relay.read();
342
- if (held.armed) {
343
- held.armed.offlineAttempt = offlineAttempt;
344
- held.armed.attempt = record.attempt || 0;
345
- held.armed.continuation = record.continuation;
641
+ if (mine(held, record)) {
642
+ mine(held, record).offlineAttempt = offlineAttempt;
643
+ mine(held, record).attempt = record.attempt || 0;
644
+ mine(held, record).continuation = record.continuation;
346
645
  relay.write(held);
347
646
  }
348
647
  // Said once, on the first miss, then quiet. Twelve toasts through the
@@ -371,7 +670,8 @@ async function run(now, argv, overrides) {
371
670
  continuation,
372
671
  work: record.work && record.work.todos ? record.work : null,
373
672
  thinking: config.thinking !== 'off',
374
- voice: voice.card(),
673
+ voice: config.voice === false ? null : voice.card(),
674
+ bugcheck: config.bugcheck,
375
675
  });
376
676
 
377
677
  let mode = config.mode;
@@ -399,6 +699,31 @@ async function run(now, argv, overrides) {
399
699
 
400
700
  deps.toast('Usage limits: resuming', 'Carrying on with ' + (record.project || path.basename(record.cwd)) + ' where the limit stopped it.');
401
701
  const runs = [];
702
+ // The two start-up questions, answered again right before the launch in
703
+
704
+ // case anything reset them since arming.
705
+
706
+ if (record.host !== 'codex') {
707
+
708
+ if (record.launchCwd) {
709
+ try {
710
+ fs.mkdirSync(record.launchCwd, { recursive: true });
711
+ } catch (err) {
712
+ }
713
+ }
714
+ try {
715
+ const cj = relay.claudeJsonFile();
716
+ if (fs.existsSync(cj)) fs.copyFileSync(cj, cj + '.bak-usage-limits-wake');
717
+ } catch (err) {
718
+ }
719
+ const pre = relay.preflightPrompts(record.launchCwd || record.cwd, config);
720
+
721
+ if (pre.ok && pre.changes.length) relay.note('wake ' + record.id + ': pre-answered ' + pre.changes.join(', '), now);
722
+
723
+ else if (!pre.ok) relay.note('wake ' + record.id + ': could not pre-answer the start-up questions: ' + pre.error, now);
724
+
725
+ }
726
+
402
727
  const delivered = record.host === host.CODEX
403
728
  ? deps.deliverCodex(record, prompt, cli, runs)
404
729
  : deps.deliverClaude(record, prompt, config, cli, runs);
@@ -443,8 +768,8 @@ async function run(now, argv, overrides) {
443
768
  });
444
769
  if (again.ok) {
445
770
  const held = relay.read();
446
- held.armed.attempt = attempt;
447
- held.armed.continuation = record.continuation;
771
+ mine(held, record).attempt = attempt;
772
+ mine(held, record).continuation = record.continuation;
448
773
  relay.write(held);
449
774
  deps.toast(
450
775
  'Usage limits: retrying',
@@ -482,11 +807,11 @@ async function run(now, argv, overrides) {
482
807
  });
483
808
  if (nextWindow.ok) {
484
809
  const held = relay.read();
485
- if (held.armed) {
486
- held.armed.attempt = 0;
487
- held.armed.offlineAttempt = 0;
488
- held.armed.rearms = rearms + 1;
489
- held.armed.continuation = record.continuation;
810
+ if (mine(held, record)) {
811
+ mine(held, record).attempt = 0;
812
+ mine(held, record).offlineAttempt = 0;
813
+ mine(held, record).rearms = rearms + 1;
814
+ mine(held, record).continuation = record.continuation;
490
815
  relay.write(held);
491
816
  }
492
817
  deps.toast('Usage limits: could not resume',
@@ -514,4 +839,4 @@ if (require.main === module) {
514
839
  );
515
840
  }
516
841
 
517
- module.exports = { run, toast, userIsPresent, claudeArgs, deliverClaude, deliverCodex, windowReopened, argOf, appendRun, spawnOptionsFor };
842
+ module.exports = { launcherScriptPosix, shQuote, visibleArgs, launcherScript, pointerPrompt, transcriptExists, wakePromptFile, LAUNCH_GRACE_MS, run, toast, userIsPresent, claudeArgs, deliverClaude, deliverCodex, windowReopened, argOf, appendRun, spawnOptionsFor };