claude-overnight 1.2.1 → 1.3.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/README.md +3 -1
- package/dist/index.js +1 -1
- package/dist/planner.js +37 -8
- package/dist/swarm.d.ts +0 -1
- package/dist/swarm.js +86 -48
- package/dist/types.d.ts +5 -0
- package/dist/types.js +9 -1
- package/dist/ui.js +8 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ claude-overnight "fix auth bug in src/auth.ts" "add tests for user model"
|
|
|
178
178
|
| `--usage-cap=N` | unlimited | Stop at N% utilization |
|
|
179
179
|
| `--allow-extra-usage` | off | Allow extra/overage usage (billed separately) |
|
|
180
180
|
| `--extra-usage-budget=N` | — | Max $ for extra usage (implies --allow-extra-usage) |
|
|
181
|
-
| `--timeout=SECONDS` | `
|
|
181
|
+
| `--timeout=SECONDS` | `900` | Inactivity timeout per agent (nudges at timeout, kills at 2×) |
|
|
182
182
|
| `--no-flex` | — | Disable multi-wave steering |
|
|
183
183
|
| `--dry-run` | — | Show planned tasks without running |
|
|
184
184
|
|
|
@@ -225,6 +225,8 @@ The usage bar cycles through all rate limit windows (5h, 7d, etc.) every 3 secon
|
|
|
225
225
|
|
|
226
226
|
Built for unattended runs lasting hours or days.
|
|
227
227
|
|
|
228
|
+
- **Smooth overage transition**: when extra usage is allowed, plan limit rejection is seamless — no dispatch blocking, agents continue into overage
|
|
229
|
+
- **Interrupt + resume**: agents and planner queries that go silent are interrupted and resumed with full conversation context via SDK session resume — not killed and restarted from scratch
|
|
228
230
|
- **Hard block**: pauses until the rate limit window resets, then resumes
|
|
229
231
|
- **Soft throttle**: slows dispatch at >75% utilization
|
|
230
232
|
- **Extra usage guard**: detects overage billing and stops unless explicitly allowed
|
package/dist/index.js
CHANGED
|
@@ -525,7 +525,7 @@ async function main() {
|
|
|
525
525
|
--usage-cap=N Stop at N% utilization ${chalk.dim("(e.g. 90 to save 10% for other work)")}
|
|
526
526
|
--allow-extra-usage Allow extra/overage usage ${chalk.dim("(default: stop when plan limits hit)")}
|
|
527
527
|
--extra-usage-budget=N Max $ for extra usage ${chalk.dim("(implies --allow-extra-usage)")}
|
|
528
|
-
--timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default:
|
|
528
|
+
--timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default: 900s, nudges at timeout, kills at 2×)")}
|
|
529
529
|
--no-flex Disable adaptive multi-wave planning ${chalk.dim("(run all tasks in one shot)")}
|
|
530
530
|
|
|
531
531
|
${chalk.cyan("Defaults")} ${chalk.dim("(non-interactive)")}
|
package/dist/planner.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
|
-
|
|
3
|
+
import { NudgeError } from "./types.js";
|
|
4
|
+
const NUDGE_MS = 15 * 60 * 1000; // 15 min — close & restart with "continue"
|
|
5
|
+
const HARD_TIMEOUT_MS = 30 * 60 * 1000; // 30 min — give up
|
|
4
6
|
export function detectModelTier(model) {
|
|
5
7
|
const m = model.toLowerCase();
|
|
6
8
|
if (m === "default" || m.includes("opus"))
|
|
@@ -155,11 +157,25 @@ function isRateLimitError(err) {
|
|
|
155
157
|
async function runPlannerQuery(prompt, opts, onLog) {
|
|
156
158
|
const MAX_RETRIES = 3;
|
|
157
159
|
const BACKOFF = [30_000, 60_000, 120_000];
|
|
160
|
+
let currentPrompt = prompt;
|
|
161
|
+
let currentOpts = opts;
|
|
158
162
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
159
163
|
try {
|
|
160
|
-
return await runPlannerQueryOnce(
|
|
164
|
+
return await runPlannerQueryOnce(currentPrompt, currentOpts, onLog);
|
|
161
165
|
}
|
|
162
166
|
catch (err) {
|
|
167
|
+
if (err instanceof NudgeError) {
|
|
168
|
+
if (err.sessionId) {
|
|
169
|
+
onLog("Silent 15m — resuming session with continue");
|
|
170
|
+
currentPrompt = "Continue. Complete the task.";
|
|
171
|
+
currentOpts = { ...opts, resumeSessionId: err.sessionId };
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
onLog("Silent 15m — restarting planner (no session to resume)");
|
|
175
|
+
// No session captured, just retry from scratch
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
163
179
|
if (attempt < MAX_RETRIES && isRateLimitError(err)) {
|
|
164
180
|
const waitMs = BACKOFF[attempt];
|
|
165
181
|
const waitSec = Math.round(waitMs / 1000);
|
|
@@ -181,6 +197,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
181
197
|
_plannerRateLimitInfo = { utilization: 0, status: "", isUsingOverage: false, windows: new Map(), costUsd: 0 };
|
|
182
198
|
let resultText = "";
|
|
183
199
|
const startedAt = Date.now();
|
|
200
|
+
const isResume = !!opts.resumeSessionId;
|
|
184
201
|
const pq = query({
|
|
185
202
|
prompt,
|
|
186
203
|
options: {
|
|
@@ -190,8 +207,9 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
190
207
|
allowedTools: ["Read", "Glob", "Grep", "Write"],
|
|
191
208
|
permissionMode: opts.permissionMode,
|
|
192
209
|
...(opts.permissionMode === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
|
|
193
|
-
persistSession:
|
|
210
|
+
persistSession: true, // needed for interrupt+resume
|
|
194
211
|
includePartialMessages: true,
|
|
212
|
+
...(isResume && { resume: opts.resumeSessionId }),
|
|
195
213
|
},
|
|
196
214
|
});
|
|
197
215
|
// Progress ticker — fast updates with compact format
|
|
@@ -210,23 +228,34 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
210
228
|
const extra = lastLogText ? ` · ${lastLogText}` : "";
|
|
211
229
|
onLog(`${timeStr}${toolStr}${costStr}${rlStr}${extra}`);
|
|
212
230
|
}, 500);
|
|
231
|
+
const timeoutMs = isResume ? HARD_TIMEOUT_MS : NUDGE_MS;
|
|
232
|
+
let sessionId;
|
|
213
233
|
let lastActivity = Date.now();
|
|
214
234
|
let timer;
|
|
215
235
|
const watchdog = new Promise((_, reject) => {
|
|
216
236
|
const check = () => {
|
|
217
237
|
const silent = Date.now() - lastActivity;
|
|
218
|
-
if (silent >=
|
|
219
|
-
|
|
220
|
-
|
|
238
|
+
if (silent >= timeoutMs) {
|
|
239
|
+
// Try interrupt (graceful), fall back to close (hard kill)
|
|
240
|
+
pq.interrupt().catch(() => pq.close());
|
|
241
|
+
if (isResume) {
|
|
242
|
+
reject(new Error(`Planner silent for ${Math.round(silent / 1000)}s — assumed hung`));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
reject(new NudgeError(sessionId, silent));
|
|
246
|
+
}
|
|
221
247
|
}
|
|
222
248
|
else
|
|
223
|
-
timer = setTimeout(check, Math.min(30_000,
|
|
249
|
+
timer = setTimeout(check, Math.min(30_000, timeoutMs - silent + 1000));
|
|
224
250
|
};
|
|
225
|
-
timer = setTimeout(check,
|
|
251
|
+
timer = setTimeout(check, timeoutMs);
|
|
226
252
|
});
|
|
227
253
|
const consume = async () => {
|
|
228
254
|
for await (const msg of pq) {
|
|
229
255
|
lastActivity = Date.now();
|
|
256
|
+
if (!sessionId && "session_id" in msg) {
|
|
257
|
+
sessionId = msg.session_id;
|
|
258
|
+
}
|
|
230
259
|
if (msg.type === "stream_event") {
|
|
231
260
|
const ev = msg.event;
|
|
232
261
|
if (ev?.type === "content_block_start" && ev.content_block?.type === "tool_use") {
|
package/dist/swarm.d.ts
CHANGED
|
@@ -44,7 +44,6 @@ export declare class Swarm {
|
|
|
44
44
|
cappedOut: boolean;
|
|
45
45
|
mergeResults: MergeResult[];
|
|
46
46
|
rateLimitUtilization: number;
|
|
47
|
-
rateLimitStatus: string;
|
|
48
47
|
rateLimitResetsAt?: number;
|
|
49
48
|
/** Per-window rate limit snapshots (updated on every rate_limit_event). */
|
|
50
49
|
rateLimitWindows: Map<string, RateLimitWindow>;
|
package/dist/swarm.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
5
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
+
import { NudgeError } from "./types.js";
|
|
6
7
|
export class Swarm {
|
|
7
8
|
agents = [];
|
|
8
9
|
logs = [];
|
|
@@ -20,7 +21,6 @@ export class Swarm {
|
|
|
20
21
|
mergeResults = [];
|
|
21
22
|
// Rate limit tracking for auto-concurrency
|
|
22
23
|
rateLimitUtilization = 0;
|
|
23
|
-
rateLimitStatus = "";
|
|
24
24
|
rateLimitResetsAt;
|
|
25
25
|
/** Per-window rate limit snapshots (updated on every rate_limit_event). */
|
|
26
26
|
rateLimitWindows = new Map();
|
|
@@ -212,7 +212,7 @@ export class Swarm {
|
|
|
212
212
|
this.log(id, `Starting: ${task.prompt.slice(0, 60)}`);
|
|
213
213
|
const maxRetries = this.config.maxRetries ?? 2;
|
|
214
214
|
// Inactivity timeout: kill agent only if it goes silent (no messages)
|
|
215
|
-
const inactivityMs = this.config.agentTimeoutMs ??
|
|
215
|
+
const inactivityMs = this.config.agentTimeoutMs ?? 15 * 60 * 1000;
|
|
216
216
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
217
217
|
if (attempt > 0) {
|
|
218
218
|
const backoffMs = Math.min(30000, 1000 * 2 ** (attempt - 1)) * (0.5 + Math.random());
|
|
@@ -224,51 +224,82 @@ export class Swarm {
|
|
|
224
224
|
}
|
|
225
225
|
try {
|
|
226
226
|
const perm = this.config.permissionMode ?? "auto";
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
lastActivity = Date.now();
|
|
263
|
-
this.handleMsg(agent, msg);
|
|
227
|
+
let resumeSessionId;
|
|
228
|
+
const runOnce = async (isResume) => {
|
|
229
|
+
const agentPrompt = isResume
|
|
230
|
+
? "Continue. Complete the task."
|
|
231
|
+
: this.config.useWorktrees
|
|
232
|
+
? `You are working in an isolated git worktree. Focus only on this task. Do NOT commit your changes — the framework handles that.\n\n${task.prompt}`
|
|
233
|
+
: task.prompt;
|
|
234
|
+
const agentQuery = query({
|
|
235
|
+
prompt: agentPrompt,
|
|
236
|
+
options: {
|
|
237
|
+
cwd: agentCwd,
|
|
238
|
+
model: task.model || this.config.model,
|
|
239
|
+
permissionMode: perm,
|
|
240
|
+
...(perm === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
|
|
241
|
+
allowedTools: this.config.allowedTools,
|
|
242
|
+
includePartialMessages: true,
|
|
243
|
+
persistSession: true,
|
|
244
|
+
...(isResume && resumeSessionId && { resume: resumeSessionId }),
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
const timeoutMs = isResume ? inactivityMs * 2 : inactivityMs;
|
|
248
|
+
let sessionId;
|
|
249
|
+
let lastActivity = Date.now();
|
|
250
|
+
let timer;
|
|
251
|
+
const watchdog = new Promise((_, reject) => {
|
|
252
|
+
const check = () => {
|
|
253
|
+
const silent = Date.now() - lastActivity;
|
|
254
|
+
if (silent >= timeoutMs) {
|
|
255
|
+
agentQuery.interrupt().catch(() => agentQuery.close());
|
|
256
|
+
if (isResume) {
|
|
257
|
+
reject(new AgentTimeoutError(silent));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
reject(new NudgeError(sessionId, silent));
|
|
261
|
+
}
|
|
264
262
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
else {
|
|
264
|
+
timer = setTimeout(check, Math.min(30_000, timeoutMs - silent + 1000));
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
timer = setTimeout(check, timeoutMs);
|
|
268
|
+
});
|
|
269
|
+
this.activeQueries.add(agentQuery);
|
|
270
|
+
try {
|
|
271
|
+
await Promise.race([
|
|
272
|
+
(async () => {
|
|
273
|
+
for await (const msg of agentQuery) {
|
|
274
|
+
lastActivity = Date.now();
|
|
275
|
+
if (!sessionId && "session_id" in msg) {
|
|
276
|
+
sessionId = msg.session_id;
|
|
277
|
+
}
|
|
278
|
+
this.handleMsg(agent, msg);
|
|
279
|
+
}
|
|
280
|
+
})(),
|
|
281
|
+
watchdog,
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
clearTimeout(timer);
|
|
286
|
+
this.activeQueries.delete(agentQuery);
|
|
287
|
+
if (sessionId)
|
|
288
|
+
resumeSessionId = sessionId;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
// Run with nudge: first attempt can be interrupted and resumed
|
|
292
|
+
try {
|
|
293
|
+
await runOnce(false);
|
|
268
294
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
catch (nudgeErr) {
|
|
296
|
+
if (nudgeErr instanceof NudgeError && resumeSessionId) {
|
|
297
|
+
this.log(id, `Silent ${Math.round(inactivityMs / 60000)}m — resuming with continue`);
|
|
298
|
+
await runOnce(true);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
throw nudgeErr;
|
|
302
|
+
}
|
|
272
303
|
}
|
|
273
304
|
if (agent.status === "running") {
|
|
274
305
|
agent.finishedAt = Date.now();
|
|
@@ -658,10 +689,14 @@ export class Swarm {
|
|
|
658
689
|
const rl = msg;
|
|
659
690
|
const info = rl.rate_limit_info;
|
|
660
691
|
this.rateLimitUtilization = info.utilization ?? 0;
|
|
661
|
-
|
|
662
|
-
|
|
692
|
+
if (info.status === "rejected" && info.resetsAt && !this.allowExtraUsage) {
|
|
693
|
+
// Only block dispatch on rejection if extra usage is NOT allowed.
|
|
694
|
+
// When extra usage is allowed, rejection is just the plan→overage transition.
|
|
663
695
|
this.rateLimitResetsAt = info.resetsAt;
|
|
664
696
|
}
|
|
697
|
+
else if (info.status !== "rejected") {
|
|
698
|
+
this.rateLimitResetsAt = undefined;
|
|
699
|
+
}
|
|
665
700
|
// Track per-window state
|
|
666
701
|
const windowType = info.rateLimitType;
|
|
667
702
|
if (windowType) {
|
|
@@ -684,7 +719,10 @@ export class Swarm {
|
|
|
684
719
|
}
|
|
685
720
|
const pct = info.utilization != null ? `${Math.round(info.utilization * 100)}%` : "";
|
|
686
721
|
const overageTag = this.isUsingOverage ? " [EXTRA]" : "";
|
|
687
|
-
|
|
722
|
+
const statusLabel = info.status === "rejected" && this.allowExtraUsage
|
|
723
|
+
? "switching to extra usage"
|
|
724
|
+
: info.status;
|
|
725
|
+
this.log(agent.id, `Rate: ${statusLabel} ${pct}${overageTag}${windowType ? ` (${windowType})` : ""}`);
|
|
688
726
|
break;
|
|
689
727
|
}
|
|
690
728
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -110,6 +110,11 @@ export interface RateLimitWindow {
|
|
|
110
110
|
status: string;
|
|
111
111
|
resetsAt?: number;
|
|
112
112
|
}
|
|
113
|
+
/** Thrown when a query goes silent — carries session ID for interrupt+resume. */
|
|
114
|
+
export declare class NudgeError extends Error {
|
|
115
|
+
sessionId: string | undefined;
|
|
116
|
+
constructor(sessionId: string | undefined, silentMs: number);
|
|
117
|
+
}
|
|
113
118
|
/** Persisted run state for crash recovery and resume. */
|
|
114
119
|
export interface RunState {
|
|
115
120
|
id: string;
|
package/dist/types.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/** Thrown when a query goes silent — carries session ID for interrupt+resume. */
|
|
2
|
+
export class NudgeError extends Error {
|
|
3
|
+
sessionId;
|
|
4
|
+
constructor(sessionId, silentMs) {
|
|
5
|
+
super(`Silent for ${Math.round(silentMs / 1000)}s — nudging`);
|
|
6
|
+
this.sessionId = sessionId;
|
|
7
|
+
this.name = "NudgeError";
|
|
8
|
+
}
|
|
9
|
+
}
|
package/dist/ui.js
CHANGED
|
@@ -75,14 +75,18 @@ export function renderFrame(swarm, showHotkeys = false) {
|
|
|
75
75
|
label = chalk.yellow(`Capped at ${capFrac != null ? Math.round(capFrac * 100) : 100}% — finishing active`);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
else if (swarm.rateLimitResetsAt) {
|
|
79
|
-
const waitSec = Math.
|
|
78
|
+
else if (swarm.rateLimitResetsAt && swarm.rateLimitResetsAt > Date.now()) {
|
|
79
|
+
const waitSec = Math.ceil((swarm.rateLimitResetsAt - Date.now()) / 1000);
|
|
80
80
|
const mm = Math.floor(waitSec / 60);
|
|
81
81
|
const ss = waitSec % 60;
|
|
82
82
|
label = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
|
|
83
83
|
}
|
|
84
|
-
if (swarm.isUsingOverage && !swarm.cappedOut)
|
|
85
|
-
|
|
84
|
+
if (swarm.isUsingOverage && !swarm.cappedOut) {
|
|
85
|
+
const budgetInfo = swarm.extraUsageBudget != null
|
|
86
|
+
? ` $${swarm.overageCostUsd.toFixed(2)}/$${swarm.extraUsageBudget}`
|
|
87
|
+
: "";
|
|
88
|
+
label += chalk.red(` [EXTRA USAGE${budgetInfo}]`);
|
|
89
|
+
}
|
|
86
90
|
const prefix = windowLabel ? chalk.dim(windowLabel.padEnd(6)) : chalk.dim("Usage ");
|
|
87
91
|
out.push(` ${prefix}${barStr} ${label}`);
|
|
88
92
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Run 10, 100, or 1000 Claude agents overnight. Parallel autonomous AI coding with thinking waves, iterative quality steering, crash recovery, and rate limit handling. Built on the Claude Agent SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|