claude-usage-limits 1.23.0 → 1.25.0
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/commands/defer.md +47 -0
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +30 -1
- package/skills/usage-limits/scripts/defer.js +318 -0
- package/skills/usage-limits/scripts/net.js +179 -0
- package/skills/usage-limits/scripts/relay.js +298 -5
- package/skills/usage-limits/scripts/stop.js +102 -1
- package/skills/usage-limits/scripts/wake.js +235 -34
|
@@ -30,6 +30,7 @@ const path = require('path');
|
|
|
30
30
|
const { spawnSync } = require('child_process');
|
|
31
31
|
|
|
32
32
|
const relay = require('./relay.js');
|
|
33
|
+
const net = require('./net.js');
|
|
33
34
|
const voice = require('./voice.js');
|
|
34
35
|
const host = require('./host.js');
|
|
35
36
|
|
|
@@ -145,38 +146,78 @@ function userIsPresent(cli) {
|
|
|
145
146
|
function claudeArgs(record, prompt, config, fallback) {
|
|
146
147
|
const args = fallback ? ['--continue', '-p', prompt] : ['--resume', record.id, '-p', prompt];
|
|
147
148
|
if (config.permissionMode) args.push('--permission-mode', config.permissionMode);
|
|
149
|
+
// Nobody is awake to answer a prompt. Deny it rather than stall on it: a
|
|
150
|
+
// resumed run that sits waiting for a keystroke burns its whole timeout and
|
|
151
|
+
// reports nothing.
|
|
152
|
+
args.push('--permission-prompts', 'none');
|
|
148
153
|
if (!fallback && config.model) args.push('--model', config.model);
|
|
149
154
|
return args;
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
// The whole output of a resumed run, kept on disk.
|
|
158
|
+
//
|
|
159
|
+
// A wake that failed at four in the morning left one line in the note log, and
|
|
160
|
+
// the only honest answer to "what happened" was "something". The run log is the
|
|
161
|
+
// rest of the story: what the CLI printed, on both streams, for every launch
|
|
162
|
+
// this wake attempted. `relay log --run` reads the newest one back.
|
|
163
|
+
function appendRun(runs, label, result) {
|
|
164
|
+
if (!runs) return result;
|
|
165
|
+
const code = result && result.status !== null && result.status !== undefined ? result.status : 'null';
|
|
166
|
+
runs.push(
|
|
167
|
+
'--- ' + label + ' (exit ' + code + ') ---' + '\n' +
|
|
168
|
+
((result && result.stdout) || '') +
|
|
169
|
+
((result && result.stderr) ? '\n' + '--- stderr ---' + '\n' + result.stderr : '')
|
|
170
|
+
);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Invisible is tidier, and it is also how somebody finds out in the morning
|
|
175
|
+
// that nothing happened and has no way to tell why. `relay show off` restores
|
|
176
|
+
// the old behaviour for anyone who wants their screen left alone.
|
|
177
|
+
function spawnOptionsFor(record, config, cli) {
|
|
178
|
+
return {
|
|
155
179
|
encoding: 'utf8',
|
|
156
180
|
cwd: record.cwd,
|
|
157
181
|
timeout: RESUME_TIMEOUT_MS,
|
|
158
|
-
windowsHide:
|
|
182
|
+
windowsHide: config && config.show === false,
|
|
159
183
|
shell: process.platform === 'win32' && /\.(cmd|bat)$/i.test(cli),
|
|
160
|
-
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function deliverClaude(record, prompt, config, cli, runs) {
|
|
188
|
+
const args = claudeArgs(record, prompt, config, false);
|
|
189
|
+
const options = spawnOptionsFor(record, config, cli);
|
|
190
|
+
const run = appendRun(runs, 'claude --resume', spawnSync(cli, args, options));
|
|
161
191
|
if (run.status === 0) return { ok: true, how: 'claude --resume' };
|
|
162
192
|
const detail = ((run.stderr || run.stdout || '') + '').trim().split('\n')[0];
|
|
163
|
-
// A session id that no longer resolves
|
|
164
|
-
//
|
|
193
|
+
// A session id that no longer resolves used to fall back to `--continue`,
|
|
194
|
+
// on the reasoning that the work still needs doing and only the thread is
|
|
195
|
+
// gone. That fallback is removed, because of what `--continue` actually
|
|
196
|
+
// selects.
|
|
197
|
+
//
|
|
198
|
+
// `--continue` normally SKIPS sessions created by `claude -p`, the SDK and
|
|
199
|
+
// /loop - but `claude -p --continue`, which is exactly what this ran,
|
|
200
|
+
// INCLUDES them. So the most recent session it could land on is a previous
|
|
201
|
+
// relay's own headless run, not the user's work. Resuming that, unattended,
|
|
202
|
+
// at four in the morning, with permissionMode bypassPermissions, means an
|
|
203
|
+
// agent continuing a conversation nobody chose, in a directory it was not
|
|
204
|
+
// asked about.
|
|
205
|
+
//
|
|
206
|
+
// The plan is on disk either way. A wake that stops and says the thread is
|
|
207
|
+
// gone loses nothing; a wake that resumes the wrong conversation can.
|
|
165
208
|
if (/No conversation found/i.test(detail)) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
if (second.status === 0) return { ok: true, how: 'claude --continue' };
|
|
174
|
-
return { ok: false, error: ((second.stderr || second.stdout || '') + '').trim().split('\n')[0] || detail };
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
error:
|
|
212
|
+
'the session ' + String(record.id).slice(0, 8) + ' no longer exists, so there was nothing to resume. ' +
|
|
213
|
+
'The plan is still on disk: run "claude" in ' + record.cwd + ' and paste it in.',
|
|
214
|
+
permanent: true,
|
|
215
|
+
};
|
|
175
216
|
}
|
|
176
217
|
return { ok: false, error: detail || 'claude exited ' + run.status };
|
|
177
218
|
}
|
|
178
219
|
|
|
179
|
-
function deliverCodex(record, prompt, cli) {
|
|
220
|
+
function deliverCodex(record, prompt, cli, runs) {
|
|
180
221
|
// Codex keeps interactive sessions on a local app server, and queue is the
|
|
181
222
|
// supported way to put a message into one from outside. If the thread is
|
|
182
223
|
// gone, exec resume does the same work in a fresh process.
|
|
@@ -186,6 +227,7 @@ function deliverCodex(record, prompt, cli) {
|
|
|
186
227
|
timeout: 60000,
|
|
187
228
|
windowsHide: true,
|
|
188
229
|
});
|
|
230
|
+
appendRun(runs, 'codex queue', queued);
|
|
189
231
|
if (queued.status === 0) return { ok: true, how: 'codex queue' };
|
|
190
232
|
const run = spawnSync(cli, ['exec', 'resume', record.id, prompt, '--skip-git-repo-check'], {
|
|
191
233
|
encoding: 'utf8',
|
|
@@ -193,6 +235,7 @@ function deliverCodex(record, prompt, cli) {
|
|
|
193
235
|
timeout: RESUME_TIMEOUT_MS,
|
|
194
236
|
windowsHide: true,
|
|
195
237
|
});
|
|
238
|
+
appendRun(runs, 'codex exec resume', run);
|
|
196
239
|
if (run.status === 0) return { ok: true, how: 'codex exec resume' };
|
|
197
240
|
return { ok: false, error: ((run.stderr || run.stdout || '') + '').trim().split('\n')[0] || 'codex exited ' + run.status };
|
|
198
241
|
}
|
|
@@ -216,7 +259,14 @@ function finish(state, record, outcome, detail, now) {
|
|
|
216
259
|
}
|
|
217
260
|
}
|
|
218
261
|
|
|
219
|
-
|
|
262
|
+
// `overrides` exists so the retry path can be tested without spawning a CLI or
|
|
263
|
+
// registering a real scheduled task. Production passes nothing and gets the
|
|
264
|
+
// real functions; only the names listed here can be swapped.
|
|
265
|
+
async function run(now, argv, overrides) {
|
|
266
|
+
const deps = Object.assign(
|
|
267
|
+
{ windowReopened, deliverClaude, deliverCodex, toast, userIsPresent, arm: relay.arm, capabilities: relay.capabilities },
|
|
268
|
+
overrides || null
|
|
269
|
+
);
|
|
220
270
|
const id = argOf(argv, '--id');
|
|
221
271
|
const state = relay.read();
|
|
222
272
|
const record = state.armed;
|
|
@@ -224,20 +274,20 @@ async function run(now, argv) {
|
|
|
224
274
|
if (id && record.id !== id) return { outcome: 'superseded' };
|
|
225
275
|
|
|
226
276
|
const config = relay.settings(state);
|
|
227
|
-
const capabilities =
|
|
277
|
+
const capabilities = deps.capabilities();
|
|
228
278
|
|
|
229
|
-
const reopened = await windowReopened(record, now);
|
|
279
|
+
const reopened = await deps.windowReopened(record, now);
|
|
230
280
|
// Only reschedule on a reading that says the window is still full. An
|
|
231
281
|
// unreadable meter is not evidence of a limit, and refusing to act on it
|
|
232
282
|
// would turn every offline moment into a cancelled relay.
|
|
233
283
|
if (reopened.known && Number.isFinite(reopened.percent) && reopened.percent >= config.at) {
|
|
234
284
|
const attempt = (record.attempt || 0) + 1;
|
|
235
285
|
if (attempt >= config.attempts) {
|
|
236
|
-
toast('Usage limits', 'The window still reads ' + Math.round(reopened.percent) + ' per cent after ' + attempt + ' checks. The plan is saved; pick it up when you are ready.');
|
|
286
|
+
deps.toast('Usage limits', 'The window still reads ' + Math.round(reopened.percent) + ' per cent after ' + attempt + ' checks. The plan is saved; pick it up when you are ready.');
|
|
237
287
|
finish(state, record, 'gave-up', 'window still at ' + Math.round(reopened.percent) + '%', now);
|
|
238
288
|
return { outcome: 'gave-up' };
|
|
239
289
|
}
|
|
240
|
-
const again =
|
|
290
|
+
const again = deps.arm({
|
|
241
291
|
now,
|
|
242
292
|
sessionId: record.id,
|
|
243
293
|
cwd: record.cwd,
|
|
@@ -257,6 +307,65 @@ async function run(now, argv) {
|
|
|
257
307
|
return { outcome: 'rescheduled', attempt };
|
|
258
308
|
}
|
|
259
309
|
|
|
310
|
+
// THE PREFLIGHT.
|
|
311
|
+
//
|
|
312
|
+
// The retry above is the right shape but the wrong budget for the failure it
|
|
313
|
+
// was written for. A relay woke at 04:25:01 and said "SSL certificate
|
|
314
|
+
// hostname mismatch" 1.5 seconds later - a laptop whose wifi is off, with
|
|
315
|
+
// something answering the handshake in the endpoint's place. Three retries
|
|
316
|
+
// five minutes apart cover fifteen minutes. A router that is off overnight is
|
|
317
|
+
// off for hours, and at the end of those fifteen minutes the relay was spent.
|
|
318
|
+
//
|
|
319
|
+
// So: ask first, before spending a launch on it, and give being offline its
|
|
320
|
+
// own much longer budget. A machine that cannot reach the API has not failed.
|
|
321
|
+
// It is waiting, and waiting is free.
|
|
322
|
+
const link = await net.reachable({ timeoutMs: 8000 });
|
|
323
|
+
if (!link.online) {
|
|
324
|
+
const offlineAttempt = (record.offlineAttempt || 0) + 1;
|
|
325
|
+
if (offlineAttempt <= config.offlineAttempts) {
|
|
326
|
+
const wait = net.backoffMinutes(offlineAttempt - 1, config.offlineRetryMinutes);
|
|
327
|
+
const again = deps.arm({
|
|
328
|
+
now,
|
|
329
|
+
sessionId: record.id,
|
|
330
|
+
cwd: record.cwd,
|
|
331
|
+
hostName: record.host,
|
|
332
|
+
project: record.project,
|
|
333
|
+
resetsAt: now + wait * MINUTE,
|
|
334
|
+
binding: { percentUsed: reopened.known ? reopened.percent : record.percentAtArming, resetsAt: now + wait * MINUTE },
|
|
335
|
+
work: Object.assign({ hasWork: true, pending: 1, source: null, todos: [] }, record.work || {}),
|
|
336
|
+
// graceMinutes is already inside `wait`; adding it again would compound
|
|
337
|
+
// the backoff every round until the retries were hours apart.
|
|
338
|
+
config: Object.assign({}, config, { graceMinutes: 0, armOn: 'threshold' }),
|
|
339
|
+
});
|
|
340
|
+
if (again.ok) {
|
|
341
|
+
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;
|
|
346
|
+
relay.write(held);
|
|
347
|
+
}
|
|
348
|
+
// Said once, on the first miss, then quiet. Twelve toasts through the
|
|
349
|
+
// night is how a useful notification becomes something to turn off.
|
|
350
|
+
if (offlineAttempt === 1) {
|
|
351
|
+
deps.toast(
|
|
352
|
+
'Usage limits: waiting for the network',
|
|
353
|
+
link.reason === 'intercepted'
|
|
354
|
+
? 'The window reset, but something is answering in the API\'s place - a captive portal, a VPN or a proxy. Holding the plan and retrying.'
|
|
355
|
+
: 'The window reset, but this machine has no route to the API. Holding the plan and retrying.'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
relay.note('wake ' + record.id + ': offline (' + link.reason + '), retry ' + offlineAttempt + ' in ' + wait + 'm', now);
|
|
359
|
+
return { outcome: 'offline', reason: link.reason, attempt: offlineAttempt, retryInMinutes: wait };
|
|
360
|
+
}
|
|
361
|
+
relay.note('wake ' + record.id + ': offline and could not book a retry: ' + again.error, now);
|
|
362
|
+
}
|
|
363
|
+
deps.toast('Usage limits: still offline',
|
|
364
|
+
'No route to the API after ' + (record.offlineAttempt || 0) + ' tries. The plan is saved: claude --resume ' + record.id.slice(0, 8));
|
|
365
|
+
finish(state, record, 'offline', link.reason + ' - ' + link.detail, now);
|
|
366
|
+
return { outcome: 'offline-gave-up', detail: link.detail };
|
|
367
|
+
}
|
|
368
|
+
|
|
260
369
|
const continuation = relay.readContinuation(record.id);
|
|
261
370
|
const prompt = relay.compose({
|
|
262
371
|
continuation,
|
|
@@ -266,14 +375,14 @@ async function run(now, argv) {
|
|
|
266
375
|
});
|
|
267
376
|
|
|
268
377
|
let mode = config.mode;
|
|
269
|
-
const presence = userIsPresent(config.mode === 'resume' ? capabilities.computerUse : null);
|
|
378
|
+
const presence = deps.userIsPresent(config.mode === 'resume' ? capabilities.computerUse : null);
|
|
270
379
|
if (mode === 'resume' && presence.known && presence.present && config.whenBusy === 'notify') {
|
|
271
380
|
mode = 'notify';
|
|
272
381
|
relay.note('wake ' + record.id + ': someone is at the machine, leaving a note instead', now);
|
|
273
382
|
}
|
|
274
383
|
|
|
275
384
|
if (mode === 'notify') {
|
|
276
|
-
toast(
|
|
385
|
+
deps.toast(
|
|
277
386
|
'Usage limits: the window has reset',
|
|
278
387
|
'The plan for ' + (record.project || path.basename(record.cwd)) + ' is ready to pick up. Run: claude --resume ' + record.id.slice(0, 8)
|
|
279
388
|
);
|
|
@@ -283,21 +392,113 @@ async function run(now, argv) {
|
|
|
283
392
|
|
|
284
393
|
const cli = record.host === host.CODEX ? capabilities.codex : capabilities.claude;
|
|
285
394
|
if (!cli) {
|
|
286
|
-
toast('Usage limits', 'The window has reset but the ' + record.host + ' CLI could not be found, so the plan was left on disk.');
|
|
395
|
+
deps.toast('Usage limits', 'The window has reset but the ' + record.host + ' CLI could not be found, so the plan was left on disk.');
|
|
287
396
|
finish(state, record, 'no-cli', null, now);
|
|
288
397
|
return { outcome: 'no-cli' };
|
|
289
398
|
}
|
|
290
399
|
|
|
291
|
-
toast('Usage limits: resuming', 'Carrying on with ' + (record.project || path.basename(record.cwd)) + ' where the limit stopped it.');
|
|
292
|
-
const
|
|
400
|
+
deps.toast('Usage limits: resuming', 'Carrying on with ' + (record.project || path.basename(record.cwd)) + ' where the limit stopped it.');
|
|
401
|
+
const runs = [];
|
|
402
|
+
const delivered = record.host === host.CODEX
|
|
403
|
+
? deps.deliverCodex(record, prompt, cli, runs)
|
|
404
|
+
: deps.deliverClaude(record, prompt, config, cli, runs);
|
|
405
|
+
const logPath = config.runLog === false ? null : relay.writeRunLog(relay.runLogFile(record.id, now), runs.join('\n' + '\n'));
|
|
293
406
|
if (delivered.ok) {
|
|
294
|
-
toast('Usage limits: done', 'The resumed run finished. Open the session to read it.');
|
|
295
|
-
finish(state, record, 'resumed', delivered.how, now);
|
|
296
|
-
return { outcome: 'resumed', how: delivered.how };
|
|
407
|
+
deps.toast('Usage limits: done', 'The resumed run finished. Open the session to read it.');
|
|
408
|
+
finish(state, record, 'resumed', delivered.how + (logPath ? ' - log at ' + logPath : ''), now);
|
|
409
|
+
return { outcome: 'resumed', how: delivered.how, log: logPath };
|
|
410
|
+
}
|
|
411
|
+
// A launch that failed is not the same as a job that cannot be done, and
|
|
412
|
+
// until now it was treated as one.
|
|
413
|
+
//
|
|
414
|
+
// `attempts` is 3 by default, but the only path that ever counted an attempt
|
|
415
|
+
// was the one above, for a window that had not really reset. A failure to
|
|
416
|
+
// LAUNCH went straight to finish() and the relay was over - permanently,
|
|
417
|
+
// after a single try, hours later, with nobody awake to see it.
|
|
418
|
+
//
|
|
419
|
+
// Measured on this machine on 2026-09-14: a relay armed at 94 per cent woke
|
|
420
|
+
// at 04:25:01 and reported "Unable to connect to API: SSL certificate
|
|
421
|
+
// hostname mismatch" 1.5 seconds later, with attempt still 0. That is what a
|
|
422
|
+
// machine that has just woken looks like before its network is up. The work
|
|
423
|
+
// was saved and never picked up, which is the one outcome this whole feature
|
|
424
|
+
// exists to prevent.
|
|
425
|
+
//
|
|
426
|
+
// So a failed delivery now re-arms, up to the same attempt budget. No attempt
|
|
427
|
+
// is made to sort transient errors from permanent ones: the cost of retrying
|
|
428
|
+
// a permanent failure is one more launch and a later toast, and the cost of
|
|
429
|
+
// not retrying a transient one is the entire night's work.
|
|
430
|
+
const attempt = (record.attempt || 0) + 1;
|
|
431
|
+
if (attempt < config.attempts) {
|
|
432
|
+
// `resetsAt: now` means "treat this moment as the reset", so wakeAt() adds
|
|
433
|
+
// the usual grace and books the next try that far out.
|
|
434
|
+
const again = deps.arm({
|
|
435
|
+
now,
|
|
436
|
+
sessionId: record.id,
|
|
437
|
+
cwd: record.cwd,
|
|
438
|
+
hostName: record.host,
|
|
439
|
+
resetsAt: now,
|
|
440
|
+
binding: { percentUsed: reopened.known ? reopened.percent : null, resetsAt: now },
|
|
441
|
+
work: Object.assign({ hasWork: true, pending: 1, source: null, todos: [] }, record.work || {}),
|
|
442
|
+
config,
|
|
443
|
+
});
|
|
444
|
+
if (again.ok) {
|
|
445
|
+
const held = relay.read();
|
|
446
|
+
held.armed.attempt = attempt;
|
|
447
|
+
held.armed.continuation = record.continuation;
|
|
448
|
+
relay.write(held);
|
|
449
|
+
deps.toast(
|
|
450
|
+
'Usage limits: retrying',
|
|
451
|
+
'The resume could not start (' + (delivered.error || 'unknown error') + '). Trying again in ' +
|
|
452
|
+
config.graceMinutes + ' minutes.'
|
|
453
|
+
);
|
|
454
|
+
relay.note('wake ' + record.id + ': ' + delivered.error + ', retry ' + attempt + ' in ' + config.graceMinutes + 'm', now);
|
|
455
|
+
return { outcome: 'retrying', attempt, error: delivered.error };
|
|
456
|
+
}
|
|
457
|
+
// Could not even book the retry; fall through and say so plainly.
|
|
458
|
+
relay.note('wake ' + record.id + ': retry could not be scheduled: ' + again.error, now);
|
|
459
|
+
}
|
|
460
|
+
// Out of the short retries. Classification earns its place here, where the
|
|
461
|
+
// question is no longer "retry or not" but "how long is it worth waiting",
|
|
462
|
+
// and what to tell somebody who is asleep.
|
|
463
|
+
//
|
|
464
|
+
// wait the network or the service. Another window is cheap.
|
|
465
|
+
// permanent a key, a login, a missing binary. Another window changes
|
|
466
|
+
// nothing, and saying "retrying" would be a lie.
|
|
467
|
+
const verdict = net.classify((delivered.error || '') + ' ' + runs.join(' '));
|
|
468
|
+
const rearms = record.rearms || 0;
|
|
469
|
+
if (config.onFailure === 'rearm' && verdict.kind !== 'permanent' && rearms < config.maxRearms) {
|
|
470
|
+
// A whole window later, not five more minutes: whatever this is, time is
|
|
471
|
+
// the only variable left worth changing.
|
|
472
|
+
const nextWindow = deps.arm({
|
|
473
|
+
now,
|
|
474
|
+
sessionId: record.id,
|
|
475
|
+
cwd: record.cwd,
|
|
476
|
+
hostName: record.host,
|
|
477
|
+
project: record.project,
|
|
478
|
+
resetsAt: now + 5 * 60 * MINUTE,
|
|
479
|
+
binding: { percentUsed: record.percentAtArming, resetsAt: now + 5 * 60 * MINUTE },
|
|
480
|
+
work: Object.assign({ hasWork: true, pending: 1, source: null, todos: [] }, record.work || {}),
|
|
481
|
+
config: Object.assign({}, config, { armOn: 'threshold' }),
|
|
482
|
+
});
|
|
483
|
+
if (nextWindow.ok) {
|
|
484
|
+
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;
|
|
490
|
+
relay.write(held);
|
|
491
|
+
}
|
|
492
|
+
deps.toast('Usage limits: could not resume',
|
|
493
|
+
(delivered.error || 'the run failed') + ' - ' + verdict.why + '. Armed again for the next window.' + (logPath ? ' See: relay log --run' : ''));
|
|
494
|
+
relay.note('wake ' + record.id + ': failed (' + verdict.kind + '), armed again for the next window (' + (rearms + 1) + ' of ' + config.maxRearms + ')', now);
|
|
495
|
+
return { outcome: 'rearmed', error: delivered.error, why: verdict.why, rearms: rearms + 1, log: logPath };
|
|
496
|
+
}
|
|
297
497
|
}
|
|
298
|
-
toast('Usage limits: could not resume',
|
|
299
|
-
|
|
300
|
-
|
|
498
|
+
deps.toast('Usage limits: could not resume',
|
|
499
|
+
(delivered.error || 'The plan is still on disk.') + (verdict.kind === 'permanent' ? ' This will not fix itself: ' + verdict.why + '.' : '') + (logPath ? ' See: relay log --run' : ''));
|
|
500
|
+
finish(state, record, 'failed', (delivered.error || '') + ' [' + verdict.kind + ']' + (logPath ? ' - log at ' + logPath : ''), now);
|
|
501
|
+
return { outcome: 'failed', error: delivered.error, attempts: attempt, kind: verdict.kind, log: logPath };
|
|
301
502
|
}
|
|
302
503
|
|
|
303
504
|
if (require.main === module) {
|
|
@@ -313,4 +514,4 @@ if (require.main === module) {
|
|
|
313
514
|
);
|
|
314
515
|
}
|
|
315
516
|
|
|
316
|
-
module.exports = { run, toast, userIsPresent, claudeArgs, deliverClaude, deliverCodex, windowReopened, argOf };
|
|
517
|
+
module.exports = { run, toast, userIsPresent, claudeArgs, deliverClaude, deliverCodex, windowReopened, argOf, appendRun, spawnOptionsFor };
|