@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 +29 -0
- package/README.md +20 -11
- package/dist/h2-bridge.mjs +88 -12
- package/dist/index.js +17 -15
- package/package.json +1 -1
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
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
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
|
|
package/dist/h2-bridge.mjs
CHANGED
|
@@ -95,31 +95,102 @@ async function readMessage() {
|
|
|
95
95
|
const configBuf = await readMessage();
|
|
96
96
|
if (!configBuf) process.exit(1);
|
|
97
97
|
|
|
98
|
-
|
|
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
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
165
|
+
// After first I/O, only the activity idle (if enabled) applies.
|
|
166
|
+
armBridgeTimeout(idleTimeoutMs);
|
|
110
167
|
}
|
|
111
168
|
|
|
112
169
|
function killBridge() {
|
|
113
|
-
|
|
170
|
+
clearBridgeTimeout();
|
|
171
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
114
172
|
client.destroy();
|
|
115
173
|
process.exit(1);
|
|
116
174
|
}
|
|
117
175
|
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|