@rahularya01/pi-cursor 1.2.0 → 1.2.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.2] - 2026-07-23
4
+
5
+ ### Fixed
6
+
7
+ - **Root hang:** Cursor `InteractionQuery` messages (web search, Exa, ask-question, switch-mode, create-plan, WebFetch field #9, unknown fields) are now always answered. Previously only WebFetch field #9 was handled — any other permission/query left the AgentService stream parked forever ("stops after a few minutes").
8
+ - Always-on lifecycle log at `$TMPDIR/pi-cursor-lifecycle.jsonl` (override with `PI_CURSOR_LIFECYCLE_LOG`) for stream start/close and interaction handling.
9
+ - h2-bridge: HTTP/2 PING every 20s to prevent intermediary idle GOAWAY; stderr/errors are surfaced instead of swallowed.
10
+ - Parent bridge now captures child stderr; heartbeats stay referenced during long tool pauses.
11
+ - Treat `heartbeat` / tool-call start / thinking-completed / summary updates as stream progress.
12
+
13
+ ## [1.2.1] - 2026-07-23
14
+
15
+ ### Fixed
16
+
17
+ - Stream idle watchdog no longer treats long pure-reasoning turns as dead: `tokenDelta`, handled native-tool reject round-trips, and `toolCallCompleted` now count as progress.
18
+ - **Idle timeouts and silent retries are disabled by default** (`PI_CURSOR_STREAM_IDLE_TIMEOUT_MS=0`, `PI_CURSOR_RESUME_IDLE_TIMEOUT_MS=0`, `PI_CURSOR_STREAM_IDLE_MAX_RETRIES=0`, `PI_CURSOR_H2_IDLE_TIMEOUT_MS=0`) so agent turns can run as long as Cursor keeps the stream open. Re-enable via env if you want a safety net.
19
+ - h2-bridge activity kill is off by default and configurable (`PI_CURSOR_H2_*_TIMEOUT_MS`); parent heartbeats still reset it when enabled.
20
+ - Blind idle retries (when re-enabled) are skipped if partial text/thinking was already streamed (avoids duplicated/jumbled answers).
21
+ - Idle retries force-refresh access tokens when a token provider is available.
22
+ - Conversation blob stores are soft-capped (~128 MiB) to limit long-session memory growth.
23
+ - Tool result `isError` is propagated into Cursor MCP results.
24
+ - Context-mode side-channel detection covers additional compaction / `[context]` injections.
25
+
26
+ ### Added
27
+
28
+ - `/cursor.doctor` surfaces `lastStreamEvent`, last idle timeout metadata, and configured idle timeouts.
29
+ - Documented stream/bridge idle env vars in README.
30
+ - Unit coverage for idle progress classification, blind-restart gating, blob trimming, and timeout resolvers.
31
+
3
32
  ## [1.2.0] - 2026-07-23
4
33
 
5
34
  ### Changed
package/README.md CHANGED
@@ -140,16 +140,24 @@ Pi Coding Agent → streamSimple (cursor-native)
140
140
 
141
141
  ## Configuration
142
142
 
143
- | Variable | Purpose |
144
- | ------------------------------------------ | ------------------------------------------------------------------------ |
145
- | `PI_CURSOR_AGENT_URL` / `CURSOR_AGENT_URL` | Override agent base URL (default: `https://agentn.us.api5.cursor.sh`). |
146
- | `CURSOR_ACCESS_TOKEN` | Static access token override. |
147
- | `PI_CURSOR_CLIENT_VERSION` | Pin `x-cursor-client-version` header sent by the HTTP/2 bridge. |
148
- | `PI_CURSOR_SYSTEM_CREDENTIALS` | `0`/`false` to disable Keychain/IDE credential reuse (default: allow). |
149
- | `PI_CURSOR_RAW_MODELS` | Disable effort-suffix model collapse. |
150
- | `PI_CURSOR_PROVIDER_DEBUG` | Enable JSONL debug logging (`/tmp/pi-cursor-debug.jsonl`). |
151
- | `CURSOR_USAGE_SESSION_TOKEN` | Optional `WorkosCursorSessionToken` fallback cookie for `/cursor.usage`. |
152
- | `PI_OFFLINE` | Skip live model discovery on startup. |
143
+ | Variable | Purpose |
144
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
145
+ | `PI_CURSOR_AGENT_URL` / `CURSOR_AGENT_URL` | Override agent base URL (default: `https://agentn.us.api5.cursor.sh`). |
146
+ | `CURSOR_ACCESS_TOKEN` | Static access token override. |
147
+ | `PI_CURSOR_CLIENT_VERSION` | Pin `x-cursor-client-version` header sent by the HTTP/2 bridge. |
148
+ | `PI_CURSOR_SYSTEM_CREDENTIALS` | `0`/`false` to disable Keychain/IDE credential reuse (default: allow). |
149
+ | `PI_CURSOR_RAW_MODELS` | Disable effort-suffix model collapse. |
150
+ | `PI_CURSOR_PROVIDER_DEBUG` | Enable verbose JSONL debug logging. |
151
+ | `PI_CURSOR_LIFECYCLE_LOG` | Always-on compact lifecycle log path (default: `$TMPDIR/pi-cursor-lifecycle.jsonl`). |
152
+ | `CURSOR_USAGE_SESSION_TOKEN` | Optional `WorkosCursorSessionToken` fallback cookie for `/cursor.usage`. |
153
+ | `PI_OFFLINE` | Skip live model discovery on startup. |
154
+ | `PI_CURSOR_STREAM_IDLE_TIMEOUT_MS` | Stream idle watchdog. **Default `0` (disabled)** so turns can run unbounded. Set e.g. `600000` to re-enable. |
155
+ | `PI_CURSOR_RESUME_IDLE_TIMEOUT_MS` | Idle timeout after tool-result resume. **Default `0` (disabled)**. |
156
+ | `PI_CURSOR_STREAM_IDLE_MAX_RETRIES` | Silent full-request retries after idle. **Default `0` (disabled)**. |
157
+ | `PI_CURSOR_ACTIVE_BRIDGE_TTL_MS` | How long a mid-tool bridge stays parked waiting for tool results (default: 1 hour). |
158
+ | `PI_CURSOR_H2_CONNECT_TIMEOUT_MS` | h2-bridge initial connect kill (default: `30000`; `0` disables). |
159
+ | `PI_CURSOR_H2_IDLE_TIMEOUT_MS` | h2-bridge activity idle kill. **Default `0` (disabled)**. Parent heartbeats reset it when enabled. |
160
+ | `PI_CURSOR_MIDPAUSE_REBUILD_MAX_AGE_MS` | Max age of mid-pause metadata used for full-history rebuild (default: 15 min). |
153
161
 
154
162
  ## Architecture notes
155
163
 
@@ -170,8 +178,9 @@ The OpenAI-compatible local proxy remains **internal/quarantined** (not exported
170
178
 
171
179
  ## Troubleshooting
172
180
 
173
- - **Not logged in / 401:** Ensure Cursor CLI or app is logged in, or run `/login cursor` again. Check `/cursor.doctor` to verify your `tokenSource`. Tokens from CLI/IDE are re-resolved when near expiry.
181
+ - **Not logged in / 401:** Ensure Cursor CLI or app is logged in, or run `/login cursor` again. Check `/cursor.doctor` to verify your `tokenSource`. Tokens from CLI/IDE are re-resolved when near expiry; idle stream retries also force-refresh credentials.
174
182
  - **Empty / hung stream:** Cursor may have updated wire headers; verify network connectivity or bump `PI_CURSOR_CLIENT_VERSION`. `/cursor.doctor` prints the active `clientVersion`.
183
+ - **Stuck / dies after a few minutes of work:** v1.2.2 answers all Cursor `InteractionQuery` permission prompts (web search / ask-question / etc.) that previously parked the stream. Inspect `$TMPDIR/pi-cursor-lifecycle.jsonl` for `interaction_query` / `bridge_close` events, and `/cursor.doctor` for `lastStreamEvent`. Full debug: `PI_CURSOR_PROVIDER_DEBUG=1`.
175
184
  - **Tool continuation lost:** The provider now prefers full-history rebuild when checkpoints are stale/mismatched. If recovery still skips, `/cursor.doctor` shows `lastRecoverySkipReason`. Retry the turn or start a new chat.
176
185
  - **WSL credential detection:** Ensure your Windows user profile folder exists under `/mnt/c/Users/` and is readable from WSL. Disable with `PI_CURSOR_SYSTEM_CREDENTIALS=0` if undesired.
177
186
 
@@ -95,31 +95,102 @@ async function readMessage() {
95
95
  const configBuf = await readMessage();
96
96
  if (!configBuf) process.exit(1);
97
97
 
98
- const config = JSON.parse(configBuf.toString("utf8"));
98
+ let config;
99
+ try {
100
+ config = JSON.parse(configBuf.toString("utf8"));
101
+ } catch {
102
+ process.stderr.write("[h2-bridge] invalid config JSON\n");
103
+ process.exit(1);
104
+ }
105
+ if (!config || typeof config !== "object") {
106
+ process.stderr.write("[h2-bridge] config must be a JSON object\n");
107
+ process.exit(1);
108
+ }
99
109
  const { accessToken, url, path: rpcPath, unary } = config;
110
+ // Connect timeout still protects against a hung first handshake (default 30s).
111
+ // Activity idle is off by default (0) so long agent turns are not killed;
112
+ // set idleTimeoutMs / PI_CURSOR_H2_IDLE_TIMEOUT_MS to re-enable a safety net.
113
+ // Parent heartbeats every 5s also reset the timer when it is enabled.
114
+ const connectTimeoutMs = optionalMs(config.connectTimeoutMs, 30_000);
115
+ const idleTimeoutMs = optionalMs(config.idleTimeoutMs, 0);
116
+
117
+ /** Parse ms; 0 means disabled. Invalid/missing uses fallback. */
118
+ function optionalMs(value, fallback) {
119
+ if (value === undefined || value === null || value === "") return fallback;
120
+ const n = Number(value);
121
+ if (!Number.isFinite(n) || n < 0) return fallback;
122
+ if (n === 0) return 0;
123
+ return Math.floor(n);
124
+ }
100
125
 
101
126
  const client = http2.connect(url || "https://api2.cursor.sh");
102
127
 
103
- // Guard against initial connection failure. Reset on any h2 activity
104
- // so long-running agent conversations (with tool call round-trips) survive.
105
- let timeout = setTimeout(killBridge, 30_000);
128
+ // HTTP/2 PING keeps intermediary/load-balancer sessions alive during long pure-thinking
129
+ // stretches where no DATA frames flow. Cursor may otherwise GOAWAY the stream mid-turn.
130
+ const pingEveryMs = optionalMs(config.pingIntervalMs, 20_000);
131
+ let pingTimer = undefined;
132
+ if (pingEveryMs > 0) {
133
+ pingTimer = setInterval(() => {
134
+ if (client.destroyed || client.closed) return;
135
+ try {
136
+ client.ping((err) => {
137
+ if (err) {
138
+ process.stderr.write(`[h2-bridge] ping failed: ${err.message}\n`);
139
+ }
140
+ });
141
+ } catch (err) {
142
+ process.stderr.write(
143
+ `[h2-bridge] ping threw: ${err instanceof Error ? err.message : String(err)}\n`,
144
+ );
145
+ }
146
+ }, pingEveryMs);
147
+ pingTimer.unref?.();
148
+ }
149
+
150
+ // Optional watchdog: connect timeout until first activity, then activity idle.
151
+ let timeout = undefined;
152
+
153
+ function clearBridgeTimeout() {
154
+ if (timeout) clearTimeout(timeout);
155
+ timeout = undefined;
156
+ }
157
+
158
+ function armBridgeTimeout(ms) {
159
+ clearBridgeTimeout();
160
+ if (!ms || ms <= 0) return;
161
+ timeout = setTimeout(killBridge, ms);
162
+ }
106
163
 
107
164
  function resetTimeout() {
108
- clearTimeout(timeout);
109
- timeout = setTimeout(killBridge, 120_000);
165
+ // After first I/O, only the activity idle (if enabled) applies.
166
+ armBridgeTimeout(idleTimeoutMs);
110
167
  }
111
168
 
112
169
  function killBridge() {
113
- clearTimeout(timeout);
170
+ clearBridgeTimeout();
171
+ if (pingTimer) clearInterval(pingTimer);
114
172
  client.destroy();
115
173
  process.exit(1);
116
174
  }
117
175
 
118
- client.on("error", () => {
119
- clearTimeout(timeout);
176
+ // Initial connect guard only (skipped when connectTimeoutMs is 0).
177
+ armBridgeTimeout(connectTimeoutMs);
178
+
179
+ client.on("error", (err) => {
180
+ clearBridgeTimeout();
181
+ if (pingTimer) clearInterval(pingTimer);
182
+ process.stderr.write(
183
+ `[h2-bridge] client error: ${err instanceof Error ? err.message : String(err)}\n`,
184
+ );
120
185
  process.exit(1);
121
186
  });
122
187
 
188
+ client.on("goaway", (errorCode, _lastStreamId, opaqueData) => {
189
+ process.stderr.write(
190
+ `[h2-bridge] GOAWAY errorCode=${errorCode} opaque=${opaqueData ? opaqueData.toString("utf8").slice(0, 200) : ""}\n`,
191
+ );
192
+ });
193
+
123
194
  const headers = {
124
195
  ":method": "POST",
125
196
  ":path": rpcPath || "/agent.v1.AgentService/Run",
@@ -156,7 +227,8 @@ h2Stream.on("data", (chunk) => {
156
227
  });
157
228
 
158
229
  h2Stream.on("end", () => {
159
- clearTimeout(timeout);
230
+ clearBridgeTimeout();
231
+ if (pingTimer) clearInterval(pingTimer);
160
232
  client.close();
161
233
  if (isErrorStatus()) {
162
234
  const body = Buffer.concat(errorChunks).toString("utf8").trim();
@@ -171,8 +243,12 @@ h2Stream.on("end", () => {
171
243
  setTimeout(() => process.exit(0), 100);
172
244
  });
173
245
 
174
- h2Stream.on("error", () => {
175
- clearTimeout(timeout);
246
+ h2Stream.on("error", (err) => {
247
+ clearBridgeTimeout();
248
+ if (pingTimer) clearInterval(pingTimer);
249
+ process.stderr.write(
250
+ `[h2-bridge] stream error: ${err instanceof Error ? err.message : String(err)}\n`,
251
+ );
176
252
  client.close();
177
253
  process.exit(1);
178
254
  });