claude-overnight 1.50.0 → 1.50.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.
@@ -1 +1 @@
1
- export declare const VERSION = "1.50.0";
1
+ export declare const VERSION = "1.50.1";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build — do not edit manually.
2
- export const VERSION = "1.50.0";
2
+ export const VERSION = "1.50.1";
@@ -12,6 +12,15 @@ export interface Task {
12
12
  noWorktree?: boolean;
13
13
  /** SDK session ID to resume from (set when task was paused mid-turn). */
14
14
  resumeSessionId?: string;
15
+ /**
16
+ * Discriminator for the (provider, model, cwd) that produced `resumeSessionId`.
17
+ * The SDK keys sessions by project path locally and by account/model on the
18
+ * backend; if any of those differ at resume time the saved id points at a
19
+ * conversation neither side can find. Compared against the live key on resume;
20
+ * mismatch drops `resumeSessionId` before the SDK errors with
21
+ * "No conversation found with session ID".
22
+ */
23
+ resumeContextKey?: string;
15
24
  /** Working directory preserved from a previous run (worktree dir for paused-and-resumed tasks). */
16
25
  agentCwd?: string;
17
26
  /** The kind of work: "execute" modifies files, others are read-only/analysis. Defaults to "execute". */
@@ -9,6 +9,7 @@ import { withCursorWorkspaceHeader, getAgentTimeout } from "./config.js";
9
9
  import { renderPrompt } from "../prompts/load.js";
10
10
  import { AgentTimeoutError, StreamStalledError, isRateLimitError, isStreamStalledError, isTransientError, sleep } from "./errors.js";
11
11
  import { handleMsg, checkStreamHealth, NO_CONTENT_TIMEOUT_MS } from "./message-handler.js";
12
+ import { getModelCapability } from "../core/models.js";
12
13
  import { sdkQueryRateLimiter, acquireSdkQueryRateLimit } from "../core/rate-limiter.js";
13
14
  import { StreamSink } from "../core/transcripts.js";
14
15
  import { StallGuard, StallMonitor, runWithStallRotation } from "../core/stall-guard.js";
@@ -82,6 +83,16 @@ export async function runAgent(host, task) {
82
83
  host.log(id, `Worktree failed after retry -- running without isolation`);
83
84
  }
84
85
  }
86
+ const effectiveModelInit = task.model || host.model;
87
+ const contextKey = sessionContextKey(effectiveModelInit, agentCwd, host.config.envForModel?.(effectiveModelInit));
88
+ // Drop a saved sessionId whose (provider, model, cwd) no longer matches the
89
+ // live one. Otherwise the first resume call fails with "No conversation found
90
+ // with session ID" and burns an attempt before any tool use. See
91
+ // Task.resumeContextKey for the why.
92
+ if (task.resumeSessionId && task.resumeContextKey && task.resumeContextKey !== contextKey) {
93
+ host.log(id, `Dropping stale resume id (context changed: ${task.resumeContextKey} → ${contextKey})`);
94
+ task = { ...task, resumeSessionId: undefined, resumeContextKey: undefined };
95
+ }
85
96
  const isResumed = !!task.resumeSessionId;
86
97
  host.log(id, isResumed ? `Resuming: ${task.prompt.slice(0, 60)}` : `Starting: ${task.prompt.slice(0, 60)}`);
87
98
  const maxRetries = host.config.maxRetries ?? 2;
@@ -90,6 +101,21 @@ export async function runAgent(host, task) {
90
101
  // Hoisted so the catch block can read the session captured during the turn
91
102
  // when routing a pause-interrupt through the requeue path.
92
103
  let resumeSessionId = task.resumeSessionId;
104
+ // Carry the resume session forward only if the prior turn isn't already
105
+ // close to filling its context window. A saturated session would resume
106
+ // with little room to do real work and would auto-compact (or hit the
107
+ // window) almost immediately — cheaper to start the next attempt fresh.
108
+ const carrySession = () => {
109
+ if (!resumeSessionId)
110
+ return false;
111
+ const safe = getModelCapability(effectiveModelInit ?? "").safeContext;
112
+ const used = agent.peakContextTokens ?? agent.contextTokens ?? 0;
113
+ if (safe > 0 && used >= safe * 0.85) {
114
+ host.log(id, `Discarding resume id (context ${used}/${safe} tokens, near saturation)`);
115
+ return false;
116
+ }
117
+ return true;
118
+ };
93
119
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
94
120
  if (attempt > 0) {
95
121
  const backoffMs = Math.min(30000, 1000 * 2 ** (attempt - 1)) * (0.5 + Math.random());
@@ -210,9 +236,10 @@ export async function runAgent(host, task) {
210
236
  if (!host.paused || agent.status !== "running")
211
237
  return false;
212
238
  agent.status = "paused";
213
- host.log(id, resumeSessionId ? "Paused mid-task (will resume)" : "Paused before first turn (will restart)");
214
- host.queue.unshift(resumeSessionId
215
- ? { ...task, resumeSessionId, agentCwd }
239
+ const carry = carrySession();
240
+ host.log(id, carry ? "Paused mid-task (will resume)" : "Paused before first turn (will restart)");
241
+ host.queue.unshift(carry
242
+ ? { ...task, resumeSessionId, resumeContextKey: contextKey, agentCwd }
216
243
  : { ...task });
217
244
  return true;
218
245
  };
@@ -299,9 +326,12 @@ export async function runAgent(host, task) {
299
326
  if (host.paused) {
300
327
  agent.status = "paused";
301
328
  host.log(id, "Paused mid-task (interrupt thrown)");
302
- // Reuse resume info when we already have a sessionId; otherwise restart fresh.
303
- const reuseSession = (typeof resumeSessionId === "string") && resumeSessionId.length > 0;
304
- host.queue.unshift(reuseSession ? { ...task, resumeSessionId, agentCwd } : { ...task });
329
+ // Reuse resume info when we have a sessionId AND the prior context isn't
330
+ // already saturated; otherwise restart fresh.
331
+ const reuseSession = carrySession();
332
+ host.queue.unshift(reuseSession
333
+ ? { ...task, resumeSessionId, resumeContextKey: contextKey, agentCwd }
334
+ : { ...task });
305
335
  return;
306
336
  }
307
337
  // Stream stall: the server went silent mid-response. If we captured a
@@ -423,6 +453,19 @@ function installLspFirstHookInto(worktreeDir) {
423
453
  // settings.local.json is gitignored by Claude Code convention — won't pollute the agent's commit.
424
454
  writeFileSync(join(dir, "settings.local.json"), JSON.stringify(settings, null, 2), "utf-8");
425
455
  }
456
+ /**
457
+ * Stable per-(provider, model, cwd) tag for scoping `resume` session ids.
458
+ * Provider matters because Cursor proxy and Anthropic direct keep separate
459
+ * backend session stores; cwd matters because the SDK keys its on-disk session
460
+ * cache by project path, so a recreated worktree under a new path can't find
461
+ * the prior conversation. Model is included for completeness.
462
+ */
463
+ function sessionContextKey(model, cwd, env) {
464
+ const isCursor = !!(env?.CURSOR_API_KEY || env?.CURSOR_AUTH_TOKEN || env?.CURSOR_BRIDGE_MODE);
465
+ const baseUrl = env?.ANTHROPIC_BASE_URL?.trim();
466
+ const provider = isCursor ? "cursor" : baseUrl ? `url:${baseUrl}` : "anthropic";
467
+ return `${provider}|${model ?? "default"}|${cwd}`;
468
+ }
426
469
  /** Extract a ### SKILL CANDIDATE block from agent text. Returns undefined if not found. */
427
470
  function extractSkillProposal(text) {
428
471
  const m = text.match(/###\s*SKILL CANDIDATE\s*\n([\s\S]+?)$/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.50.0",
3
+ "version": "1.50.1",
4
4
  "description": "Parallel Claude agents in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Provider-agnostic model catalog (Anthropic, Cursor, OpenAI, Gemini, DeepSeek, Llama, Qwen) with capability-based task scoping.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.50.0",
3
+ "version": "1.50.1",
4
4
  "description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs -- parallel Claude agents in git worktrees with thinking waves, multi-wave steering, and crash-safe resume. Supports Cursor API Proxy, Qwen, OpenRouter.",
5
5
  "author": {
6
6
  "name": "Francesco Fornace"