claude-overnight 1.2.2 → 1.4.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 +5 -2
- package/dist/index.js +1 -1
- package/dist/planner.js +37 -8
- package/dist/swarm.d.ts +0 -1
- package/dist/swarm.js +97 -46
- package/dist/types.d.ts +5 -0
- package/dist/types.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,7 +70,9 @@ An orchestrator agent reads all design documents and synthesizes concrete execut
|
|
|
70
70
|
|
|
71
71
|
### 3. Iterative execution
|
|
72
72
|
|
|
73
|
-
Tasks run in parallel (each agent in its own git worktree). After
|
|
73
|
+
Tasks run in parallel (each agent in its own git worktree). After completing its task, each agent automatically runs a **simplify pass** — reviewing its own `git diff` for code reuse opportunities, quality issues, and inefficiencies, then fixing them before the framework commits.
|
|
74
|
+
|
|
75
|
+
After each wave, steering assesses: "how good is this?" — not "what's missing?" It can:
|
|
74
76
|
|
|
75
77
|
- **Execute** more tasks to build features, fix bugs, polish UX
|
|
76
78
|
- **Reflect** by spinning up 1-2 review agents for deep quality/architecture audits
|
|
@@ -178,7 +180,7 @@ claude-overnight "fix auth bug in src/auth.ts" "add tests for user model"
|
|
|
178
180
|
| `--usage-cap=N` | unlimited | Stop at N% utilization |
|
|
179
181
|
| `--allow-extra-usage` | off | Allow extra/overage usage (billed separately) |
|
|
180
182
|
| `--extra-usage-budget=N` | — | Max $ for extra usage (implies --allow-extra-usage) |
|
|
181
|
-
| `--timeout=SECONDS` | `
|
|
183
|
+
| `--timeout=SECONDS` | `900` | Inactivity timeout per agent (nudges at timeout, kills at 2×) |
|
|
182
184
|
| `--no-flex` | — | Disable multi-wave steering |
|
|
183
185
|
| `--dry-run` | — | Show planned tasks without running |
|
|
184
186
|
|
|
@@ -226,6 +228,7 @@ The usage bar cycles through all rate limit windows (5h, 7d, etc.) every 3 secon
|
|
|
226
228
|
Built for unattended runs lasting hours or days.
|
|
227
229
|
|
|
228
230
|
- **Smooth overage transition**: when extra usage is allowed, plan limit rejection is seamless — no dispatch blocking, agents continue into overage
|
|
231
|
+
- **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
|
|
229
232
|
- **Hard block**: pauses until the rate limit window resets, then resumes
|
|
230
233
|
- **Soft throttle**: slows dispatch at >75% utilization
|
|
231
234
|
- **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,16 @@ 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";
|
|
7
|
+
const SIMPLIFY_PROMPT = `You just finished your task. Now review and simplify your changes.
|
|
8
|
+
|
|
9
|
+
Run \`git diff\` to see what you changed, then fix any issues:
|
|
10
|
+
|
|
11
|
+
1. **Reuse**: Search the codebase — did you write something that already exists? Use existing utilities, helpers, patterns instead.
|
|
12
|
+
2. **Quality**: Redundant state, copy-paste with slight variation, leaky abstractions, unnecessary wrappers/nesting, comments that narrate what the code does? Delete them.
|
|
13
|
+
3. **Efficiency**: Redundant computations, sequential operations that could be parallel, unnecessary existence checks before operations, unbounded data structures, missing cleanup?
|
|
14
|
+
|
|
15
|
+
Less code is better. Delete and simplify rather than add. Fix directly — no need to explain.`;
|
|
6
16
|
export class Swarm {
|
|
7
17
|
agents = [];
|
|
8
18
|
logs = [];
|
|
@@ -20,7 +30,6 @@ export class Swarm {
|
|
|
20
30
|
mergeResults = [];
|
|
21
31
|
// Rate limit tracking for auto-concurrency
|
|
22
32
|
rateLimitUtilization = 0;
|
|
23
|
-
rateLimitStatus = "";
|
|
24
33
|
rateLimitResetsAt;
|
|
25
34
|
/** Per-window rate limit snapshots (updated on every rate_limit_event). */
|
|
26
35
|
rateLimitWindows = new Map();
|
|
@@ -212,7 +221,7 @@ export class Swarm {
|
|
|
212
221
|
this.log(id, `Starting: ${task.prompt.slice(0, 60)}`);
|
|
213
222
|
const maxRetries = this.config.maxRetries ?? 2;
|
|
214
223
|
// Inactivity timeout: kill agent only if it goes silent (no messages)
|
|
215
|
-
const inactivityMs = this.config.agentTimeoutMs ??
|
|
224
|
+
const inactivityMs = this.config.agentTimeoutMs ?? 15 * 60 * 1000;
|
|
216
225
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
217
226
|
if (attempt > 0) {
|
|
218
227
|
const backoffMs = Math.min(30000, 1000 * 2 ** (attempt - 1)) * (0.5 + Math.random());
|
|
@@ -224,51 +233,94 @@ export class Swarm {
|
|
|
224
233
|
}
|
|
225
234
|
try {
|
|
226
235
|
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
|
-
|
|
263
|
-
|
|
236
|
+
let resumeSessionId;
|
|
237
|
+
let resumePrompt = "Continue. Complete the task.";
|
|
238
|
+
const runOnce = async (isResume) => {
|
|
239
|
+
const agentPrompt = isResume
|
|
240
|
+
? resumePrompt
|
|
241
|
+
: this.config.useWorktrees
|
|
242
|
+
? `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}`
|
|
243
|
+
: task.prompt;
|
|
244
|
+
const agentQuery = query({
|
|
245
|
+
prompt: agentPrompt,
|
|
246
|
+
options: {
|
|
247
|
+
cwd: agentCwd,
|
|
248
|
+
model: task.model || this.config.model,
|
|
249
|
+
permissionMode: perm,
|
|
250
|
+
...(perm === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
|
|
251
|
+
allowedTools: this.config.allowedTools,
|
|
252
|
+
includePartialMessages: true,
|
|
253
|
+
persistSession: true,
|
|
254
|
+
...(isResume && resumeSessionId && { resume: resumeSessionId }),
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const timeoutMs = isResume ? inactivityMs * 2 : inactivityMs;
|
|
258
|
+
let sessionId;
|
|
259
|
+
let lastActivity = Date.now();
|
|
260
|
+
let timer;
|
|
261
|
+
const watchdog = new Promise((_, reject) => {
|
|
262
|
+
const check = () => {
|
|
263
|
+
const silent = Date.now() - lastActivity;
|
|
264
|
+
if (silent >= timeoutMs) {
|
|
265
|
+
agentQuery.interrupt().catch(() => agentQuery.close());
|
|
266
|
+
if (isResume) {
|
|
267
|
+
reject(new AgentTimeoutError(silent));
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
reject(new NudgeError(sessionId, silent));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
timer = setTimeout(check, Math.min(30_000, timeoutMs - silent + 1000));
|
|
264
275
|
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
};
|
|
277
|
+
timer = setTimeout(check, timeoutMs);
|
|
278
|
+
});
|
|
279
|
+
this.activeQueries.add(agentQuery);
|
|
280
|
+
try {
|
|
281
|
+
await Promise.race([
|
|
282
|
+
(async () => {
|
|
283
|
+
for await (const msg of agentQuery) {
|
|
284
|
+
lastActivity = Date.now();
|
|
285
|
+
if (!sessionId && "session_id" in msg) {
|
|
286
|
+
sessionId = msg.session_id;
|
|
287
|
+
}
|
|
288
|
+
this.handleMsg(agent, msg);
|
|
289
|
+
}
|
|
290
|
+
})(),
|
|
291
|
+
watchdog,
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
this.activeQueries.delete(agentQuery);
|
|
297
|
+
if (sessionId)
|
|
298
|
+
resumeSessionId = sessionId;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
// Run with nudge: first attempt can be interrupted and resumed
|
|
302
|
+
try {
|
|
303
|
+
await runOnce(false);
|
|
304
|
+
}
|
|
305
|
+
catch (nudgeErr) {
|
|
306
|
+
if (nudgeErr instanceof NudgeError && resumeSessionId) {
|
|
307
|
+
this.log(id, `Silent ${Math.round(inactivityMs / 60000)}m — resuming with continue`);
|
|
308
|
+
await runOnce(true);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
throw nudgeErr;
|
|
312
|
+
}
|
|
268
313
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
314
|
+
// Simplify pass: resume session with review prompt
|
|
315
|
+
if (resumeSessionId && agent.status === "running") {
|
|
316
|
+
try {
|
|
317
|
+
this.log(id, "Simplify pass");
|
|
318
|
+
resumePrompt = SIMPLIFY_PROMPT;
|
|
319
|
+
await runOnce(true);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
this.log(id, "Simplify pass skipped");
|
|
323
|
+
}
|
|
272
324
|
}
|
|
273
325
|
if (agent.status === "running") {
|
|
274
326
|
agent.finishedAt = Date.now();
|
|
@@ -658,7 +710,6 @@ export class Swarm {
|
|
|
658
710
|
const rl = msg;
|
|
659
711
|
const info = rl.rate_limit_info;
|
|
660
712
|
this.rateLimitUtilization = info.utilization ?? 0;
|
|
661
|
-
this.rateLimitStatus = info.status;
|
|
662
713
|
if (info.status === "rejected" && info.resetsAt && !this.allowExtraUsage) {
|
|
663
714
|
// Only block dispatch on rejection if extra usage is NOT allowed.
|
|
664
715
|
// When extra usage is allowed, rejection is just the plan→overage transition.
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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": {
|