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 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 each wave, steering assesses: "how good is this?"not "what's missing?" It can:
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` | `300` | Inactivity timeout per agent |
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: 300s, kills only silent agents)")}
528
+ --timeout=SECONDS Agent inactivity timeout ${chalk.dim("(default: 900s, nudges at timeout, kills at )")}
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
- const INACTIVITY_MS = 5 * 60 * 1000;
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(prompt, opts, onLog);
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: false,
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 >= INACTIVITY_MS) {
219
- pq.close();
220
- reject(new Error(`Planner silent for ${Math.round(silent / 1000)}s — assumed hung`));
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, INACTIVITY_MS - silent + 1000));
249
+ timer = setTimeout(check, Math.min(30_000, timeoutMs - silent + 1000));
224
250
  };
225
- timer = setTimeout(check, INACTIVITY_MS);
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 ?? 5 * 60 * 1000;
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
- const agentQuery = query({
228
- prompt: this.config.useWorktrees
229
- ? `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}`
230
- : task.prompt,
231
- options: {
232
- cwd: agentCwd,
233
- model: task.model || this.config.model,
234
- permissionMode: perm,
235
- ...(perm === "bypassPermissions" && { allowDangerouslySkipPermissions: true }),
236
- allowedTools: this.config.allowedTools,
237
- includePartialMessages: true,
238
- persistSession: false,
239
- },
240
- });
241
- // Inactivity watchdog: resets on every message, only fires if agent goes silent
242
- let lastActivity = Date.now();
243
- let timer;
244
- const watchdog = new Promise((_, reject) => {
245
- const check = () => {
246
- const silent = Date.now() - lastActivity;
247
- if (silent >= inactivityMs) {
248
- agentQuery.close();
249
- reject(new AgentTimeoutError(silent));
250
- }
251
- else {
252
- timer = setTimeout(check, Math.min(30_000, inactivityMs - silent + 1000));
253
- }
254
- };
255
- timer = setTimeout(check, inactivityMs);
256
- });
257
- this.activeQueries.add(agentQuery);
258
- try {
259
- await Promise.race([
260
- (async () => {
261
- for await (const msg of agentQuery) {
262
- lastActivity = Date.now();
263
- this.handleMsg(agent, msg);
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
- watchdog,
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
- finally {
270
- clearTimeout(timer);
271
- this.activeQueries.delete(agentQuery);
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
- export {};
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.2.2",
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": {