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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +107 -2
- package/commands/relay.md +10 -5
- package/commands/usage-mode.md +3 -0
- package/package.json +4 -3
- package/skills/usage-limits/SKILL.md +39 -2
- package/skills/usage-limits/references/how-it-works.md +3 -2
- package/skills/usage-limits/scripts/agy-hook.js +11 -14
- package/skills/usage-limits/scripts/brief.js +281 -36
- package/skills/usage-limits/scripts/ceiling.js +47 -9
- package/skills/usage-limits/scripts/defer.js +9 -7
- package/skills/usage-limits/scripts/feed.js +36 -8
- package/skills/usage-limits/scripts/install-antigravity.js +2 -1
- package/skills/usage-limits/scripts/mode.js +98 -3
- package/skills/usage-limits/scripts/pulse.js +80 -29
- package/skills/usage-limits/scripts/relay.js +599 -67
- package/skills/usage-limits/scripts/stop.js +2 -2
- package/skills/usage-limits/scripts/tally.js +10 -1
- package/skills/usage-limits/scripts/usage.js +113 -14
- package/skills/usage-limits/scripts/wake.js +356 -31
|
@@ -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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
303
|
-
held.
|
|
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
|
|
343
|
-
held.
|
|
344
|
-
held.
|
|
345
|
-
held.
|
|
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.
|
|
447
|
-
held.
|
|
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
|
|
486
|
-
held.
|
|
487
|
-
held.
|
|
488
|
-
held.
|
|
489
|
-
held.
|
|
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 };
|