pi-cursor-bridge 0.1.11 → 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.1+codex.20260903121439",
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.1. 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.1";
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);
@@ -22963,10 +22963,7 @@ function createChatPanelUnavailableError(snapshot) {
22963
22963
  error2.uiDiagnostic = uiDiagnostic;
22964
22964
  return error2;
22965
22965
  }
22966
- function exprFill(text) {
22967
- const js = JSON.stringify(text);
22968
- return `(function(){${INPUT_PICKER_BODY}const inp=pickInput();if(!inp)return 'NO_INPUT';inp.focus();try{const s=getSelection();const r=document.createRange();r.selectNodeContents(inp);s.removeAllRanges();s.addRange(r);}catch(e){}const ok=document.execCommand('insertText',false,${js});try{inp.dispatchEvent(new InputEvent('input',{bubbles:true,inputType:'insertText',data:${js}}));}catch(e){inp.dispatchEvent(new Event('input',{bubbles:true}));}return ok?(inp.innerText||'').slice(0,30):'EXEC_FAIL';})()`;
22969
- }
22966
+ var EXPR_PREPARE_INPUT = `(function(){${INPUT_PICKER_BODY}const inp=pickInput();if(!inp)return 'NO_INPUT';inp.focus();try{const s=getSelection();const r=document.createRange();r.selectNodeContents(inp);s.removeAllRanges();s.addRange(r);}catch(e){}return 'READY';})()`;
22970
22967
  var EXPR_SNAP = `(function(){
22971
22968
  const md=[...document.querySelectorAll('.markdown-root,.aichat-container [class*=markdown]')]
22972
22969
  .filter(e=>e.offsetParent!==null&&!e.closest('.ui-model-picker__trigger,[class*=model-picker]'));
@@ -22977,14 +22974,17 @@ var EXPR_SNAP = `(function(){
22977
22974
  ${INPUT_PICKER_BODY}
22978
22975
  const input=pickInput();
22979
22976
  const inputText=String(input&&(input.innerText||input.textContent)||'').trim();
22980
- return JSON.stringify({messageCount:texts.length,replyLength:last.length,replyHash:hash,stop,inputTextLength:inputText.length});
22977
+ const composer=input&&(input.closest('.composer-bar,.ui-prompt-input,.agent-prompt-input-root')||input.parentElement);
22978
+ const sendButtons=composer?[...composer.querySelectorAll('button.ui-prompt-input-submit-button[data-state="send"],button.ui-prompt-input-submit-button[aria-label="Send message"],button[aria-label="Send"]')]
22979
+ .filter(button=>button.offsetParent!==null&&!button.disabled&&button.closest('.composer-bar,.ui-prompt-input,.agent-prompt-input-root')===composer):[];
22980
+ return JSON.stringify({messageCount:texts.length,replyLength:last.length,replyHash:hash,stop,inputTextLength:inputText.length,sendReady:sendButtons.length===1});
22981
22981
  })()`;
22982
22982
  var EXPR_CLICK_SEND = `(function(){${INPUT_PICKER_BODY}
22983
22983
  const input=pickInput();if(!input)return 'NO_INPUT';
22984
- const composer=input.closest('.composer-bar')||input.parentElement;
22984
+ const composer=input.closest('.composer-bar,.ui-prompt-input,.agent-prompt-input-root')||input.parentElement;
22985
22985
  if(!composer)return 'NO_COMPOSER';
22986
- const buttons=[...composer.querySelectorAll('button.ui-prompt-input-submit-button[data-state="send"],button[aria-label="Send"]')]
22987
- .filter(button=>button.offsetParent!==null&&!button.disabled&&button.closest('.composer-bar')===input.closest('.composer-bar'));
22986
+ const buttons=[...composer.querySelectorAll('button.ui-prompt-input-submit-button[data-state="send"],button.ui-prompt-input-submit-button[aria-label="Send message"],button[aria-label="Send"]')]
22987
+ .filter(button=>button.offsetParent!==null&&!button.disabled&&button.closest('.composer-bar,.ui-prompt-input,.agent-prompt-input-root')===composer);
22988
22988
  if(buttons.length!==1)return buttons.length?'AMBIGUOUS_SEND':'NO_SEND';
22989
22989
  buttons[0].click();return 'CLICKED';
22990
22990
  })()`;
@@ -23102,7 +23102,7 @@ function exprClickBoundComposerStop(agentId) {
23102
23102
  var EXPR_FIND_NEWAGENT = `(function(){const b=[...document.querySelectorAll('button,[role=button],a.action-label,.codicon')].find(e=>{if(e.offsetParent===null||e.closest('.glass-sidebar-agent-menu-btn'))return false;const s=(e.getAttribute('aria-label')||'')+' '+(e.getAttribute('title')||'')+' '+(e.innerText||'');return /(?:^|\\s)New (?:Agent|Chat)(?:\\s|$)/i.test(s);});if(!b)return '';const r=b.getBoundingClientRect();return JSON.stringify({x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)});})()`;
23103
23103
  var MODEL_PICKER_VISIBLE_BODY = `
23104
23104
  const visible=(node)=>!!(node&&(node.offsetParent!==null||(node.getClientRects&&node.getClientRects().length>0)));
23105
- const composers=[...document.querySelectorAll('.composer-bar[data-composer-id],.composer-bar,.ui-prompt-input-root')].filter(visible);
23105
+ const composers=[...document.querySelectorAll('.composer-bar[data-composer-id],.composer-bar,.ui-prompt-input-root,.agent-prompt-input-root,.ui-prompt-input')].filter(visible);
23106
23106
  const composer=composers[composers.length-1]||document;
23107
23107
  `;
23108
23108
  var EXPR_MODEL_PICKER_TRIGGER = `(function(){
@@ -23158,15 +23158,23 @@ function selectModelPickerRow(rows, requested, kind = "model") {
23158
23158
  const candidates = (Array.isArray(rows) ? rows : []).filter((row) => row && row.disabled !== true && (kind === "any" || row.kind === kind)).map((row) => {
23159
23159
  const normalized = normalizeModelPickerText(row.text);
23160
23160
  let score = normalized === wanted ? 1e3 : 0;
23161
- if (!score && normalized.startsWith(wanted)) score = 800;
23162
- if (!score && wanted.startsWith(normalized)) score = 700;
23163
- if (!score && normalized.includes(wanted)) score = 600;
23161
+ if (kind !== "parameter") {
23162
+ if (!score && normalized.startsWith(wanted)) score = 800;
23163
+ if (!score && wanted.startsWith(normalized)) score = 700;
23164
+ if (!score && normalized.includes(wanted)) score = 600;
23165
+ }
23164
23166
  return { row, score, distance: Math.abs(normalized.length - wanted.length) };
23165
23167
  }).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.distance - b.distance);
23166
23168
  if (candidates.length === 0) return null;
23167
23169
  if (candidates.length > 1 && candidates[0].score === candidates[1].score && candidates[0].distance === candidates[1].distance) return null;
23168
23170
  return candidates[0].row;
23169
23171
  }
23172
+ function createModelSelectionError(message, failureClass, retryable, diagnostic = {}) {
23173
+ const error2 = new Error(message);
23174
+ error2.code = `CURSOR_MODEL_${String(failureClass || "PROBE_ERROR").toUpperCase()}`;
23175
+ error2.modelSelectionFailure = { failureClass, retryable, ...diagnostic };
23176
+ return error2;
23177
+ }
23170
23178
  var WORKSPACE_SECTION_BODY = `
23171
23179
  const headText=(el)=>String(el&&(el.innerText||el.textContent)||'').trim();
23172
23180
  const isNewAgentButton=(node)=>{
@@ -23682,6 +23690,7 @@ var CursorBridge = class {
23682
23690
  this.projectPath = options.projectPath !== void 0 ? resolve7(String(options.projectPath)) : persistedWorkspace && persistedWorkspace.projectPath || null;
23683
23691
  this.workspaceSource = options.projectPath !== void 0 ? "constructor" : persistedWorkspace ? "persistent_init" : "auto_detect";
23684
23692
  this.workspaceUpdatedAt = persistedWorkspace && persistedWorkspace.updatedAt || null;
23693
+ this.workspaceConfirmationRequired = this.workspaceKey === "default" && !!persistedWorkspace;
23685
23694
  this.modelPreferencesFile = options.modelPreferencesFile === null ? null : resolve7(options.modelPreferencesFile || resolveCursorModelPreferencesFile());
23686
23695
  this.modelPreferences = readCursorModelPreferences(this.modelPreferencesFile);
23687
23696
  this.sessionFile = options.sessionFile === null ? null : resolve7(options.sessionFile || resolveCursorSessionRegistryFile());
@@ -23704,6 +23713,8 @@ var CursorBridge = class {
23704
23713
  const resolvedProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
23705
23714
  return {
23706
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,
23707
23718
  projectPath: resolvedProjectPath,
23708
23719
  workspaceSource: this.projectPath ? this.workspaceSource : resolvedProjectPath ? "host_auto_detect" : this.workspaceSource,
23709
23720
  workspaceUpdatedAt: this.workspaceUpdatedAt,
@@ -23738,7 +23749,10 @@ var CursorBridge = class {
23738
23749
  agentLabel: session.agentLabel || null,
23739
23750
  turnIndex: Number(session.turnIndex || 0),
23740
23751
  activeTaskId: session.activeTaskId || null,
23741
- lastTask: session.lastTask || null,
23752
+ lastTask: session.lastTask ? {
23753
+ ...session.lastTask,
23754
+ resultUnavailable: this.tasks.get(session.lastTask.taskId)?.result == null
23755
+ } : null,
23742
23756
  modelPreference: session.modelPreference || null,
23743
23757
  readOnly: session.scopeEnvelope && session.scopeEnvelope.readOnly === true,
23744
23758
  allowedPaths: session.scopeEnvelope && Array.isArray(session.scopeEnvelope.allowedPaths) ? session.scopeEnvelope.allowedPaths : [],
@@ -23750,6 +23764,7 @@ var CursorBridge = class {
23750
23764
  };
23751
23765
  }
23752
23766
  _claimNewSession({ taskId, projectPath, readOnly, allowedPaths, modelPreference, requestId, timeoutMs }) {
23767
+ this._ensureTaskCapacity();
23753
23768
  if (!this.sessionFile) throw cursorSessionError("SESSION_STORAGE_DISABLED", "persistent session storage is disabled");
23754
23769
  const sessionId = createCursorSessionId();
23755
23770
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -23795,6 +23810,7 @@ var CursorBridge = class {
23795
23810
  if (requestId && session.lastRequestId === requestId) {
23796
23811
  return { duplicate: true, session: { ...session } };
23797
23812
  }
23813
+ this._ensureTaskCapacity();
23798
23814
  if (!sameSessionProject(session.projectPath, projectPath)) {
23799
23815
  throw cursorSessionError("SESSION_WORKSPACE_MISMATCH", `session is bound to ${session.projectPath}`);
23800
23816
  }
@@ -23814,6 +23830,15 @@ var CursorBridge = class {
23814
23830
  if (session.state !== "ready") {
23815
23831
  throw cursorSessionError("SESSION_NOT_READY", `state=${session.state}; recovery=${session.recoveryState || "none"}`);
23816
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
+ }
23817
23842
  if (!session.agentId) throw cursorSessionError("SESSION_AGENT_NOT_BOUND", sessionId);
23818
23843
  const envelope = session.scopeEnvelope || { readOnly: false, allowedPaths: [] };
23819
23844
  if (envelope.readOnly === true) {
@@ -23833,6 +23858,7 @@ var CursorBridge = class {
23833
23858
  session.epoch = epoch;
23834
23859
  session.activeTaskId = taskId;
23835
23860
  session.lastRequestId = requestId || null;
23861
+ delete session.responseBaseline;
23836
23862
  session.lease = {
23837
23863
  taskId,
23838
23864
  instanceId: this.sessionInstanceId,
@@ -23861,6 +23887,20 @@ var CursorBridge = class {
23861
23887
  });
23862
23888
  job.sessionState = "busy";
23863
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
+ }
23864
23904
  _settleSessionJob(job, outcome, options = {}) {
23865
23905
  if (!job || !job.sessionId || !this.sessionFile) return;
23866
23906
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -23888,6 +23928,7 @@ var CursorBridge = class {
23888
23928
  }
23889
23929
  session.activeTaskId = null;
23890
23930
  session.lease = null;
23931
+ if (outcome !== "completed") delete session.responseBaseline;
23891
23932
  session.recoveryState = null;
23892
23933
  session.attention = null;
23893
23934
  session.state = session.agentId ? "ready" : "failed";
@@ -23902,6 +23943,19 @@ var CursorBridge = class {
23902
23943
  job.sessionError = error2 instanceof Error ? error2.message : String(error2);
23903
23944
  }
23904
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
+ }
23905
23959
  sessionStatus(sessionId) {
23906
23960
  const session = this._readSession(sessionId);
23907
23961
  return {
@@ -23910,17 +23964,28 @@ var CursorBridge = class {
23910
23964
  ...session ? this._sessionView(session) : { sessionId: String(sessionId || "") }
23911
23965
  };
23912
23966
  }
23913
- async _reconcileSession(sessionId) {
23967
+ async _reconcileSession(sessionId, { collectResult = false } = {}) {
23968
+ const action = collectResult ? "collect_result" : "reconcile";
23914
23969
  const initial = this._readSession(sessionId);
23915
- if (!initial) return { found: false, action: "reconcile", sessionId };
23916
- if (initial.state === "ready" || initial.state === "closed" || initial.state === "failed") {
23917
- 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");
23918
23983
  }
23919
23984
  if (!initial.agentId) {
23920
23985
  return {
23921
23986
  found: true,
23922
23987
  changed: false,
23923
- action: "reconcile",
23988
+ action,
23924
23989
  state: "agent_missing",
23925
23990
  attention: "No exact agentId is available. Confirm the Cursor task state manually before explicit abandon.",
23926
23991
  ...this._sessionView(initial)
@@ -23938,21 +24003,44 @@ var CursorBridge = class {
23938
24003
  effectiveExecution: "parallel_agent",
23939
24004
  lastRecoveryAt: null,
23940
24005
  recoveryState: null,
23941
- error: null
24006
+ error: null,
24007
+ responseBaseline: initial.responseBaseline || null
23942
24008
  };
23943
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
+ }
23944
24023
  let result;
23945
24024
  updateCursorSessionRegistry(this.sessionFile, (registry2) => {
23946
24025
  const session = registry2.sessions[sessionId];
23947
24026
  if (!session) {
23948
- result = { found: false, action: "reconcile", sessionId };
24027
+ result = { found: false, action, sessionId };
23949
24028
  return;
23950
24029
  }
23951
- if (Number(session.epoch || 0) !== Number(initial.epoch || 0) || session.activeTaskId !== initial.activeTaskId) {
23952
- 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) };
23953
24032
  return;
23954
24033
  }
23955
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
+ }
23956
24044
  if (!observed.stable || !observed.entry) {
23957
24045
  session.state = "needs_attention";
23958
24046
  session.recoveryState = probe.recoveryState || "history_unavailable";
@@ -23992,7 +24080,8 @@ var CursorBridge = class {
23992
24080
  session.lease = null;
23993
24081
  session.state = "ready";
23994
24082
  session.recoveryState = terminalClass === "completed" ? "reconciled_result_uncollected" : null;
23995
- 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;
23996
24085
  session.updatedAt = now;
23997
24086
  result = { found: true, changed: true, action: "reconcile", state: terminalClass, ...this._sessionView(session) };
23998
24087
  });
@@ -24002,10 +24091,11 @@ var CursorBridge = class {
24002
24091
  const id = String(sessionId || "").trim();
24003
24092
  if (!id) throw cursorSessionError("SESSION_REQUIRED", "session_id must not be empty");
24004
24093
  const action = String(options.action || "").trim().toLowerCase();
24005
- if (!["reconcile", "close", "forget", "abandon"].includes(action)) {
24006
- 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");
24007
24096
  }
24008
24097
  if (action === "reconcile") return this._reconcileSession(id);
24098
+ if (action === "collect_result") return this._reconcileSession(id, { collectResult: true });
24009
24099
  let result;
24010
24100
  updateCursorSessionRegistry(this.sessionFile, (registry2) => {
24011
24101
  const session = registry2.sessions[id];
@@ -24063,6 +24153,7 @@ var CursorBridge = class {
24063
24153
  }
24064
24154
  const previousProjectPath = this.projectPath || this._lastLifecycle && this._lastLifecycle.projectPath || null;
24065
24155
  const saved = writeWorkspaceBinding(this.workspaceFile, this.workspaceKey, projectPath);
24156
+ this.workspaceConfirmationRequired = false;
24066
24157
  this.projectPath = saved.projectPath;
24067
24158
  this.workspaceSource = "persistent_init";
24068
24159
  this.workspaceUpdatedAt = saved.updatedAt;
@@ -24257,6 +24348,7 @@ var CursorBridge = class {
24257
24348
  const text = String(query || "").trim();
24258
24349
  if (!text) throw new Error("query must not be empty");
24259
24350
  if (text.length > 2e4) throw new Error("query exceeds the 20,000-character limit");
24351
+ this._assertWorkspaceConfirmed();
24260
24352
  if (this._hasGlobalReservation()) {
24261
24353
  throw new Error("A global Cursor reservation has an unconfirmed Stop state; resolve blockingTaskIds from cursor_status first");
24262
24354
  }
@@ -24269,7 +24361,9 @@ var CursorBridge = class {
24269
24361
  allowedPaths: [],
24270
24362
  modelPreference: this._modelPreferenceFor("cce")
24271
24363
  });
24272
- return normalizeCceSearchResult(await job.promise);
24364
+ const result = normalizeCceSearchResult(await job.promise);
24365
+ this._markTaskResultCollected(job);
24366
+ return result;
24273
24367
  }
24274
24368
  // Unlisted compatibility aliases for clients that cached the pre-3.0 tool surface.
24275
24369
  async search(query) {
@@ -24285,6 +24379,7 @@ var CursorBridge = class {
24285
24379
  const text = String(prompt || "").trim();
24286
24380
  if (!text) throw new Error("prompt must not be empty");
24287
24381
  if (text.length > 1e5) throw new Error("prompt exceeds the 100,000-character limit");
24382
+ this._assertWorkspaceConfirmed();
24288
24383
  if (this._hasGlobalReservation()) {
24289
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");
24290
24385
  }
@@ -24420,6 +24515,7 @@ var CursorBridge = class {
24420
24515
  return `cursor-${Date.now().toString(36)}-${this.nextTaskId++}`;
24421
24516
  }
24422
24517
  _enqueue(kind, prompt, options) {
24518
+ this._ensureTaskCapacity();
24423
24519
  const id = options.taskId || this._nextTaskId();
24424
24520
  let resolvePromise;
24425
24521
  let rejectPromise;
@@ -24463,6 +24559,8 @@ var CursorBridge = class {
24463
24559
  targetUiFlavor: null,
24464
24560
  fallbackReason: null,
24465
24561
  result: null,
24562
+ resultCollectedAt: null,
24563
+ waitDeadlineAt: null,
24466
24564
  error: null,
24467
24565
  cancelRequested: false,
24468
24566
  cancelReason: null,
@@ -24821,7 +24919,7 @@ var CursorBridge = class {
24821
24919
  this._throwIfCancelledBeforeSend(options);
24822
24920
  await this._applyModelPreference(c, options.modelPreference, options);
24823
24921
  this._throwIfCancelledBeforeSend(options);
24824
- const filled = await evalJS(c, exprFill(prompt));
24922
+ const filled = await this._fillPrompt(c, prompt, options);
24825
24923
  if (filled === "NO_INPUT") await this._throwChatPanelUnavailableAfterNoInput(c);
24826
24924
  if (filled === "NO_INPUT" || filled === "EXEC_FAIL") throw new Error("Failed to enter the query because the input state was invalid");
24827
24925
  await sleep2(450);
@@ -24941,25 +25039,83 @@ var CursorBridge = class {
24941
25039
  modelRow = selectModelPickerRow(expanded && expanded.rows, requestedModel, "model");
24942
25040
  return { snapshot: expanded, modelRow };
24943
25041
  }
24944
- async _selectedEffortRow(c, modelRow, effort) {
24945
- let snapshot = await this._readModelPickerRows(c);
24946
- let effortRow = selectModelPickerRow(snapshot.rows, cursorEffortUiValue(effort), "parameter");
24947
- if (!effortRow) {
24948
- snapshot = await this._openModelPickerControl(c, snapshot, "effort_control");
24949
- effortRow = selectModelPickerRow(snapshot.rows, cursorEffortUiValue(effort), "parameter");
25042
+ _inspectModelPickerRows(snapshot, requested, kind) {
25043
+ const rows = (snapshot && Array.isArray(snapshot.rows) ? snapshot.rows : []).filter((row) => row && row.disabled !== true && row.kind === kind);
25044
+ const wanted = normalizeModelPickerText(requested);
25045
+ const exact = rows.filter((row) => normalizeModelPickerText(row.text) === wanted);
25046
+ return {
25047
+ snapshot,
25048
+ row: exact.length === 1 ? exact[0] : null,
25049
+ ambiguous: exact.length > 1,
25050
+ available: rows.map((row) => row.text)
25051
+ };
25052
+ }
25053
+ async _waitForModelPickerMatch(c, requested, kind, job, options = {}) {
25054
+ const timeoutMs = Math.max(0, Number(options.timeoutMs ?? 700));
25055
+ const pollMs = Math.max(10, Number(options.pollMs ?? 100));
25056
+ const stableMs = Math.max(0, Number(options.stableMs ?? 250));
25057
+ const deadline = Date.now() + timeoutMs;
25058
+ let stableSignature = null;
25059
+ let stableSince = 0;
25060
+ let last = this._inspectModelPickerRows({ open: false, rows: [] }, requested, kind);
25061
+ do {
25062
+ this._throwIfCancelledBeforeSend(job);
25063
+ last = this._inspectModelPickerRows(await this._readModelPickerRows(c), requested, kind);
25064
+ if (last.row) return { ...last, state: "matched" };
25065
+ const signature = JSON.stringify(last.available);
25066
+ if (last.available.length > 0) {
25067
+ if (signature !== stableSignature) {
25068
+ stableSignature = signature;
25069
+ stableSince = Date.now();
25070
+ } else if (Date.now() - stableSince >= stableMs) {
25071
+ return { ...last, state: last.ambiguous ? "ambiguous" : "unsupported" };
25072
+ }
25073
+ } else {
25074
+ stableSignature = null;
25075
+ stableSince = 0;
25076
+ }
25077
+ if (Date.now() >= deadline) break;
25078
+ await sleep2(Math.min(pollMs, Math.max(0, deadline - Date.now())));
25079
+ } while (Date.now() <= deadline);
25080
+ return { ...last, state: last.ambiguous ? "ambiguous" : "not_rendered" };
25081
+ }
25082
+ async _selectedEffortRow(c, modelRow, effort, job) {
25083
+ const requested = cursorEffortUiValue(effort);
25084
+ const snapshot = await this._readModelPickerRows(c);
25085
+ const immediate = this._inspectModelPickerRows(snapshot, requested, "parameter");
25086
+ if (immediate.row) return { ...immediate, state: "matched", attempts: ["visible"] };
25087
+ const attempts = [];
25088
+ const effortControl = (snapshot.rows || []).find((row) => row.kind === "effort_control" && row.disabled !== true);
25089
+ if (effortControl) {
25090
+ this._throwIfCancelledBeforeSend(job);
25091
+ attempts.push("effort_control");
25092
+ await this._clickModelPickerPoint(c, effortControl);
25093
+ const result2 = await this._waitForModelPickerMatch(c, requested, "parameter", job, { timeoutMs: 700 });
25094
+ if (result2.state !== "not_rendered") return { ...result2, attempts };
24950
25095
  }
24951
- if (effortRow) return effortRow;
25096
+ this._throwIfCancelledBeforeSend(job);
25097
+ attempts.push("model_hover");
24952
25098
  await this._hoverModelPickerPoint(c, modelRow);
24953
- await sleep2(400);
24954
- snapshot = await this._readModelPickerRows(c);
24955
- effortRow = selectModelPickerRow(snapshot.rows, cursorEffortUiValue(effort), "parameter");
24956
- if (!effortRow && modelRow && modelRow.hasSubmenu) {
25099
+ let result = await this._waitForModelPickerMatch(c, requested, "parameter", job, { timeoutMs: 700 });
25100
+ if (result.state !== "not_rendered") return { ...result, attempts };
25101
+ if (modelRow && modelRow.hasSubmenu) {
25102
+ this._throwIfCancelledBeforeSend(job);
25103
+ attempts.push("model_click");
24957
25104
  await this._clickModelPickerPoint(c, modelRow);
24958
- await sleep2(350);
24959
- snapshot = await this._readModelPickerRows(c);
24960
- effortRow = selectModelPickerRow(snapshot.rows, cursorEffortUiValue(effort), "parameter");
25105
+ result = await this._waitForModelPickerMatch(c, requested, "parameter", job, { timeoutMs: 1100 });
24961
25106
  }
24962
- return effortRow;
25107
+ return { ...result, attempts };
25108
+ }
25109
+ async _waitForSelectedModelPickerRow(c, requested, kind, job, timeoutMs = 1500) {
25110
+ const deadline = Date.now() + timeoutMs;
25111
+ do {
25112
+ this._throwIfCancelledBeforeSend(job);
25113
+ const inspected = this._inspectModelPickerRows(await this._readModelPickerRows(c), requested, kind);
25114
+ if (inspected.row && inspected.row.selected) return inspected.row;
25115
+ if (Date.now() >= deadline) break;
25116
+ await sleep2(Math.min(100, Math.max(0, deadline - Date.now())));
25117
+ } while (Date.now() <= deadline);
25118
+ return null;
24963
25119
  }
24964
25120
  async _applyModelPreference(c, preference, job) {
24965
25121
  if (!preference) {
@@ -24968,63 +25124,167 @@ var CursorBridge = class {
24968
25124
  }
24969
25125
  const requestedModel = String(preference.model || "").trim();
24970
25126
  const requestedEffort = preference.effort ? normalizeCursorModelEffort(preference.effort, "") : null;
24971
- const opened = await this._openModelPicker(c);
24972
- let located = await this._findModelPickerModel(c, opened, requestedModel);
24973
- let modelRow = located.modelRow;
24974
- if (!modelRow) {
24975
- throw new Error(`Configured Cursor model is unavailable or ambiguous: ${requestedModel}`);
24976
- }
24977
- let selectedEffortRow = null;
24978
- if (requestedEffort && modelRow.hasSubmenu) {
24979
- selectedEffortRow = await this._selectedEffortRow(c, modelRow, requestedEffort);
24980
- if (!selectedEffortRow) {
24981
- throw new Error(`Cursor model ${requestedModel} does not expose effort ${requestedEffort}`);
24982
- }
24983
- if (!selectedEffortRow.selected || !modelRow.selected) {
24984
- await this._clickModelPickerPoint(c, selectedEffortRow);
24985
- await sleep2(550);
24986
- }
24987
- } else if (!modelRow.selected) {
24988
- await this._clickModelPickerPoint(c, modelRow);
24989
- await sleep2(550);
24990
- }
24991
- let trigger = await this._readModelPickerTrigger(c);
24992
- if (!trigger.found || !normalizeModelPickerText(trigger.text).includes(normalizeModelPickerText(requestedModel))) {
24993
- const reopened = await this._openModelPicker(c);
24994
- located = await this._findModelPickerModel(c, reopened, requestedModel);
24995
- const selected = selectModelPickerRow(located.snapshot.rows.filter((row) => row.selected), requestedModel, "model");
24996
- if (!selected) throw new Error(`Cursor did not confirm configured model ${requestedModel}`);
24997
- modelRow = selected;
24998
- }
25127
+ let modelRow = null;
24999
25128
  let effectiveEffort = null;
25000
- if (requestedEffort) {
25001
- const reopened = await this._openModelPicker(c);
25002
- located = await this._findModelPickerModel(c, reopened, requestedModel);
25129
+ let primaryError = null;
25130
+ try {
25131
+ const opened = await this._openModelPicker(c);
25132
+ let located = await this._findModelPickerModel(c, opened, requestedModel);
25003
25133
  modelRow = located.modelRow;
25004
- if (!modelRow) throw new Error(`Cursor model row disappeared while applying effort: ${requestedModel}`);
25005
- let effortRow = await this._selectedEffortRow(c, modelRow, requestedEffort);
25006
- if (!effortRow) {
25007
- throw new Error(`Cursor model ${requestedModel} does not expose effort ${requestedEffort}`);
25134
+ if (!modelRow) {
25135
+ throw createModelSelectionError(
25136
+ `Configured Cursor model is unavailable or ambiguous: ${requestedModel}`,
25137
+ "model_unavailable",
25138
+ false,
25139
+ { available: (located.snapshot.rows || []).filter((row) => row.kind === "model").map((row) => row.text) }
25140
+ );
25008
25141
  }
25009
- if (!effortRow.selected) {
25010
- await this._clickModelPickerPoint(c, effortRow);
25011
- await sleep2(450);
25142
+ let effortPickerReopened = false;
25143
+ const resolveEffort = async () => {
25144
+ let outcome = await this._selectedEffortRow(c, modelRow, requestedEffort, job);
25145
+ if (!outcome.row && outcome.state === "not_rendered" && !effortPickerReopened) {
25146
+ effortPickerReopened = true;
25147
+ await this._closeModelPicker(c);
25148
+ const reopened = await this._openModelPicker(c);
25149
+ located = await this._findModelPickerModel(c, reopened, requestedModel);
25150
+ modelRow = located.modelRow;
25151
+ if (!modelRow) {
25152
+ throw createModelSelectionError(
25153
+ `Cursor model row disappeared while applying effort: ${requestedModel}`,
25154
+ "model_unavailable",
25155
+ true
25156
+ );
25157
+ }
25158
+ outcome = await this._selectedEffortRow(c, modelRow, requestedEffort, job);
25159
+ outcome.attempts = ["picker_reopen", ...outcome.attempts || []];
25160
+ }
25161
+ return outcome;
25162
+ };
25163
+ const throwEffortFailure = (outcome) => {
25164
+ const failureClass = outcome.state === "ambiguous" ? "effort_ambiguous" : outcome.state === "unsupported" ? "effort_unsupported" : "effort_menu_not_rendered";
25165
+ const message = failureClass === "effort_menu_not_rendered" ? `Cursor effort menu did not render for model ${requestedModel}` : failureClass === "effort_ambiguous" ? `Cursor model ${requestedModel} exposes ambiguous effort ${requestedEffort}` : `Cursor model ${requestedModel} does not expose effort ${requestedEffort}`;
25166
+ throw createModelSelectionError(
25167
+ message,
25168
+ failureClass,
25169
+ failureClass === "effort_menu_not_rendered",
25170
+ { available: outcome.available || [], attempts: outcome.attempts || [] }
25171
+ );
25172
+ };
25173
+ if (requestedEffort && modelRow.hasSubmenu) {
25174
+ const selectedEffort = await resolveEffort();
25175
+ if (!selectedEffort.row) throwEffortFailure(selectedEffort);
25176
+ if (!selectedEffort.row.selected || !modelRow.selected) {
25177
+ this._throwIfCancelledBeforeSend(job);
25178
+ await this._clickModelPickerPoint(c, selectedEffort.row);
25179
+ await sleep2(550);
25180
+ }
25181
+ } else if (!modelRow.selected) {
25182
+ this._throwIfCancelledBeforeSend(job);
25183
+ await this._clickModelPickerPoint(c, modelRow);
25184
+ await sleep2(550);
25012
25185
  }
25013
- trigger = await this._readModelPickerTrigger(c);
25014
- const detailMatches = normalizeModelPickerText(`${trigger.detail || ""} ${trigger.text || ""}`).includes(normalizeModelPickerText(requestedEffort));
25015
- if (!detailMatches) {
25016
- const verify = await this._openModelPicker(c);
25017
- const verifiedModel = await this._findModelPickerModel(c, verify, requestedModel);
25018
- const selectedModel = verifiedModel.modelRow;
25019
- effortRow = await this._selectedEffortRow(c, selectedModel, requestedEffort);
25186
+ let trigger2 = await this._readModelPickerTrigger(c);
25187
+ if (!trigger2.found || !normalizeModelPickerText(trigger2.text).includes(normalizeModelPickerText(requestedModel))) {
25188
+ const reopened = await this._openModelPicker(c);
25189
+ located = await this._findModelPickerModel(c, reopened, requestedModel);
25190
+ const selected = selectModelPickerRow(located.snapshot.rows.filter((row) => row.selected), requestedModel, "model");
25191
+ if (!selected) {
25192
+ throw createModelSelectionError(
25193
+ `Cursor did not confirm configured model ${requestedModel}`,
25194
+ "model_not_confirmed",
25195
+ true
25196
+ );
25197
+ }
25198
+ modelRow = selected;
25199
+ }
25200
+ if (requestedEffort) {
25201
+ const reopened = await this._openModelPicker(c);
25202
+ located = await this._findModelPickerModel(c, reopened, requestedModel);
25203
+ modelRow = located.modelRow;
25204
+ if (!modelRow) {
25205
+ throw createModelSelectionError(
25206
+ `Cursor model row disappeared while applying effort: ${requestedModel}`,
25207
+ "model_unavailable",
25208
+ true
25209
+ );
25210
+ }
25211
+ const effortOutcome = await resolveEffort();
25212
+ if (!effortOutcome.row) throwEffortFailure(effortOutcome);
25213
+ let effortRow = effortOutcome.row;
25214
+ if (!effortRow.selected) {
25215
+ effortRow = await this._waitForSelectedModelPickerRow(
25216
+ c,
25217
+ cursorEffortUiValue(requestedEffort),
25218
+ "parameter",
25219
+ job
25220
+ );
25221
+ }
25020
25222
  if (!effortRow || !effortRow.selected) {
25021
- throw new Error(`Cursor did not confirm effort ${requestedEffort} for model ${requestedModel}`);
25223
+ throw createModelSelectionError(
25224
+ `Cursor did not confirm effort ${requestedEffort} for model ${requestedModel}`,
25225
+ "effort_not_confirmed",
25226
+ true
25227
+ );
25022
25228
  }
25229
+ effectiveEffort = requestedEffort;
25230
+ }
25231
+ } catch (error2) {
25232
+ primaryError = error2 instanceof Error ? error2 : new Error(String(error2));
25233
+ let failure = primaryError.modelSelectionFailure;
25234
+ if (!failure) {
25235
+ const pickerUnavailable = /model picker (?:is unavailable|did not open)/i.test(primaryError.message);
25236
+ failure = {
25237
+ failureClass: pickerUnavailable ? "picker_did_not_open" : "probe_error",
25238
+ retryable: true
25239
+ };
25240
+ }
25241
+ const diagnostic = {
25242
+ configured: true,
25243
+ applied: false,
25244
+ requestedModel,
25245
+ requestedEffort,
25246
+ failureClass: failure.failureClass,
25247
+ retryable: failure.retryable === true,
25248
+ errorCode: primaryError.code || `CURSOR_MODEL_${String(failure.failureClass).toUpperCase()}`,
25249
+ available: failure.available || [],
25250
+ attempts: failure.attempts || [],
25251
+ runtimeMode: this.runtimeMode,
25252
+ lastError: primaryError.message,
25253
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
25254
+ };
25255
+ primaryError.modelSelection = diagnostic;
25256
+ if (job) job.modelSelection = diagnostic;
25257
+ throw primaryError;
25258
+ } finally {
25259
+ try {
25260
+ await this._closeModelPicker(c);
25261
+ } catch (cleanupError) {
25262
+ const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
25263
+ if (!primaryError) {
25264
+ const error2 = createModelSelectionError(message, "picker_cleanup_failed", true);
25265
+ const diagnostic = {
25266
+ configured: true,
25267
+ applied: false,
25268
+ requestedModel,
25269
+ requestedEffort,
25270
+ failureClass: "picker_cleanup_failed",
25271
+ retryable: true,
25272
+ errorCode: error2.code,
25273
+ available: [],
25274
+ attempts: [],
25275
+ runtimeMode: this.runtimeMode,
25276
+ lastError: message,
25277
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
25278
+ };
25279
+ error2.modelSelection = diagnostic;
25280
+ if (job) job.modelSelection = diagnostic;
25281
+ throw error2;
25282
+ }
25283
+ if (primaryError.modelSelection) primaryError.modelSelection.cleanupError = message;
25284
+ if (job && job.modelSelection) job.modelSelection.cleanupError = message;
25023
25285
  }
25024
- effectiveEffort = requestedEffort;
25025
25286
  }
25026
- await this._closeModelPicker(c);
25027
- trigger = await this._readModelPickerTrigger(c);
25287
+ const trigger = await this._readModelPickerTrigger(c);
25028
25288
  const result = {
25029
25289
  configured: true,
25030
25290
  applied: true,
@@ -25203,6 +25463,38 @@ var CursorBridge = class {
25203
25463
  }
25204
25464
  }
25205
25465
  }
25466
+ async _fillPrompt(c, text, job, timeoutMs = 2500) {
25467
+ const prepared = await evalJS(c, EXPR_PREPARE_INPUT);
25468
+ if (prepared !== "READY") return prepared;
25469
+ try {
25470
+ await c.send("Input.insertText", { text: String(text) });
25471
+ } catch {
25472
+ return "EXEC_FAIL";
25473
+ }
25474
+ const deadline = Date.now() + timeoutMs;
25475
+ let snapshot = {};
25476
+ do {
25477
+ this._throwIfCancelledBeforeSend(job);
25478
+ try {
25479
+ snapshot = JSON.parse(await evalJS(c, EXPR_SNAP));
25480
+ } catch {
25481
+ }
25482
+ if (Number(snapshot.inputTextLength || 0) > 0 && snapshot.sendReady === true) {
25483
+ return String(text).slice(0, 30);
25484
+ }
25485
+ if (Date.now() >= deadline) break;
25486
+ await sleep2(Math.min(100, Math.max(0, deadline - Date.now())));
25487
+ } while (Date.now() <= deadline);
25488
+ const error2 = new Error("Cursor composer did not become send-ready after trusted input");
25489
+ error2.code = "CURSOR_COMPOSER_NOT_SEND_READY";
25490
+ error2.confirmedNotSent = true;
25491
+ error2.composerDiagnostic = {
25492
+ inputTextLength: Number(snapshot.inputTextLength || 0),
25493
+ sendReady: snapshot.sendReady === true,
25494
+ runtimeMode: this.runtimeMode
25495
+ };
25496
+ throw error2;
25497
+ }
25206
25498
  async _confirmSubmission(c, baselineCount = 0, providerErrorBaseline = "") {
25207
25499
  const accepted = async () => {
25208
25500
  await this._throwIfNewProviderError(c, providerErrorBaseline);
@@ -25292,7 +25584,7 @@ var CursorBridge = class {
25292
25584
  this._throwIfCancelledBeforeSend(job);
25293
25585
  await this._applyModelPreference(c, job.modelPreference, job);
25294
25586
  this._throwIfCancelledBeforeSend(job);
25295
- const filled = await evalJS(c, exprFill(job.prompt));
25587
+ const filled = await this._fillPrompt(c, job.prompt, job);
25296
25588
  if (filled === "NO_INPUT") await this._throwChatPanelUnavailableAfterNoInput(c);
25297
25589
  if (filled === "NO_INPUT" || filled === "EXEC_FAIL") throw new Error("Failed to enter the parallel_agent task");
25298
25590
  await sleep2(350);
@@ -25411,8 +25703,9 @@ var CursorBridge = class {
25411
25703
  await this._applyModelPreference(c, job.modelPreference, job);
25412
25704
  this._throwIfCancelledBeforeSend(job);
25413
25705
  job.responseBaseline = await this._captureSessionResponseBaseline(c);
25706
+ this._persistSessionResponseBaseline(job);
25414
25707
  const providerErrorBaseline = providerErrorSignature(await this._readProviderError(c));
25415
- const filled = await evalJS(c, exprFill(job.prompt));
25708
+ const filled = await this._fillPrompt(c, job.prompt, job);
25416
25709
  if (filled === "NO_INPUT") await this._throwChatPanelUnavailableAfterNoInput(c);
25417
25710
  if (filled === "NO_INPUT" || filled === "EXEC_FAIL") {
25418
25711
  throw cursorSessionError("SESSION_INPUT_FAILED", "failed to enter the continued task");
@@ -25485,8 +25778,16 @@ var CursorBridge = class {
25485
25778
  job.monitorPromise = monitorPromise;
25486
25779
  return true;
25487
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
+ }
25488
25789
  async _monitorParallelAgent(job, generation) {
25489
- const started = Date.now();
25790
+ const deadline = this._taskWaitDeadline(job);
25490
25791
  let sawGenerating = false;
25491
25792
  let completedStable = 0;
25492
25793
  let missingPolls = 0;
@@ -25495,7 +25796,7 @@ var CursorBridge = class {
25495
25796
  let lastTerminalErrorSignature = "";
25496
25797
  let collectionAttempts = 0;
25497
25798
  let lastCollectionError = "";
25498
- while (Date.now() - started < job.timeoutMs) {
25799
+ while (Date.now() < deadline) {
25499
25800
  if (!this._monitorOwns(job, generation)) return;
25500
25801
  await sleep2(1400);
25501
25802
  if (!this._monitorOwns(job, generation)) return;
@@ -25613,51 +25914,63 @@ var CursorBridge = class {
25613
25914
  }
25614
25915
  throw new Error(`After opening ${agentId}, it could not be confirmed as the selected Agent`);
25615
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
+ }
25616
25934
  async _collectParallelAgent(job) {
25617
25935
  const page = await findPage({ targetId: job.targetId, purpose: "parallel_agent" });
25618
25936
  const c = makeClient(page.webSocketDebuggerUrl);
25619
25937
  await c.ready;
25620
- let previousSelectedId = null;
25621
25938
  try {
25622
- const entries = await this._readAgentEntries(c, true);
25623
- previousSelectedId = (entries.find((e) => e.isSelected) || {}).id || null;
25624
- const opened = await evalJS(c, exprOpenAgent(job.agentId));
25625
- if (opened !== "OPENED") throw new Error(`Unable to open ${job.agentId}: ${opened}`);
25626
- await this._closeHistory(c);
25627
- await this._waitForSelectedAgent(c, job.agentId);
25628
- let answer = "";
25629
- let lastKey = "";
25630
- let stable = 0;
25631
- for (let i = 0; i < 80; i++) {
25632
- const snap = await this._readResponseSnapshot(c);
25633
- const candidate = String(await evalJS(c, EXPR_EXTRACT) || "").trim();
25634
- const hasNewTurn = isSessionTurnReplyReady(job.responseBaseline, snap);
25635
- if (candidate && hasNewTurn && Number(snap.stop || 0) === 0) {
25636
- const key = `${snap.replyLength}:${snap.replyHash}`;
25637
- if (key === lastKey) stable++;
25638
- else {
25639
- lastKey = key;
25640
- stable = 0;
25641
- }
25642
- if (stable >= 1) {
25643
- answer = candidate;
25644
- 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
+ }
25645
25962
  }
25963
+ await sleep2(300);
25646
25964
  }
25647
- await sleep2(300);
25648
- }
25649
- if (!answer) throw new Error(`${job.agentId} was opened, but no final assistant reply was found`);
25650
- if (previousSelectedId && previousSelectedId !== job.agentId) {
25651
- if (await this._ensureHistoryOpen(c)) {
25652
- await evalJS(c, exprOpenAgent(previousSelectedId));
25653
- await this._closeHistory(c);
25654
- await this._waitForSelectedAgent(c, previousSelectedId);
25655
- }
25656
- }
25657
- return answer;
25965
+ if (!answer) throw new Error(`${job.agentId} was opened, but no final assistant reply was found`);
25966
+ return answer;
25967
+ });
25658
25968
  } finally {
25659
- await this._closeHistory(c);
25660
- c.close();
25969
+ try {
25970
+ await this._closeHistory(c);
25971
+ } finally {
25972
+ c.close();
25973
+ }
25661
25974
  }
25662
25975
  }
25663
25976
  _reapWithoutResult(job, error2, evidence) {
@@ -25672,7 +25985,6 @@ var CursorBridge = class {
25672
25985
  job.terminalEvidence = evidence || "stable_completed_history_icon";
25673
25986
  job.recoveryState = "terminal_result_uncollected";
25674
25987
  job.reservationScope = uncertainSubmissionReservationScope(job, error2);
25675
- job.recoveryState = error2.recoveryState || "monitoring_uncertain_submission";
25676
25988
  this._safeSettleSessionJob(job, "terminal_result_uncollected", { needsAttention: true });
25677
25989
  }
25678
25990
  _abandonJob(job, reason) {
@@ -25749,6 +26061,7 @@ var CursorBridge = class {
25749
26061
  job.phase = "running";
25750
26062
  job.error = null;
25751
26063
  job.recoveryState = "monitoring";
26064
+ if (options.reattach !== false && !job.cancelRequested) job.waitDeadlineAt = Date.now() + job.timeoutMs;
25752
26065
  const attached = options.reattach !== false && !job.cancelRequested && this._startParallelMonitor(job);
25753
26066
  return { changed: attached, state: "running", monitorReattached: attached, task: this._taskView(job, true) };
25754
26067
  }
@@ -26147,12 +26460,12 @@ var CursorBridge = class {
26147
26460
  }).catch((e) => console.error("\u26A0\uFE0F Failed to restore the original Cursor Agent: " + e.message));
26148
26461
  }
26149
26462
  async _waitComplete(c, timeoutMs = QUERY_TIMEOUT, baselineCount = 0, job = null, providerErrorBaseline = "") {
26150
- const start = Date.now();
26463
+ const deadline = this._taskWaitDeadline(job, timeoutMs);
26151
26464
  const INTERVAL = 1e3;
26152
26465
  let sawStop = false;
26153
26466
  let lastReplyKey = "", stableReply = 0;
26154
26467
  await sleep2(1200);
26155
- while (Date.now() - start < timeoutMs) {
26468
+ while (Date.now() < deadline) {
26156
26469
  await this._throwIfNewProviderError(c, providerErrorBaseline);
26157
26470
  if (job && job.cancelRequested) {
26158
26471
  if (job.agentId) {
@@ -26217,6 +26530,7 @@ var CursorBridge = class {
26217
26530
  throw new Error(`Cursor task timed out (${timeoutMs}ms) before generation was confirmed stopped with a complete assistant reply${taskHint}`);
26218
26531
  }
26219
26532
  _taskView(job, includeResult = false) {
26533
+ if (includeResult) this._markTaskResultCollected(job);
26220
26534
  const view = {
26221
26535
  taskId: job.id,
26222
26536
  kind: job.kind,
@@ -26255,6 +26569,8 @@ var CursorBridge = class {
26255
26569
  monitorAttached: job.monitorAttached,
26256
26570
  monitorGeneration: job.monitorGeneration,
26257
26571
  resultUnavailable: job.resultUnavailable,
26572
+ resultCollectedAt: job.resultCollectedAt || null,
26573
+ waitDeadlineAt: Number.isFinite(job.waitDeadlineAt) ? new Date(job.waitDeadlineAt).toISOString() : null,
26258
26574
  terminalEvidence: job.terminalEvidence,
26259
26575
  providerError: job.providerError,
26260
26576
  uiDiagnostic: job.uiDiagnostic,
@@ -26264,7 +26580,7 @@ var CursorBridge = class {
26264
26580
  blocksFifo: this.activeParallel.has(job.id) && !isTerminalTask(job),
26265
26581
  blocksAll: this.activeParallel.has(job.id) && !isTerminalTask(job) && job.reservationScope === "global"
26266
26582
  };
26267
- if (includeResult || isTerminalTask(job)) view.result = job.result;
26583
+ if (includeResult) view.result = job.result;
26268
26584
  if (job.phase === "orphaned") {
26269
26585
  if (job.recoveryState === "terminal_result_uncollected") {
26270
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.";
@@ -26274,11 +26590,22 @@ var CursorBridge = class {
26274
26590
  }
26275
26591
  return view;
26276
26592
  }
26277
- _trimTasks() {
26278
- 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;
26279
26606
  for (const [id, job] of this.tasks) {
26280
- if (isTerminalTask(job)) this.tasks.delete(id);
26281
- 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;
26282
26609
  }
26283
26610
  }
26284
26611
  async status(taskId = "") {
@@ -26308,6 +26635,8 @@ var CursorBridge = class {
26308
26635
  blockedQueuedCount: this.activeParallel.size > 0 ? this.queue.filter((job) => globalBlocked || job.effectiveExecution !== "parallel_agent").length : 0,
26309
26636
  activeParallel: [...this.activeParallel.values()].map((job) => this._taskView(job)),
26310
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,
26311
26640
  cdpPort: CDP_PORT2,
26312
26641
  lifecycle: this._lastLifecycle || {
26313
26642
  adapterPid: process.pid,
@@ -26373,7 +26702,7 @@ function buildToolDefinitions(bridgeInstance) {
26373
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." },
26374
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." },
26375
26704
  read_only: { type: "boolean", default: false, description: "Set true when Cursor must not change the workspace." },
26376
- 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." },
26377
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." },
26378
26707
  completion_contract: { type: "string", description: "Optional acceptance checks or a required final-report format." },
26379
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." },
@@ -26401,12 +26730,12 @@ function buildToolDefinitions(bridgeInstance) {
26401
26730
  },
26402
26731
  {
26403
26732
  name: "cursor_session_control",
26404
- 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.",
26405
26734
  inputSchema: {
26406
26735
  type: "object",
26407
26736
  properties: {
26408
26737
  session_id: { type: "string", description: "The exact session ID returned by cursor_do(session_mode=create)." },
26409
- 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." },
26410
26739
  confirm: { type: "boolean", default: false, description: "Required for forget and abandon." },
26411
26740
  reason: { type: "string", description: "Required and non-empty for abandon." },
26412
26741
  acknowledge_may_still_write: { type: "boolean", default: false, description: "Required for abandon; acknowledges that the underlying Cursor Agent may still run or write." }
@@ -26629,6 +26958,7 @@ export {
26629
26958
  EXPR_MODEL_PICKER_ROWS,
26630
26959
  EXPR_MODEL_PICKER_TRIGGER,
26631
26960
  EXPR_PAGE_CAPABILITIES,
26961
+ EXPR_PREPARE_INPUT,
26632
26962
  EXPR_PROVIDER_ERROR,
26633
26963
  EXPR_VISIBLE,
26634
26964
  EXPR_VISIBLE_COMPOSER,
@@ -26644,7 +26974,6 @@ export {
26644
26974
  exprClickBoundComposerStop,
26645
26975
  exprClickSelectedAgentStop,
26646
26976
  exprCreateAgentForWorkspace,
26647
- exprFill,
26648
26977
  exprInspectWorkspaceRepository,
26649
26978
  exprOpenAgent,
26650
26979
  isConfirmedCompletedReply,
@@ -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.11",
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.11",
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.1"
50
+ "embeddedProductVersion": "5.9.0"
51
51
  }
52
52
  }