claude-overnight 1.25.14 → 1.25.18

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
@@ -97,11 +97,11 @@ security unlock-keychain ~/Library/Keychains/login.keychain-db
97
97
 
98
98
  **Advanced:** If something else must share port `8765` and you manage the proxy yourself, set `CURSOR_OVERNIGHT_NO_PROXY_RESTART=1` to skip the automatic “replace listener” step when a Cursor API token is present.
99
99
 
100
- **How headless Cursor + macOS Keychain actually works (discovery):** We documented the full investigation: why ACP + skip-authenticate + `CURSOR_API_KEY` were not enough, how **chat-only workspace** (default in cursor-composer) fakes `HOME` and still triggered **Keychain timeouts** despite a User API key, and how **`composer-2-fast`** can fail the ACP smoke test for reasons unrelated to Keychain. See **[docs/CURSOR_PROXY_MACOS_DISCOVERY.md](docs/CURSOR_PROXY_MACOS_DISCOVERY.md)**.
100
+ **How headless Cursor + macOS Keychain actually works (discovery):** We documented the full investigation: why ACP was the wrong path for opus/sonnet `*-thinking-*` variants (model-name mismatch silent `exit 1`), how **chat-only workspace** (default in cursor-composer) fakes `HOME` and triggers **Keychain timeouts** despite a User API key, and how a cloned **account pool** makes parallel cursor-agent spawns race-free. See **[docs/CURSOR_PROXY_MACOS_DISCOVERY.md](docs/CURSOR_PROXY_MACOS_DISCOVERY.md)**.
101
101
 
102
- **Quick reference — bundled proxy env:** `CURSOR_BRIDGE_ACP_SKIP_AUTHENTICATE=1`, `CURSOR_BRIDGE_USE_ACP=1`, `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`, plus `CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN` / `CURSOR_BRIDGE_API_KEY` and `CURSOR_SKIP_KEYCHAIN=1` / `CI=true`. Details and tables are in the doc above.
102
+ **Quick reference — bundled proxy env:** `CURSOR_BRIDGE_USE_ACP=0` (CLI streaming path accepts all friendly model names), `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`, `CURSOR_CONFIG_DIRS=<5 cloned pool dirs>` (parallel-safe), plus `CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN` / `CURSOR_BRIDGE_API_KEY` and `CURSOR_SKIP_KEYCHAIN=1` / `CI=true`. Details and tables are in the doc above.
103
103
 
104
- **Regression / stress test:** `npm run matrix:cursor-proxy` (optional `--quick`, `--include-danger`). Use `MATRIX_MODELS=composer-2,composer-2-fast` to compare models; override `MATRIX_PORT_BASE`, `MATRIX_MODEL`, `MATRIX_MSG_TIMEOUT_MS` as needed.
104
+ **Regression / stress test:** `npm run matrix:cursor-proxy` (optional `--quick`, `--include-danger`). Use `MATRIX_MODELS=composer-2,claude-opus-4-7-thinking-high` to compare models; override `MATRIX_PORT_BASE`, `MATRIX_MODEL`, `MATRIX_MSG_TIMEOUT_MS` as needed.
105
105
 
106
106
  ## Install
107
107
 
@@ -1 +1 @@
1
- export declare const VERSION = "1.25.14";
1
+ export declare const VERSION = "1.25.18";
package/dist/_version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build — do not edit manually.
2
- export const VERSION = "1.25.14";
2
+ export const VERSION = "1.25.18";
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { Swarm } from "./swarm.js";
9
9
  import { planTasks, refinePlan, identifyThemes, buildThinkingTasks, orchestrate, salvageFromFile } from "./planner.js";
10
10
  import { modelDisplayName, formatContextWindow, DEFAULT_MODEL } from "./models.js";
11
11
  import { setPlannerEnvResolver } from "./planner-query.js";
12
- import { pickModel, loadProviders, preflightProvider, buildEnvResolver, healthCheckCursorProxy, PROXY_DEFAULT_URL, isCursorProxyProvider, ensureCursorProxyRunning, bundledComposerProxyShellCommand, warnMacCursorAgentShellPatchIfNeeded, hasCursorAgentToken, } from "./providers.js";
12
+ import { pickModel, loadProviders, preflightProvider, buildEnvResolver, healthCheckCursorProxy, PROXY_DEFAULT_URL, isCursorProxyProvider, readCursorProxyLogTail, ensureCursorProxyRunning, bundledComposerProxyShellCommand, warnMacCursorAgentShellPatchIfNeeded, hasCursorAgentToken, } from "./providers.js";
13
13
  import { RunDisplay } from "./ui.js";
14
14
  import { renderSummary } from "./render.js";
15
15
  import { executeRun } from "./run.js";
@@ -806,11 +806,11 @@ async function main() {
806
806
  }
807
807
  }
808
808
  process.stdout.write(` ${chalk.dim(`◆ Pinging ${pending.map(([r, p]) => `${r} (${p.displayName})`).join(", ")}…`)}\n`);
809
- // All preflights run in parallel. Cursor proxy preflights now go through a
810
- // plain HTTP POST /v1/messages (see preflightCursorProxyViaHttp) instead of
811
- // spawning the claude CLI, and the bundled proxy has no internal queue —
812
- // each request spawns its own cursor-agent subprocess (request-listener.js
813
- // handleAnthropicMessages runAgentStream).
809
+ // Preflight strategy: all providers run fully in parallel. Cursor proxy
810
+ // providers used to race on the shared `~/.cursor/cli-config.json`, but the
811
+ // proxy now uses an account pool (`CURSOR_CONFIG_DIRS`) each parallel
812
+ // cursor-agent subprocess gets its own config dir, eliminating the race.
813
+ // See ensureCursorAccountPool() in providers.ts.
814
814
  const progress = (msg) => process.stdout.write(chalk.dim(` ${msg}\n`));
815
815
  /** Cursor agent cold start + thinking-variant model latency can exceed 20s; API providers stay tight. */
816
816
  const preflightMs = (p) => isCursorProxyProvider(p) ? 60_000 : 20_000;
@@ -823,10 +823,14 @@ async function main() {
823
823
  if (!result.ok) {
824
824
  console.error(chalk.red(` ✗ ${role} preflight failed: ${chalk.dim(result.error)}`));
825
825
  if (isCursorProxyProvider(provider)) {
826
- {
827
- const cmd = bundledComposerProxyShellCommand();
828
- console.error(chalk.yellow(` The proxy at ${PROXY_DEFAULT_URL} may have crashed or timed out (e.g. keychain/UI). Retry, or start the bundled proxy: ${cmd ?? "npm install in the claude-overnight package, then re-run"}`));
826
+ const tail = readCursorProxyLogTail(25);
827
+ if (tail) {
828
+ console.error(chalk.yellow(` ── proxy log tail (agent stderr + sessions) ──`));
829
+ for (const line of tail.split("\n"))
830
+ console.error(chalk.dim(` ${line}`));
829
831
  }
832
+ const cmd = bundledComposerProxyShellCommand();
833
+ console.error(chalk.yellow(` The proxy at ${PROXY_DEFAULT_URL} may have crashed or timed out (e.g. keychain/UI). Retry, or start the bundled proxy: ${cmd ?? "npm install in the claude-overnight package, then re-run"}`));
830
834
  }
831
835
  else {
832
836
  console.error(chalk.red(` Fix the provider at ~/.claude/claude-overnight/providers.json and retry.`));
@@ -60,8 +60,33 @@ export declare function preflightProvider(p: ProviderConfig, cwd: string, timeou
60
60
  error: string;
61
61
  }>;
62
62
  export declare const PROXY_DEFAULT_URL = "http://127.0.0.1:8765";
63
+ /** File we write the proxy child's stdout+stderr to (so agent errors aren't lost to stdio:ignore). */
64
+ export declare function cursorProxyOutLogPath(): string;
65
+ /** cursor-composer-in-claude's default sessions log (request trace + ERROR lines). */
66
+ export declare function cursorProxySessionsLogPath(): string;
67
+ /**
68
+ * Read the tail of both proxy logs for diagnostics. Returns a human-readable
69
+ * block with file paths + last lines, or null if neither log exists.
70
+ */
71
+ export declare function readCursorProxyLogTail(linesPerFile?: number): string | null;
63
72
  /** Check if a provider routes through cursor-composer-in-claude. */
64
73
  export declare function isCursorProxyProvider(p: ProviderConfig): boolean;
74
+ /**
75
+ * Ensure an "account pool" of cloned config dirs exists under
76
+ * `~/.cursor-api-proxy/accounts/pool-{1..N}`. Each clone is just a copy of the
77
+ * user's `~/.cursor/cli-config.json` (has `authInfo.email` so cursor-composer
78
+ * auto-discovers it as an authenticated account).
79
+ *
80
+ * Purpose: cursor-agent subprocesses write their own cli-config.json on every
81
+ * startup via atomic tmp+rename. When N siblings all write to the same file in
82
+ * parallel, rename can lose the race and raise ENOENT. Giving each spawned
83
+ * agent its own CURSOR_CONFIG_DIR (one per pool entry) lets cursor-composer's
84
+ * AccountPool round-robin between them — zero shared writes, zero race.
85
+ *
86
+ * Refreshed every startup so token rotations in ~/.cursor flow through.
87
+ * Returns the list of pool dir paths, or null if the source config is missing.
88
+ */
89
+ export declare function ensureCursorAccountPool(poolSize?: number): string[] | null;
65
90
  /** True if ~/.zshrc / ~/.zprofile contain the `run_cursor_agent` workaround (see README). */
66
91
  export declare function hasCursorMacAgentZshPatch(): boolean;
67
92
  /**
package/dist/providers.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, realpathSync } from "fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, realpathSync, openSync, statSync, readSync, closeSync } from "fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { homedir } from "os";
4
4
  import { join, dirname } from "path";
@@ -385,10 +385,104 @@ async function preflightCursorProxyViaHttp(p, timeoutMs, opts) {
385
385
  }
386
386
  // ── Cursor API Proxy ──
387
387
  export const PROXY_DEFAULT_URL = "http://127.0.0.1:8765";
388
+ /** Directory cursor-composer-in-claude uses for its own sessions.log (see env.js). */
389
+ function cursorProxyLogDir() {
390
+ return join(homedir(), ".cursor-api-proxy");
391
+ }
392
+ /** File we write the proxy child's stdout+stderr to (so agent errors aren't lost to stdio:ignore). */
393
+ export function cursorProxyOutLogPath() {
394
+ return join(cursorProxyLogDir(), "proxy.out.log");
395
+ }
396
+ /** cursor-composer-in-claude's default sessions log (request trace + ERROR lines). */
397
+ export function cursorProxySessionsLogPath() {
398
+ return join(cursorProxyLogDir(), "sessions.log");
399
+ }
400
+ function tailFile(path, maxLines, maxBytes = 32_768) {
401
+ try {
402
+ const st = statSync(path);
403
+ const size = st.size;
404
+ const start = size > maxBytes ? size - maxBytes : 0;
405
+ const buf = Buffer.alloc(size - start);
406
+ const fd = openSync(path, "r");
407
+ try {
408
+ readSync(fd, buf, 0, buf.length, start);
409
+ }
410
+ finally {
411
+ try {
412
+ closeSync(fd);
413
+ }
414
+ catch { }
415
+ }
416
+ const text = buf.toString("utf8");
417
+ const lines = text.split("\n").filter(Boolean);
418
+ return lines.slice(-maxLines).join("\n");
419
+ }
420
+ catch {
421
+ return null;
422
+ }
423
+ }
424
+ /**
425
+ * Read the tail of both proxy logs for diagnostics. Returns a human-readable
426
+ * block with file paths + last lines, or null if neither log exists.
427
+ */
428
+ export function readCursorProxyLogTail(linesPerFile = 20) {
429
+ const out = cursorProxyOutLogPath();
430
+ const sess = cursorProxySessionsLogPath();
431
+ const parts = [];
432
+ const outTail = tailFile(out, linesPerFile);
433
+ if (outTail)
434
+ parts.push(`── ${out} (last ${linesPerFile} lines) ──\n${outTail}`);
435
+ const sessTail = tailFile(sess, linesPerFile);
436
+ if (sessTail)
437
+ parts.push(`── ${sess} (last ${linesPerFile} lines) ──\n${sessTail}`);
438
+ return parts.length ? parts.join("\n\n") : null;
439
+ }
388
440
  /** Check if a provider routes through cursor-composer-in-claude. */
389
441
  export function isCursorProxyProvider(p) {
390
442
  return p.cursorProxy === true || p.baseURL === PROXY_DEFAULT_URL;
391
443
  }
444
+ /**
445
+ * Ensure an "account pool" of cloned config dirs exists under
446
+ * `~/.cursor-api-proxy/accounts/pool-{1..N}`. Each clone is just a copy of the
447
+ * user's `~/.cursor/cli-config.json` (has `authInfo.email` so cursor-composer
448
+ * auto-discovers it as an authenticated account).
449
+ *
450
+ * Purpose: cursor-agent subprocesses write their own cli-config.json on every
451
+ * startup via atomic tmp+rename. When N siblings all write to the same file in
452
+ * parallel, rename can lose the race and raise ENOENT. Giving each spawned
453
+ * agent its own CURSOR_CONFIG_DIR (one per pool entry) lets cursor-composer's
454
+ * AccountPool round-robin between them — zero shared writes, zero race.
455
+ *
456
+ * Refreshed every startup so token rotations in ~/.cursor flow through.
457
+ * Returns the list of pool dir paths, or null if the source config is missing.
458
+ */
459
+ export function ensureCursorAccountPool(poolSize = 5) {
460
+ if (poolSize <= 0)
461
+ return null;
462
+ const source = join(homedir(), ".cursor", "cli-config.json");
463
+ if (!existsSync(source))
464
+ return null;
465
+ let sourceBuf;
466
+ try {
467
+ sourceBuf = readFileSync(source);
468
+ }
469
+ catch {
470
+ return null;
471
+ }
472
+ const dirs = [];
473
+ for (let i = 1; i <= poolSize; i++) {
474
+ const dir = join(homedir(), ".cursor-api-proxy", "accounts", `pool-${i}`);
475
+ try {
476
+ mkdirSync(dir, { recursive: true });
477
+ writeFileSync(join(dir, "cli-config.json"), sourceBuf);
478
+ dirs.push(dir);
479
+ }
480
+ catch {
481
+ // skip this slot; pool still works with fewer dirs
482
+ }
483
+ }
484
+ return dirs.length > 0 ? dirs : null;
485
+ }
392
486
  /** True if ~/.zshrc / ~/.zprofile contain the `run_cursor_agent` workaround (see README). */
393
487
  export function hasCursorMacAgentZshPatch() {
394
488
  let combined = "";
@@ -772,12 +866,18 @@ async function startProxyProcess(baseUrl, url, port) {
772
866
  // if the shell omitted CURSOR_API_KEY (GUI launches, etc.).
773
867
  CURSOR_API_KEY: agentToken,
774
868
  CURSOR_AUTH_TOKEN: agentToken,
775
- // cursor-composer loadBridgeConfig: forces acpSkipAuthenticate so ACP never sends
776
- // `authenticate` / `cursor_login` (that path touches macOS Keychain for `cursor-user`).
777
- CURSOR_BRIDGE_ACP_SKIP_AUTHENTICATE: "1",
778
- // Default bridge is useAcp=false agent uses runStreaming; skip-authenticate only applies
779
- // to runAcpStream. Force ACP so real traffic matches the headless/keychain-avoidance path.
780
- CURSOR_BRIDGE_USE_ACP: "1",
869
+ // Use the CLI streaming path (runStreaming), NOT ACP. The ACP path is broken for
870
+ // opus/sonnet *-thinking-*/effort-variant friendly names: cursor-composer's
871
+ // resolveAcpModelConfigValue only matches against ACP `name` fields (e.g.
872
+ // `claude-opus-4-7`), while friendly IDs like `claude-opus-4-7-thinking-high`
873
+ // come from `agent --list-models`. The ACP agent then replies
874
+ // `{error: "Invalid model value: claude-opus-4-7-thinking-high"}` and
875
+ // cursor-composer's acp-client swallows the error to a silent exit-1.
876
+ // The CLI path accepts all friendly names (verified with opus-thinking-high,
877
+ // gemini-3.1-pro, composer-2 via HTTP preflight). Keychain safety is preserved:
878
+ // the CLI path injects keychain-shim-inject.js via NODE_OPTIONS which no-ops
879
+ // /usr/bin/security calls on macOS (cursor-composer/dist/lib/process.js).
880
+ CURSOR_BRIDGE_USE_ACP: "0",
781
881
  // cursor-composer chat-only mode fakes HOME to a temp dir; on macOS the agent still waits on
782
882
  // Keychain (~30s) for `cursor-user` despite CURSOR_API_KEY. Use the real workspace profile.
783
883
  CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE: "false",
@@ -786,6 +886,12 @@ async function startProxyProcess(baseUrl, url, port) {
786
886
  proxyEnv.CURSOR_AGENT_NODE = sysNode;
787
887
  proxyEnv.CURSOR_AGENT_SCRIPT = agentJs;
788
888
  }
889
+ // Enable the account pool so parallel cursor-agent subprocesses get
890
+ // separate CURSOR_CONFIG_DIRs — no more cli-config.json write race.
891
+ const pool = ensureCursorAccountPool(5);
892
+ if (pool && !proxyEnv.CURSOR_CONFIG_DIRS) {
893
+ proxyEnv.CURSOR_CONFIG_DIRS = pool.join(",");
894
+ }
789
895
  console.log(chalk.dim(JSON.stringify({
790
896
  claudeOvernight: VERSION,
791
897
  spawnProxy: {
@@ -802,14 +908,23 @@ async function startProxyProcess(baseUrl, url, port) {
802
908
  CURSOR_BRIDGE_USE_ACP: proxyEnv.CURSOR_BRIDGE_USE_ACP,
803
909
  CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE: proxyEnv.CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE,
804
910
  CURSOR_API_KEY: "(set)",
911
+ accountPool: proxyEnv.CURSOR_CONFIG_DIRS ? `${proxyEnv.CURSOR_CONFIG_DIRS.split(",").length} dirs` : "disabled",
805
912
  },
806
913
  },
807
914
  })));
808
915
  try {
809
- console.log(chalk.dim(` Spawning proxy…`));
916
+ // Capture proxy stdout+stderr to a log file — stdio:"ignore" was hiding
917
+ // agent stderr so "cursor_cli_error" responses had no actionable context.
918
+ const logPath = cursorProxyOutLogPath();
919
+ try {
920
+ mkdirSync(dirname(logPath), { recursive: true });
921
+ }
922
+ catch { }
923
+ const logFd = openSync(logPath, "a");
924
+ console.log(chalk.dim(` Spawning proxy… ${chalk.dim(`(logs: ${logPath})`)}`));
810
925
  const child = spawn(process.execPath, [composerCli], {
811
926
  detached: true,
812
- stdio: "ignore",
927
+ stdio: ["ignore", logFd, logFd],
813
928
  env: proxyEnv,
814
929
  });
815
930
  child.unref(); // let it outlive this process
@@ -1,116 +1,112 @@
1
- # Cursor bundled proxy on macOS: Keychain, ACP, and what actually fixed it
1
+ # Cursor bundled proxy on macOS: Keychain, ACP, parallel safety
2
2
 
3
- This document records **why** the Cursor API proxy (`cursor-composer-in-claude`) triggered macOS Keychain dialogs and long hangs on automation, **what did not fix it**, and **which environment variables and model choices** make headless runs reliable. It is written for maintainers and for anyone debugging similar “it still asks for Keychain” reports.
3
+ This document records **why** the Cursor API proxy (`cursor-composer-in-claude`) is tricky to run headlessly (macOS Keychain dialogs, parallel crashes, model-name mismatches), **what did not fix it**, and the **env vars + account-pool setup** `claude-overnight` now ships as defaults.
4
4
 
5
5
  ---
6
6
 
7
7
  ## Context
8
8
 
9
- - **claude-overnight** can bundle **cursor-composer-in-claude**, which exposes an Anthropic-compatible HTTP server and forwards requests to the Cursor **`agent`** CLI (often via **ACP**, the Agent Client Protocol over stdio).
10
- - Headless use is supposed to rely on a **[User API key](https://cursor.com/docs/cli/headless)** (`CURSOR_API_KEY` / dashboard), not on interactive login stored as **`cursor-user`** in the login keychain.
11
- - Despite setting `CURSOR_SKIP_KEYCHAIN=1`, `CI=true`, and API keys, macOS could still show Keychain UI or block for ~30s with errors like **`Keychain operation timed out after 30000ms`** in the proxy log (`~/.cursor-api-proxy/sessions.log` or stderr).
9
+ - **claude-overnight** bundles **cursor-composer-in-claude**, an Anthropic-compatible HTTP server that forwards requests to the Cursor **`agent`** CLI. cursor-composer has **two agent paths**: **CLI streaming** (default, `useAcp=false`) and **ACP** (JSON-RPC over stdio, `useAcp=true`).
10
+ - Headless use is supposed to rely on a **[User API key](https://cursor.com/docs/cli/headless)** (`CURSOR_API_KEY`), not on interactive login stored as **`cursor-user`** in the login keychain.
12
11
 
13
- ---
14
-
15
- ## Symptoms we saw
16
-
17
- 1. **GUI:** System Keychain prompts, or “Keychain Not Found” style dialogs for `cursor-user`.
18
- 2. **Proxy logs:** `Agent error: Cursor CLI failed (exit 1): Error: Keychain operation timed out after 30000ms`.
19
- 3. **Stress tests:** Every matrix row returning **HTTP 500** looked like one bug; in reality **two different failure modes** were mixed (see below).
12
+ Three independent failure modes were mixed together in early reports: **Keychain contention**, **ACP model-name mismatch**, and a **`cli-config.json` write race** under parallel load.
20
13
 
21
14
  ---
22
15
 
23
- ## What we tried that was necessary but not sufficient
24
-
25
- These are still **correct** to set; they address real issues, but they did **not** alone stop Keychain contention on macOS.
16
+ ## Failure mode 1: Keychain contention (chat-only workspace + temp `HOME`)
26
17
 
27
- | Measure | Role |
28
- |--------|------|
29
- | **`CURSOR_SKIP_KEYCHAIN=1`** + **`CI=true`** | Cursor’s own convention to discourage interactive keychain probes in CI-style runs. |
30
- | **`CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN`** (User API key) | Headless auth for the native agent; must be injected into the **proxy process** env, not only the parent shell (GUI launches often omit them). |
31
- | **`CURSOR_BRIDGE_API_KEY`** | HTTP bearer for the proxy’s `/health` and `/v1/*` routes; often mirrored from the same token. |
32
- | **`CURSOR_BRIDGE_ACP_SKIP_AUTHENTICATE=1`** | In `cursor-composer-in-claude`, `loadBridgeConfig` sets `acpSkipAuthenticate` when this is on **or** when an API key is present. Skips the ACP **`authenticate` / `cursor_login`** step that can touch Keychain. |
33
- | **`CURSOR_BRIDGE_USE_ACP=1`** | Default bridge config has **`useAcp: false`**. Without ACP, traffic used **`runStreaming`** instead of **`runAcpStream`**; skip-authenticate only applies on the **ACP** path. Forcing ACP keeps behavior aligned with the intended headless/ACP pipeline. |
34
-
35
- Without **`CURSOR_BRIDGE_USE_ACP=1`**, skip-authenticate did not apply to the code path that handled streaming requests.
18
+ - cursor-composer defaults **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=true`**. For each request it creates a temp dir and sets **`HOME`** (and related profile vars) to that temp dir so rules from the real `~/.cursor` are not loaded.
19
+ - With a valid User API key in env, `composer-2` could still hit **`Keychain operation timed out after 30000ms`** under chat-only. Setting **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`** made the same call succeed — the Cursor CLI was probing Keychain for `cursor-user` when its profile view was empty, even though API key auth was set.
20
+ - **Fix shipped:** spawn the proxy with `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`. Trade-off: the agent no longer runs with a disposable fake `HOME` per request.
21
+ - Orthogonally, cursor-composer injects **`keychain-shim-inject.js`** via `NODE_OPTIONS` on macOS (see `node_modules/cursor-composer-in-claude/dist/lib/keychain-shim-inject.js`). It no-ops `/usr/bin/security` at the Node level for spawned agents Keychain safety is preserved on the CLI streaming path without needing ACP's `skipAuthenticate`.
36
22
 
37
23
  ---
38
24
 
39
- ## Discovery 1: Chat-only workspace and a fake `HOME` (main Keychain fix)
40
-
41
- **cursor-composer** defaults **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE`** to **`true`** (“chat-only workspace: yes (isolated temp dir)” in the startup banner).
42
-
43
- For each request it:
44
-
45
- - Creates a **temporary directory** and points **`CURSOR_CONFIG_DIR`** at a minimal tree under it.
46
- - In **`getChatOnlyEnvOverrides`** (when no account-pool `authConfigDir`), it sets **`HOME`** (and related profile vars) to that **temp** directory so rules from the real `~/.cursor` are not loaded.
47
-
48
- **Observation:** With a valid User API key in env, **`composer-2`** could still hit **`Keychain operation timed out after 30000ms`** when chat-only was **on**. With **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`**, the same model and key **succeeded** (real workspace / real profile resolution, no temp `HOME`).
49
-
50
- **Interpretation:** The Cursor CLI in ACP mode was still probing macOS Keychain for `cursor-user` when the process believed it was in an isolated “empty” profile (temp `HOME`), even though API key auth was set. That matches a **profile / keychain resolution** path, not a missing `CURSOR_API_KEY` in the parent shell.
51
-
52
- **Fix shipped in claude-overnight:** spawn the bundled proxy with **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`**.
53
-
54
- **Trade-off:** You lose the strictest isolation (the agent no longer runs with a disposable fake `HOME` for every request). You gain reliable headless behavior on macOS with API keys. For many automation setups this is the right default.
55
-
56
- **How to see it in tests:** The matrix script includes a row **`12-chat-workspace-isolated`** (`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=true`). With **`composer-2`**, that row tends to **fail** while **`01-overnight-parity`** passes, reproducing the regression.
25
+ ## Failure mode 2: ACP model-name mismatch (opus/sonnet `*-thinking-*`)
26
+
27
+ - `agent --list-models` returns friendly IDs like `claude-opus-4-7-thinking-high`, `gemini-3.1-pro`.
28
+ - The ACP model catalog only exposes **bracketed** IDs keyed by stripped `name` fields: `claude-opus-4-7` with `modelId: claude-opus-4-7[thinking=true,effort=high]`. cursor-composer's `resolveAcpModelConfigValue` has no mapping from `claude-opus-4-7-thinking-high` to the bracketed form.
29
+ - When `USE_ACP=1`, the ACP agent replies:
30
+ ```
31
+ {"error":{"code":-32602,"message":"Invalid params","data":{"message":"Invalid model value: claude-opus-4-7-thinking-high"}}}
32
+ ```
33
+ `acp-client.js` swallows this RPC error to a silent `exit 1`, which the proxy surfaces as:
34
+ ```
35
+ HTTP 500: The Cursor agent process exited with code 1. See server logs for details.
36
+ ```
37
+ - **Fix shipped:** force **`CURSOR_BRIDGE_USE_ACP=0`**. The CLI streaming path accepts every `agent --list-models` friendly name (verified: `claude-opus-4-7-thinking-high`, `gemini-3.1-pro`, `composer-2`). Keychain safety is preserved by the `NODE_OPTIONS` shim above.
38
+ - `CURSOR_BRIDGE_ACP_SKIP_AUTHENTICATE` is no longer needed and no longer set: the CLI path never calls `cursor_login`.
57
39
 
58
40
  ---
59
41
 
60
- ## Discovery 2: `composer-2-fast` was never a real model
42
+ ## Failure mode 3: `cli-config.json` write race (parallel spawns)
61
43
 
62
- The ACP model catalog only offers `composer-2` with `modelId: composer-2[fast=true]`. There is no separate `composer-2-fast` model `composer-2` already IS the fast variant. Passing `composer-2-fast` to `session/set_config_option` fails with "Invalid model value" because it's not in the catalog. Use **`composer-2`** as the model name.
44
+ - Every `cursor-agent` subprocess rewrites `~/.cursor/cli-config.json` on startup using atomic tmp+rename. N sibling spawns race on the `rename(*.tmp cli-config.json)` step and intermittently raise:
45
+ ```
46
+ ERROR POST /v1/messages 127.0.0.1 agent_exit_1
47
+ Error: ENOENT: no such file or directory,
48
+ rename '/Users/x/.cursor/cli-config.json.tmp' -> '/Users/x/.cursor/cli-config.json'
49
+ ```
50
+ - Observed hit rate: ~20% (3/15 queries) at swarm concurrency 5 with shared config. Preflights with 3 cursor providers hit it too.
51
+ - cursor-composer has a built-in **AccountPool**: when `CURSOR_CONFIG_DIRS=<dir1,dir2,…>` is set (or `~/.cursor-api-proxy/accounts/<name>/cli-config.json` contains `authInfo.email`), it round-robins spawns across the dirs, each exported to the agent as its own `CURSOR_CONFIG_DIR`. Separate files, no shared rename target, no race.
52
+ - **Fix shipped:** `ensureCursorAccountPool()` in `src/providers.ts` clones `~/.cursor/cli-config.json` into `~/.cursor-api-proxy/accounts/pool-{1..5}` on startup and exports `CURSOR_CONFIG_DIRS` to the proxy. Pool is refreshed every startup so token rotations in `~/.cursor` propagate.
53
+ - **Verified:** 25/25 across 5 rounds × 5 parallel `composer-2` requests, zero `agent_exit_1` / `cli-config.json.tmp` entries in `~/.cursor-api-proxy/sessions.log`.
54
+ - **Preflight impact:** 3 cursor providers in parallel drop from ~21s sequential to ~8s.
63
55
 
64
56
  ---
65
57
 
66
58
  ## What claude-overnight sets when it auto-starts the proxy
67
59
 
68
- When `startProxyProcess` runs, it builds a **`proxyEnv`** that always includes (among others):
60
+ `startProxyProcess` in `src/providers.ts` builds a `proxyEnv` that always includes:
69
61
 
70
- | Variable | Purpose |
71
- |----------|--------|
72
- | `CI` | `"true"` (forced so a parent shell cannot leave `CI` empty and re-enable interactive probes). |
73
- | `CURSOR_SKIP_KEYCHAIN` | `"1"` (forced). |
74
- | `CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN` | Resolved User API key / bridge key (same token mirrored for the native agent). |
75
- | `CURSOR_BRIDGE_API_KEY` | HTTP auth for the proxy. |
76
- | `CURSOR_BRIDGE_ACP_SKIP_AUTHENTICATE` | `"1"` (skip `cursor_login` on ACP). |
77
- | `CURSOR_BRIDGE_USE_ACP` | `"1"` (use ACP path so skip-authenticate applies). |
78
- | **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE`** | **`"false"`** (avoid temp `HOME` Keychain behavior on macOS). |
79
- | `CURSOR_AGENT_NODE` / `CURSOR_AGENT_SCRIPT` | When detected: system Node + `agent` `index.js` (avoids known issues with the bundled Node on some macOS installs). |
62
+ | Variable | Value | Purpose |
63
+ |---|---|---|
64
+ | `CI` | `"true"` | Forced; prevents a parent shell from re-enabling interactive probes. |
65
+ | `CURSOR_SKIP_KEYCHAIN` | `"1"` | Cursor's own CI convention. |
66
+ | `CURSOR_API_KEY` / `CURSOR_AUTH_TOKEN` | User API key | Headless auth for the native agent; mirrored into the proxy spawn env because GUI launches can omit them. |
67
+ | `CURSOR_BRIDGE_API_KEY` | Same token | HTTP bearer for the proxy's `/health` and `/v1/*`. |
68
+ | **`CURSOR_BRIDGE_USE_ACP`** | **`"0"`** | CLI streaming path; avoids the ACP model-name mismatch for `*-thinking-*` variants. |
69
+ | **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE`** | **`"false"`** | Avoids temp `HOME` Keychain waits on macOS. |
70
+ | **`CURSOR_CONFIG_DIRS`** | **`pool-1,…,pool-5`** | Cloned from `~/.cursor/cli-config.json`; eliminates the write race under parallel spawns. |
71
+ | `CURSOR_AGENT_NODE` / `CURSOR_AGENT_SCRIPT` | When detected | System Node + `agent/index.js` (avoids known issues with the bundled Node on some macOS installs). |
80
72
 
81
- See `startProxyProcess` in `src/providers.ts` for the exact spawn and logging.
73
+ Startup logs print an `accountPool` field in `spawnProxy.childEnv` showing how many pool dirs are active.
82
74
 
83
75
  ---
84
76
 
85
77
  ## How to verify
86
78
 
87
- 1. **Matrix (recommended):**
88
- `MATRIX_MODELS=composer-2 npm run matrix:cursor-proxy`
89
- - Expect **`composer-2`** parity row **HTTP 200**.
79
+ 1. **Matrix (recommended):**
80
+ ```
81
+ MATRIX_MODELS=composer-2,claude-opus-4-7-thinking-high npm run matrix:cursor-proxy
82
+ ```
83
+ All rows (including thinking variants) should return **HTTP 200**.
84
+
85
+ 2. **Parallel smoke (manual):** fire 5 concurrent `POST /v1/messages` at `composer-2` through the running proxy. With the account pool enabled, expect 5/5 200s and zero `agent_exit_1` in `~/.cursor-api-proxy/sessions.log`.
90
86
 
91
- 2. **Logs:** On failure, check proxy stderr / `~/.cursor-api-proxy/sessions.log` for **`Keychain operation timed out`** vs empty stderr / generic exit 1.
87
+ 3. **Logs:** claude-overnight redirects proxy stdout+stderr to `~/.cursor-api-proxy/proxy.out.log` and prints a tail on preflight failure. cursor-composer's own request trace is `~/.cursor-api-proxy/sessions.log`.
92
88
 
93
- 3. **Preflight:** claude-overnight runs provider preflights with timeouts; Cursor proxy preflights are serialized to avoid starving the single agent listener.
89
+ 4. **Preflight:** claude-overnight runs provider preflights fully in parallel (HTTP `POST /v1/messages` with `max_tokens: 4096`, not a claude-CLI spawn). Cursor proxy providers ride the account pool.
94
90
 
95
91
  ---
96
92
 
97
93
  ## When the OS keychain itself is broken
98
94
 
99
- If **`login.keychain`** is missing or damaged, macOS can still show dialogs unrelated to Cursor. Keychain Access → First Aid, or `security unlock-keychain ~/Library/Keychains/login.keychain-db`, may help. That is **orthogonal** to the chat-only / `HOME` discovery above.
95
+ If **`login.keychain`** is missing or damaged, macOS can still show dialogs unrelated to Cursor. Keychain Access → First Aid, or `security unlock-keychain ~/Library/Keychains/login.keychain-db`, may help. That is **orthogonal** to the chat-only / ACP / pool discoveries above.
100
96
 
101
97
  ---
102
98
 
103
99
  ## References in this repo
104
100
 
105
- - Implementation: `src/providers.ts` (`startProxyProcess`, `envFor`, `ensureCursorProxyRunning`).
101
+ - Implementation: `src/providers.ts` (`startProxyProcess`, `ensureCursorAccountPool`, `preflightCursorProxyViaHttp`, `readCursorProxyLogTail`).
102
+ - Preflight strategy: `src/index.ts` (all providers parallel via `Promise.all`).
106
103
  - Stress harness: `scripts/cursor-proxy-keychain-matrix.mjs`, `npm run matrix:cursor-proxy`.
107
- - Upstream behavior: `node_modules/cursor-composer-in-claude/dist/lib/config.js` (`loadBridgeConfig`), `workspace.js` (`getChatOnlyEnvOverrides`), `acp-client.js` (`buildAcpSpawnEnv`, ACP handshake).
104
+ - Upstream behavior: `node_modules/cursor-composer-in-claude/dist/lib/` — `env.js` (`configDirs` discovery), `account-pool.js` (round-robin), `process.js` (sets `CURSOR_CONFIG_DIR` from pool), `workspace.js` (`getChatOnlyEnvOverrides`), `acp-client.js` (`resolveAcpModelConfigValue` and the error-swallowing `exit 1`), `keychain-shim-inject.js` (`/usr/bin/security` no-op).
108
105
 
109
106
  ---
110
107
 
111
108
  ## Summary
112
109
 
113
- 1. **ACP + skip-authenticate + USE_ACP** are required so the bridge uses the path where headless auth is designed to apply.
114
- 2. **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`** is the macOS-specific fix that stops temp-`HOME` isolation from driving Keychain waits despite API keys.
115
- 3. **Keychain shim** (`NODE_OPTIONS=--require keychain-shim.cjs`) intercepts `/usr/bin/security` calls at the Node.js level, eliminating macOS Keychain dialogs regardless of other env vars.
116
- 4. Use **`composer-2`** as the model name — `composer-2-fast` was never a real model in the ACP catalog.
110
+ 1. **`CURSOR_BRIDGE_USE_ACP=0`** routes requests through the CLI streaming path, which accepts every friendly model name including opus/sonnet `*-thinking-*`.
111
+ 2. **`CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=false`** stops temp-`HOME` from driving Keychain waits on macOS. The `NODE_OPTIONS` shim (`keychain-shim-inject.js`) keeps `/usr/bin/security` from reaching macOS.
112
+ 3. **`CURSOR_CONFIG_DIRS=<pool-1,…,pool-5>`** gives each parallel cursor-agent subprocess its own `cli-config.json` no more `rename(.tmp→cli-config.json)` race under swarm concurrency or parallel preflights.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.25.14",
3
+ "version": "1.25.18",
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.25.14",
3
+ "version": "1.25.18",
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"