pi-cursor-bridge 0.1.12 → 0.1.13

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,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-bridge",
3
- "version": "5.8.2+codex.20260904092304",
3
+ "version": "5.9.0+codex.20260905074950",
4
4
  "description": "Evidence-backed Cursor Context Engine search and bounded Cursor Agent execution, including a UI-suppressed minimal runtime.",
5
5
  "author": {
6
6
  "name": "Vanyangyang"
package/README.md CHANGED
@@ -10,6 +10,6 @@ pi install npm:pi-cursor-bridge
10
10
 
11
11
  Restart Pi after installation. Initialize the current project by asking Pi to initialize Cursor Bridge for the absolute project path, then use it normally. The package includes the `cce-routing` and `cursor-delegate` Skills and registers the native Cursor Bridge MCP tools directly in Pi.
12
12
 
13
- This Pi package embeds Cursor Bridge 5.8.2. Cursor must be installed and signed in. Current end-to-end compatibility claims remain scoped to the environments documented in the main repository.
13
+ This Pi package embeds Cursor Bridge 5.9.0. Cursor must be installed and signed in. Current end-to-end compatibility claims remain scoped to the environments documented in the main repository.
14
14
 
15
15
  Full documentation: [English](https://github.com/Vanyangyang/cursor-bridge#readme) · [简体中文](https://github.com/Vanyangyang/cursor-bridge/blob/main/README.zh-CN.md)
@@ -22604,7 +22604,7 @@ function updateCursorSessionRegistry(filePath, mutator, options = {}) {
22604
22604
  // server.mjs
22605
22605
  init_cursor_ensure_core();
22606
22606
  init_lifecycle_paths();
22607
- var PLUGIN_VERSION = "5.8.2";
22607
+ var PLUGIN_VERSION = "5.9.0";
22608
22608
  var CDP_PORT2 = Number(process.env.CURSOR_BRIDGE_CDP_PORT || 9223);
22609
22609
  var ORIGIN = `http://localhost:${CDP_PORT2}`;
22610
22610
  var QUERY_TIMEOUT = Number(process.env.CURSOR_BRIDGE_TIMEOUT || 3e5);
@@ -23690,6 +23690,7 @@ var CursorBridge = class {
23690
23690
  this.projectPath = options.projectPath !== void 0 ? resolve7(String(options.projectPath)) : persistedWorkspace && persistedWorkspace.projectPath || null;
23691
23691
  this.workspaceSource = options.projectPath !== void 0 ? "constructor" : persistedWorkspace ? "persistent_init" : "auto_detect";
23692
23692
  this.workspaceUpdatedAt = persistedWorkspace && persistedWorkspace.updatedAt || null;
23693
+ this.workspaceConfirmationRequired = this.workspaceKey === "default" && !!persistedWorkspace;
23693
23694
  this.modelPreferencesFile = options.modelPreferencesFile === null ? null : resolve7(options.modelPreferencesFile || resolveCursorModelPreferencesFile());
23694
23695
  this.modelPreferences = readCursorModelPreferences(this.modelPreferencesFile);
23695
23696
  this.sessionFile = options.sessionFile === null ? null : resolve7(options.sessionFile || resolveCursorSessionRegistryFile());
@@ -23712,6 +23713,8 @@ var CursorBridge = class {
23712
23713
  const resolvedProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
23713
23714
  return {
23714
23715
  workspaceKey: this.workspaceKey,
23716
+ workspaceConfirmationRequired: this.workspaceConfirmationRequired,
23717
+ workspaceBindingWarning: this.workspaceConfirmationRequired ? "This adapter has no host workspace identity. Run cursor_init with the intended project before using its shared saved binding." : null,
23715
23718
  projectPath: resolvedProjectPath,
23716
23719
  workspaceSource: this.projectPath ? this.workspaceSource : resolvedProjectPath ? "host_auto_detect" : this.workspaceSource,
23717
23720
  workspaceUpdatedAt: this.workspaceUpdatedAt,
@@ -23746,7 +23749,10 @@ var CursorBridge = class {
23746
23749
  agentLabel: session.agentLabel || null,
23747
23750
  turnIndex: Number(session.turnIndex || 0),
23748
23751
  activeTaskId: session.activeTaskId || null,
23749
- lastTask: session.lastTask || null,
23752
+ lastTask: session.lastTask ? {
23753
+ ...session.lastTask,
23754
+ resultUnavailable: this.tasks.get(session.lastTask.taskId)?.result == null
23755
+ } : null,
23750
23756
  modelPreference: session.modelPreference || null,
23751
23757
  readOnly: session.scopeEnvelope && session.scopeEnvelope.readOnly === true,
23752
23758
  allowedPaths: session.scopeEnvelope && Array.isArray(session.scopeEnvelope.allowedPaths) ? session.scopeEnvelope.allowedPaths : [],
@@ -23758,6 +23764,7 @@ var CursorBridge = class {
23758
23764
  };
23759
23765
  }
23760
23766
  _claimNewSession({ taskId, projectPath, readOnly, allowedPaths, modelPreference, requestId, timeoutMs }) {
23767
+ this._ensureTaskCapacity();
23761
23768
  if (!this.sessionFile) throw cursorSessionError("SESSION_STORAGE_DISABLED", "persistent session storage is disabled");
23762
23769
  const sessionId = createCursorSessionId();
23763
23770
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -23803,6 +23810,7 @@ var CursorBridge = class {
23803
23810
  if (requestId && session.lastRequestId === requestId) {
23804
23811
  return { duplicate: true, session: { ...session } };
23805
23812
  }
23813
+ this._ensureTaskCapacity();
23806
23814
  if (!sameSessionProject(session.projectPath, projectPath)) {
23807
23815
  throw cursorSessionError("SESSION_WORKSPACE_MISMATCH", `session is bound to ${session.projectPath}`);
23808
23816
  }
@@ -23822,6 +23830,15 @@ var CursorBridge = class {
23822
23830
  if (session.state !== "ready") {
23823
23831
  throw cursorSessionError("SESSION_NOT_READY", `state=${session.state}; recovery=${session.recoveryState || "none"}`);
23824
23832
  }
23833
+ if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && this.tasks.has(session.lastTask.taskId)) {
23834
+ throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", `read cursor_status(task_id=${session.lastTask.taskId}) before continuing`);
23835
+ }
23836
+ if (session.lastTask?.status === "completed" && !session.lastTask.resultCollectedAt && !this.tasks.has(session.lastTask.taskId) && session.recoveryState !== "reconciled_result_uncollected") {
23837
+ throw cursorSessionError("SESSION_RECONCILE_REQUIRED", "the prior reply was not read before this adapter restarted; reconcile the exact Agent before continuing");
23838
+ }
23839
+ if (session.recoveryState === "reconciled_result_uncollected" && (Number(session.turnIndex) <= 1 || session.responseBaseline)) {
23840
+ throw cursorSessionError("SESSION_RESULT_UNCOLLECTED", "collect the interrupted reply with cursor_session_control(action=collect_result) before continuing");
23841
+ }
23825
23842
  if (!session.agentId) throw cursorSessionError("SESSION_AGENT_NOT_BOUND", sessionId);
23826
23843
  const envelope = session.scopeEnvelope || { readOnly: false, allowedPaths: [] };
23827
23844
  if (envelope.readOnly === true) {
@@ -23841,6 +23858,7 @@ var CursorBridge = class {
23841
23858
  session.epoch = epoch;
23842
23859
  session.activeTaskId = taskId;
23843
23860
  session.lastRequestId = requestId || null;
23861
+ delete session.responseBaseline;
23844
23862
  session.lease = {
23845
23863
  taskId,
23846
23864
  instanceId: this.sessionInstanceId,
@@ -23869,6 +23887,20 @@ var CursorBridge = class {
23869
23887
  });
23870
23888
  job.sessionState = "busy";
23871
23889
  }
23890
+ _persistSessionResponseBaseline(job) {
23891
+ if (!job.sessionId) return;
23892
+ const baseline = job.responseBaseline;
23893
+ if (!baseline || !["messageCount", "replyLength", "replyHash"].every((key) => Number.isFinite(baseline[key]))) {
23894
+ throw cursorSessionError("SESSION_BASELINE_UNAVAILABLE", "a numeric response baseline is required before sending");
23895
+ }
23896
+ updateCursorSessionRegistry(this.sessionFile, (registry2) => {
23897
+ const session = registry2.sessions[job.sessionId];
23898
+ if (!session || session.activeTaskId !== job.id || Number(session.epoch) !== Number(job.sessionEpoch)) {
23899
+ throw cursorSessionError("SESSION_STALE_SENDER", "the sender no longer owns this session turn");
23900
+ }
23901
+ session.responseBaseline = { messageCount: baseline.messageCount, replyLength: baseline.replyLength, replyHash: baseline.replyHash };
23902
+ });
23903
+ }
23872
23904
  _settleSessionJob(job, outcome, options = {}) {
23873
23905
  if (!job || !job.sessionId || !this.sessionFile) return;
23874
23906
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -23896,6 +23928,7 @@ var CursorBridge = class {
23896
23928
  }
23897
23929
  session.activeTaskId = null;
23898
23930
  session.lease = null;
23931
+ if (outcome !== "completed") delete session.responseBaseline;
23899
23932
  session.recoveryState = null;
23900
23933
  session.attention = null;
23901
23934
  session.state = session.agentId ? "ready" : "failed";
@@ -23910,6 +23943,19 @@ var CursorBridge = class {
23910
23943
  job.sessionError = error2 instanceof Error ? error2.message : String(error2);
23911
23944
  }
23912
23945
  }
23946
+ _markTaskResultCollected(job) {
23947
+ if (!isTerminalTask(job) || job.result == null || job.resultCollectedAt) return;
23948
+ const now = (/* @__PURE__ */ new Date()).toISOString();
23949
+ if (job.sessionId && this.sessionFile) {
23950
+ updateCursorSessionRegistry(this.sessionFile, (registry2) => {
23951
+ const session = registry2.sessions[job.sessionId];
23952
+ if (!session || session.lastTask?.taskId !== job.id || Number(session.epoch) !== Number(job.sessionEpoch)) return;
23953
+ session.lastTask.resultCollectedAt = now;
23954
+ delete session.responseBaseline;
23955
+ });
23956
+ }
23957
+ job.resultCollectedAt = now;
23958
+ }
23913
23959
  sessionStatus(sessionId) {
23914
23960
  const session = this._readSession(sessionId);
23915
23961
  return {
@@ -23918,17 +23964,28 @@ var CursorBridge = class {
23918
23964
  ...session ? this._sessionView(session) : { sessionId: String(sessionId || "") }
23919
23965
  };
23920
23966
  }
23921
- async _reconcileSession(sessionId) {
23967
+ async _reconcileSession(sessionId, { collectResult = false } = {}) {
23968
+ const action = collectResult ? "collect_result" : "reconcile";
23922
23969
  const initial = this._readSession(sessionId);
23923
- if (!initial) return { found: false, action: "reconcile", sessionId };
23924
- if (initial.state === "ready" || initial.state === "closed" || initial.state === "failed") {
23925
- return { found: true, changed: false, action: "reconcile", ...this._sessionView(initial) };
23970
+ if (!initial) return { found: false, action, sessionId };
23971
+ if (collectResult && initial.recoveryState === "reconciled_result_collected") {
23972
+ return { found: true, changed: false, action, ...this._sessionView(initial), state: "already_collected", resultPersisted: false };
23973
+ }
23974
+ const unreadAfterRestart = initial.lastTask?.status === "completed" && !initial.lastTask.resultCollectedAt && !this.tasks.has(initial.lastTask.taskId);
23975
+ if (!collectResult && initial.state === "ready" && !unreadAfterRestart || initial.state === "closed" || initial.state === "failed") {
23976
+ return { found: true, changed: false, action, ...this._sessionView(initial) };
23977
+ }
23978
+ if (collectResult && (initial.state !== "ready" || initial.activeTaskId || initial.lastTask?.status !== "completed" || initial.recoveryState !== "reconciled_result_uncollected")) {
23979
+ throw cursorSessionError("SESSION_RESULT_NOT_READY", "reconcile a completed turn before collecting its reply");
23980
+ }
23981
+ if (collectResult && Number(initial.turnIndex) > 1 && !initial.responseBaseline) {
23982
+ throw cursorSessionError("SESSION_RESULT_BASELINE_UNAVAILABLE", "this older interrupted turn has no reply signature; inspect the original Agent manually before continuing");
23926
23983
  }
23927
23984
  if (!initial.agentId) {
23928
23985
  return {
23929
23986
  found: true,
23930
23987
  changed: false,
23931
- action: "reconcile",
23988
+ action,
23932
23989
  state: "agent_missing",
23933
23990
  attention: "No exact agentId is available. Confirm the Cursor task state manually before explicit abandon.",
23934
23991
  ...this._sessionView(initial)
@@ -23946,21 +24003,44 @@ var CursorBridge = class {
23946
24003
  effectiveExecution: "parallel_agent",
23947
24004
  lastRecoveryAt: null,
23948
24005
  recoveryState: null,
23949
- error: null
24006
+ error: null,
24007
+ responseBaseline: initial.responseBaseline || null
23950
24008
  };
23951
24009
  const observed = await this._readStableParallelEntry(probe);
24010
+ let collectedReply;
24011
+ if (collectResult) {
24012
+ if (!observed.stable || !observed.entry || observed.entry.showSpinner || classifyParallelTerminalIcon(observed.entry.icon) !== "completed") {
24013
+ throw cursorSessionError("SESSION_RESULT_NOT_READY", "the exact Agent must have a stable completed state");
24014
+ }
24015
+ try {
24016
+ collectedReply = await this._withUiLock(() => this._collectParallelAgent(probe));
24017
+ } catch (error2) {
24018
+ if (probe.uiDiagnostic && error2 && typeof error2 === "object") error2.uiDiagnostic = probe.uiDiagnostic;
24019
+ throw error2;
24020
+ }
24021
+ if (!String(collectedReply || "").trim()) throw cursorSessionError("SESSION_RESULT_UNAVAILABLE", "no final reply was collected");
24022
+ }
23952
24023
  let result;
23953
24024
  updateCursorSessionRegistry(this.sessionFile, (registry2) => {
23954
24025
  const session = registry2.sessions[sessionId];
23955
24026
  if (!session) {
23956
- result = { found: false, action: "reconcile", sessionId };
24027
+ result = { found: false, action, sessionId };
23957
24028
  return;
23958
24029
  }
23959
- if (Number(session.epoch || 0) !== Number(initial.epoch || 0) || session.activeTaskId !== initial.activeTaskId) {
23960
- result = { found: true, changed: false, action: "reconcile", state: "stale_observation", ...this._sessionView(session) };
24030
+ if (Number(session.epoch || 0) !== Number(initial.epoch || 0) || session.activeTaskId !== initial.activeTaskId || session.state !== initial.state) {
24031
+ result = { found: true, changed: false, action, state: "stale_observation", ...this._sessionView(session) };
23961
24032
  return;
23962
24033
  }
23963
24034
  const now = (/* @__PURE__ */ new Date()).toISOString();
24035
+ if (collectResult) {
24036
+ session.lastTask.resultCollectedAt = now;
24037
+ delete session.responseBaseline;
24038
+ session.recoveryState = "reconciled_result_collected";
24039
+ session.attention = null;
24040
+ session.updatedAt = now;
24041
+ result = { found: true, changed: true, action, ...this._sessionView(session), result: collectedReply, resultPersisted: false, uiDiagnostic: probe.uiDiagnostic || null };
24042
+ return;
24043
+ }
23964
24044
  if (!observed.stable || !observed.entry) {
23965
24045
  session.state = "needs_attention";
23966
24046
  session.recoveryState = probe.recoveryState || "history_unavailable";
@@ -24000,7 +24080,8 @@ var CursorBridge = class {
24000
24080
  session.lease = null;
24001
24081
  session.state = "ready";
24002
24082
  session.recoveryState = terminalClass === "completed" ? "reconciled_result_uncollected" : null;
24003
- session.attention = terminalClass === "completed" ? "The interrupted turn completed in Cursor, but its reply was not persisted. Inspect that Agent before the next turn." : null;
24083
+ session.attention = terminalClass === "completed" ? Number(session.turnIndex) > 1 && !session.responseBaseline ? "This older interrupted turn has no saved reply signature. Inspect the original Agent manually before continuing; automatic collection cannot identify its reply safely." : "The interrupted turn completed in Cursor, but its reply was not persisted. Use cursor_session_control(action=collect_result) before the next turn." : null;
24084
+ if (terminalClass !== "completed") delete session.responseBaseline;
24004
24085
  session.updatedAt = now;
24005
24086
  result = { found: true, changed: true, action: "reconcile", state: terminalClass, ...this._sessionView(session) };
24006
24087
  });
@@ -24010,10 +24091,11 @@ var CursorBridge = class {
24010
24091
  const id = String(sessionId || "").trim();
24011
24092
  if (!id) throw cursorSessionError("SESSION_REQUIRED", "session_id must not be empty");
24012
24093
  const action = String(options.action || "").trim().toLowerCase();
24013
- if (!["reconcile", "close", "forget", "abandon"].includes(action)) {
24014
- throw cursorSessionError("SESSION_ACTION_UNSUPPORTED", "expected reconcile, close, forget, or abandon");
24094
+ if (!["reconcile", "collect_result", "close", "forget", "abandon"].includes(action)) {
24095
+ throw cursorSessionError("SESSION_ACTION_UNSUPPORTED", "expected reconcile, collect_result, close, forget, or abandon");
24015
24096
  }
24016
24097
  if (action === "reconcile") return this._reconcileSession(id);
24098
+ if (action === "collect_result") return this._reconcileSession(id, { collectResult: true });
24017
24099
  let result;
24018
24100
  updateCursorSessionRegistry(this.sessionFile, (registry2) => {
24019
24101
  const session = registry2.sessions[id];
@@ -24071,6 +24153,7 @@ var CursorBridge = class {
24071
24153
  }
24072
24154
  const previousProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
24073
24155
  const saved = writeWorkspaceBinding(this.workspaceFile, this.workspaceKey, projectPath);
24156
+ this.workspaceConfirmationRequired = false;
24074
24157
  this.projectPath = saved.projectPath;
24075
24158
  this.workspaceSource = "persistent_init";
24076
24159
  this.workspaceUpdatedAt = saved.updatedAt;
@@ -24265,6 +24348,7 @@ var CursorBridge = class {
24265
24348
  const text = String(query || "").trim();
24266
24349
  if (!text) throw new Error("query must not be empty");
24267
24350
  if (text.length > 2e4) throw new Error("query exceeds the 20,000-character limit");
24351
+ this._assertWorkspaceConfirmed();
24268
24352
  if (this._hasGlobalReservation()) {
24269
24353
  throw new Error("A global Cursor reservation has an unconfirmed Stop state; resolve blockingTaskIds from cursor_status first");
24270
24354
  }
@@ -24277,7 +24361,9 @@ var CursorBridge = class {
24277
24361
  allowedPaths: [],
24278
24362
  modelPreference: this._modelPreferenceFor("cce")
24279
24363
  });
24280
- return normalizeCceSearchResult(await job.promise);
24364
+ const result = normalizeCceSearchResult(await job.promise);
24365
+ this._markTaskResultCollected(job);
24366
+ return result;
24281
24367
  }
24282
24368
  // Unlisted compatibility aliases for clients that cached the pre-3.0 tool surface.
24283
24369
  async search(query) {
@@ -24293,6 +24379,7 @@ var CursorBridge = class {
24293
24379
  const text = String(prompt || "").trim();
24294
24380
  if (!text) throw new Error("prompt must not be empty");
24295
24381
  if (text.length > 1e5) throw new Error("prompt exceeds the 100,000-character limit");
24382
+ this._assertWorkspaceConfirmed();
24296
24383
  if (this._hasGlobalReservation()) {
24297
24384
  throw new Error("A global Cursor reservation has an unconfirmed Stop state; no new task may be submitted until it is explicitly recovered or released");
24298
24385
  }
@@ -24428,6 +24515,7 @@ var CursorBridge = class {
24428
24515
  return `cursor-${Date.now().toString(36)}-${this.nextTaskId++}`;
24429
24516
  }
24430
24517
  _enqueue(kind, prompt, options) {
24518
+ this._ensureTaskCapacity();
24431
24519
  const id = options.taskId || this._nextTaskId();
24432
24520
  let resolvePromise;
24433
24521
  let rejectPromise;
@@ -24471,6 +24559,8 @@ var CursorBridge = class {
24471
24559
  targetUiFlavor: null,
24472
24560
  fallbackReason: null,
24473
24561
  result: null,
24562
+ resultCollectedAt: null,
24563
+ waitDeadlineAt: null,
24474
24564
  error: null,
24475
24565
  cancelRequested: false,
24476
24566
  cancelReason: null,
@@ -25613,6 +25703,7 @@ var CursorBridge = class {
25613
25703
  await this._applyModelPreference(c, job.modelPreference, job);
25614
25704
  this._throwIfCancelledBeforeSend(job);
25615
25705
  job.responseBaseline = await this._captureSessionResponseBaseline(c);
25706
+ this._persistSessionResponseBaseline(job);
25616
25707
  const providerErrorBaseline = providerErrorSignature(await this._readProviderError(c));
25617
25708
  const filled = await this._fillPrompt(c, job.prompt, job);
25618
25709
  if (filled === "NO_INPUT") await this._throwChatPanelUnavailableAfterNoInput(c);
@@ -25687,8 +25778,16 @@ var CursorBridge = class {
25687
25778
  job.monitorPromise = monitorPromise;
25688
25779
  return true;
25689
25780
  }
25781
+ _taskWaitDeadline(job, timeoutMs = job?.timeoutMs) {
25782
+ if (!job) return Date.now() + timeoutMs;
25783
+ if (!Number.isFinite(job.waitDeadlineAt)) {
25784
+ const sentAt = Date.parse(job.sentAt || "");
25785
+ job.waitDeadlineAt = (Number.isFinite(sentAt) ? sentAt : Date.now()) + timeoutMs;
25786
+ }
25787
+ return job.waitDeadlineAt;
25788
+ }
25690
25789
  async _monitorParallelAgent(job, generation) {
25691
- const started = Date.now();
25790
+ const deadline = this._taskWaitDeadline(job);
25692
25791
  let sawGenerating = false;
25693
25792
  let completedStable = 0;
25694
25793
  let missingPolls = 0;
@@ -25697,7 +25796,7 @@ var CursorBridge = class {
25697
25796
  let lastTerminalErrorSignature = "";
25698
25797
  let collectionAttempts = 0;
25699
25798
  let lastCollectionError = "";
25700
- while (Date.now() - started < job.timeoutMs) {
25799
+ while (Date.now() < deadline) {
25701
25800
  if (!this._monitorOwns(job, generation)) return;
25702
25801
  await sleep2(1400);
25703
25802
  if (!this._monitorOwns(job, generation)) return;
@@ -25815,51 +25914,63 @@ var CursorBridge = class {
25815
25914
  }
25816
25915
  throw new Error(`After opening ${agentId}, it could not be confirmed as the selected Agent`);
25817
25916
  }
25917
+ async _withRestoredAgentSelection(c, job, operation) {
25918
+ const entries = await this._readAgentEntries(c, true);
25919
+ const previousSelectedId = (entries.find((entry) => entry.isSelected) || {}).id || null;
25920
+ try {
25921
+ return await operation();
25922
+ } finally {
25923
+ if (previousSelectedId && previousSelectedId !== job.agentId) {
25924
+ try {
25925
+ const opened = await this._requestExactAgentSelection(c, previousSelectedId);
25926
+ if (opened !== "OPENED") throw new Error(`Unable to restore ${previousSelectedId}: ${opened}`);
25927
+ await this._waitForSelectedAgent(c, previousSelectedId);
25928
+ } catch (error2) {
25929
+ job.uiDiagnostic = { ...job.uiDiagnostic, selectionRestore: { state: "failed", agentId: previousSelectedId, detail: String(error2.message).slice(0, 500) } };
25930
+ }
25931
+ }
25932
+ }
25933
+ }
25818
25934
  async _collectParallelAgent(job) {
25819
25935
  const page = await findPage({ targetId: job.targetId, purpose: "parallel_agent" });
25820
25936
  const c = makeClient(page.webSocketDebuggerUrl);
25821
25937
  await c.ready;
25822
- let previousSelectedId = null;
25823
25938
  try {
25824
- const entries = await this._readAgentEntries(c, true);
25825
- previousSelectedId = (entries.find((e) => e.isSelected) || {}).id || null;
25826
- const opened = await evalJS(c, exprOpenAgent(job.agentId));
25827
- if (opened !== "OPENED") throw new Error(`Unable to open ${job.agentId}: ${opened}`);
25828
- await this._closeHistory(c);
25829
- await this._waitForSelectedAgent(c, job.agentId);
25830
- let answer = "";
25831
- let lastKey = "";
25832
- let stable = 0;
25833
- for (let i = 0; i < 80; i++) {
25834
- const snap = await this._readResponseSnapshot(c);
25835
- const candidate = String(await evalJS(c, EXPR_EXTRACT) || "").trim();
25836
- const hasNewTurn = isSessionTurnReplyReady(job.responseBaseline, snap);
25837
- if (candidate && hasNewTurn && Number(snap.stop || 0) === 0) {
25838
- const key = `${snap.replyLength}:${snap.replyHash}`;
25839
- if (key === lastKey) stable++;
25840
- else {
25841
- lastKey = key;
25842
- stable = 0;
25843
- }
25844
- if (stable >= 1) {
25845
- answer = candidate;
25846
- break;
25939
+ return await this._withRestoredAgentSelection(c, job, async () => {
25940
+ const opened = await evalJS(c, exprOpenAgent(job.agentId));
25941
+ if (opened !== "OPENED") throw new Error(`Unable to open ${job.agentId}: ${opened}`);
25942
+ await this._closeHistory(c);
25943
+ await this._waitForSelectedAgent(c, job.agentId);
25944
+ let answer = "";
25945
+ let lastKey = "";
25946
+ let stable = 0;
25947
+ for (let i = 0; i < 80; i++) {
25948
+ const snap = await this._readResponseSnapshot(c);
25949
+ const candidate = String(await evalJS(c, EXPR_EXTRACT) || "").trim();
25950
+ const hasNewTurn = isSessionTurnReplyReady(job.responseBaseline, snap);
25951
+ if (candidate && hasNewTurn && Number(snap.stop || 0) === 0) {
25952
+ const key = `${snap.replyLength}:${snap.replyHash}`;
25953
+ if (key === lastKey) stable++;
25954
+ else {
25955
+ lastKey = key;
25956
+ stable = 0;
25957
+ }
25958
+ if (stable >= 1) {
25959
+ answer = candidate;
25960
+ break;
25961
+ }
25847
25962
  }
25963
+ await sleep2(300);
25848
25964
  }
25849
- await sleep2(300);
25850
- }
25851
- if (!answer) throw new Error(`${job.agentId} was opened, but no final assistant reply was found`);
25852
- if (previousSelectedId && previousSelectedId !== job.agentId) {
25853
- if (await this._ensureHistoryOpen(c)) {
25854
- await evalJS(c, exprOpenAgent(previousSelectedId));
25855
- await this._closeHistory(c);
25856
- await this._waitForSelectedAgent(c, previousSelectedId);
25857
- }
25858
- }
25859
- return answer;
25965
+ if (!answer) throw new Error(`${job.agentId} was opened, but no final assistant reply was found`);
25966
+ return answer;
25967
+ });
25860
25968
  } finally {
25861
- await this._closeHistory(c);
25862
- c.close();
25969
+ try {
25970
+ await this._closeHistory(c);
25971
+ } finally {
25972
+ c.close();
25973
+ }
25863
25974
  }
25864
25975
  }
25865
25976
  _reapWithoutResult(job, error2, evidence) {
@@ -25874,7 +25985,6 @@ var CursorBridge = class {
25874
25985
  job.terminalEvidence = evidence || "stable_completed_history_icon";
25875
25986
  job.recoveryState = "terminal_result_uncollected";
25876
25987
  job.reservationScope = uncertainSubmissionReservationScope(job, error2);
25877
- job.recoveryState = error2.recoveryState || "monitoring_uncertain_submission";
25878
25988
  this._safeSettleSessionJob(job, "terminal_result_uncollected", { needsAttention: true });
25879
25989
  }
25880
25990
  _abandonJob(job, reason) {
@@ -25951,6 +26061,7 @@ var CursorBridge = class {
25951
26061
  job.phase = "running";
25952
26062
  job.error = null;
25953
26063
  job.recoveryState = "monitoring";
26064
+ if (options.reattach !== false && !job.cancelRequested) job.waitDeadlineAt = Date.now() + job.timeoutMs;
25954
26065
  const attached = options.reattach !== false && !job.cancelRequested && this._startParallelMonitor(job);
25955
26066
  return { changed: attached, state: "running", monitorReattached: attached, task: this._taskView(job, true) };
25956
26067
  }
@@ -26349,12 +26460,12 @@ var CursorBridge = class {
26349
26460
  }).catch((e) => console.error("\u26A0\uFE0F Failed to restore the original Cursor Agent: " + e.message));
26350
26461
  }
26351
26462
  async _waitComplete(c, timeoutMs = QUERY_TIMEOUT, baselineCount = 0, job = null, providerErrorBaseline = "") {
26352
- const start = Date.now();
26463
+ const deadline = this._taskWaitDeadline(job, timeoutMs);
26353
26464
  const INTERVAL = 1e3;
26354
26465
  let sawStop = false;
26355
26466
  let lastReplyKey = "", stableReply = 0;
26356
26467
  await sleep2(1200);
26357
- while (Date.now() - start < timeoutMs) {
26468
+ while (Date.now() < deadline) {
26358
26469
  await this._throwIfNewProviderError(c, providerErrorBaseline);
26359
26470
  if (job && job.cancelRequested) {
26360
26471
  if (job.agentId) {
@@ -26419,6 +26530,7 @@ var CursorBridge = class {
26419
26530
  throw new Error(`Cursor task timed out (${timeoutMs}ms) before generation was confirmed stopped with a complete assistant reply${taskHint}`);
26420
26531
  }
26421
26532
  _taskView(job, includeResult = false) {
26533
+ if (includeResult) this._markTaskResultCollected(job);
26422
26534
  const view = {
26423
26535
  taskId: job.id,
26424
26536
  kind: job.kind,
@@ -26457,6 +26569,8 @@ var CursorBridge = class {
26457
26569
  monitorAttached: job.monitorAttached,
26458
26570
  monitorGeneration: job.monitorGeneration,
26459
26571
  resultUnavailable: job.resultUnavailable,
26572
+ resultCollectedAt: job.resultCollectedAt || null,
26573
+ waitDeadlineAt: Number.isFinite(job.waitDeadlineAt) ? new Date(job.waitDeadlineAt).toISOString() : null,
26460
26574
  terminalEvidence: job.terminalEvidence,
26461
26575
  providerError: job.providerError,
26462
26576
  uiDiagnostic: job.uiDiagnostic,
@@ -26466,7 +26580,7 @@ var CursorBridge = class {
26466
26580
  blocksFifo: this.activeParallel.has(job.id) && !isTerminalTask(job),
26467
26581
  blocksAll: this.activeParallel.has(job.id) && !isTerminalTask(job) && job.reservationScope === "global"
26468
26582
  };
26469
- if (includeResult || isTerminalTask(job)) view.result = job.result;
26583
+ if (includeResult) view.result = job.result;
26470
26584
  if (job.phase === "orphaned") {
26471
26585
  if (job.recoveryState === "terminal_result_uncollected") {
26472
26586
  view.attention = "Agent History proves that the underlying task ended, but its final reply has not been collected. Retry with reap, or explicitly abandon only if a missing reply is acceptable.";
@@ -26476,11 +26590,22 @@ var CursorBridge = class {
26476
26590
  }
26477
26591
  return view;
26478
26592
  }
26479
- _trimTasks() {
26480
- if (this.tasks.size <= 50) return;
26593
+ _assertWorkspaceConfirmed() {
26594
+ if (this.workspaceConfirmationRequired) {
26595
+ throw new Error("WORKSPACE_CONFIRMATION_REQUIRED: the saved default binding has no host workspace identity. Run cursor_init with the intended project before submitting work.");
26596
+ }
26597
+ }
26598
+ _ensureTaskCapacity() {
26599
+ this._trimTasks(49);
26600
+ if (this.tasks.size >= 50) {
26601
+ throw new Error("TASK_RETENTION_FULL: 50 tasks are active or have unread replies. Read unreadResultTaskIds with cursor_status(task_id), or wait for active tasks before submitting more work.");
26602
+ }
26603
+ }
26604
+ _trimTasks(limit = 50) {
26605
+ if (this.tasks.size <= limit) return;
26481
26606
  for (const [id, job] of this.tasks) {
26482
- if (isTerminalTask(job)) this.tasks.delete(id);
26483
- if (this.tasks.size <= 50) break;
26607
+ if (isTerminalTask(job) && (job.result == null || job.resultCollectedAt)) this.tasks.delete(id);
26608
+ if (this.tasks.size <= limit) break;
26484
26609
  }
26485
26610
  }
26486
26611
  async status(taskId = "") {
@@ -26510,6 +26635,8 @@ var CursorBridge = class {
26510
26635
  blockedQueuedCount: this.activeParallel.size > 0 ? this.queue.filter((job) => globalBlocked || job.effectiveExecution !== "parallel_agent").length : 0,
26511
26636
  activeParallel: [...this.activeParallel.values()].map((job) => this._taskView(job)),
26512
26637
  recentTasks: [...this.tasks.values()].slice(-10).map((job) => this._taskView(job)),
26638
+ unreadResultTaskIds: [...this.tasks.values()].filter((job) => isTerminalTask(job) && job.result != null && !job.resultCollectedAt).map((job) => job.id),
26639
+ taskRetentionLimit: 50,
26513
26640
  cdpPort: CDP_PORT2,
26514
26641
  lifecycle: this._lastLifecycle || {
26515
26642
  adapterPid: process.pid,
@@ -26575,7 +26702,7 @@ function buildToolDefinitions(bridgeInstance) {
26575
26702
  background: { type: "boolean", default: true, description: "When true, return the task ID immediately. When false, wait for the task to finish or need attention." },
26576
26703
  execution: { type: "string", enum: ["fifo", "parallel_agent"], default: "fifo", description: "fifo is the first-in, first-out serial queue and runs one task at a time in a clean chat. parallel_agent creates a separate top-level Cursor Agent." },
26577
26704
  read_only: { type: "boolean", default: false, description: "Set true when Cursor must not change the workspace." },
26578
- timeout_ms: { type: "integer", minimum: 3e4, maximum: 9e5, default: 6e5, description: "How long to wait before timing out, in milliseconds. The default is 10 minutes." },
26705
+ timeout_ms: { type: "integer", minimum: 3e4, maximum: 9e5, default: 6e5, description: "One monitoring budget after submission, shared by FIFO and automatic Agent recovery. Expiry needs attention; it does not cancel work. Explicit reap may start a new budget. The default is 10 minutes." },
26579
26706
  allowed_paths: { type: "array", items: { type: "string" }, description: "Workspace-relative paths Cursor may write. Parallel write tasks require non-overlapping paths. This declaration is not a filesystem sandbox." },
26580
26707
  completion_contract: { type: "string", description: "Optional acceptance checks or a required final-report format." },
26581
26708
  session_mode: { type: "string", enum: [...CURSOR_SESSION_MODES], default: "isolated", description: "isolated preserves the current clean-task behavior. create starts an update-safe persistent session. continue sends one new turn to the exact session_id." },
@@ -26603,12 +26730,12 @@ function buildToolDefinitions(bridgeInstance) {
26603
26730
  },
26604
26731
  {
26605
26732
  name: "cursor_session_control",
26606
- description: "Recover, close, or forget one exact persistent cursor_do session. reconcile checks the exact Agent twice and never resends. close prevents future sends but does not stop or delete the Cursor Agent. An active or uncertain session cannot be closed. abandon is the explicit last resort when exact stop evidence is unavailable. forget requires confirm=true and is allowed only after close; it deletes only the Bridge mapping.",
26733
+ description: "Recover, collect a reply, close, or forget one exact persistent cursor_do session. reconcile checks the exact Agent twice and never resends. collect_result reads the completed reconciled turn from that Agent without sending a prompt; it may temporarily select that Agent. close prevents future sends but does not stop or delete the Cursor Agent. An active or uncertain session cannot be closed. abandon is the explicit last resort when exact stop evidence is unavailable. forget requires confirm=true and is allowed only after close; it deletes only the Bridge mapping.",
26607
26734
  inputSchema: {
26608
26735
  type: "object",
26609
26736
  properties: {
26610
26737
  session_id: { type: "string", description: "The exact session ID returned by cursor_do(session_mode=create)." },
26611
- action: { type: "string", enum: ["reconcile", "close", "forget", "abandon"], description: "reconcile reads the exact Agent state; close ends safe continuity; forget removes a closed mapping; abandon releases an uncertain mapping without stop proof." },
26738
+ action: { type: "string", enum: ["reconcile", "collect_result", "close", "forget", "abandon"], description: "reconcile reads the exact Agent state; collect_result retrieves its completed latest turn before continuing; close ends safe continuity; forget removes a closed mapping; abandon releases an uncertain mapping without stop proof." },
26612
26739
  confirm: { type: "boolean", default: false, description: "Required for forget and abandon." },
26613
26740
  reason: { type: "string", description: "Required and non-empty for abandon." },
26614
26741
  acknowledge_may_still_write: { type: "boolean", default: false, description: "Required for abandon; acknowledges that the underlying Cursor Agent may still run or write." }
@@ -10,7 +10,7 @@ const hostWorkspaceId = hostCwd.replace(/\\/g, "/").toLowerCase();
10
10
  export default createStdioMcpExtension({
11
11
  label: "Cursor Bridge",
12
12
  clientName: "pi-cursor-bridge",
13
- packageVersion: "0.1.12",
13
+ packageVersion: "0.1.13",
14
14
  serverName: "cursor-bridge",
15
15
  serverScript: join(packageRoot, "dist", "cursor-bridge.mjs"),
16
16
  cwd: hostCwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-bridge",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Use Cursor Context Engine and bounded, explicitly continuous Cursor Agent execution from the Pi coding agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,6 +47,6 @@
47
47
  },
48
48
  "piPackage": {
49
49
  "embeddedProduct": "Cursor Bridge",
50
- "embeddedProductVersion": "5.8.2"
50
+ "embeddedProductVersion": "5.9.0"
51
51
  }
52
52
  }