@sideboard-ai/core 0.1.39 → 0.1.41

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.
@@ -443,6 +443,7 @@ function normalizeThread(raw) {
443
443
  planMode: Boolean(raw.planMode),
444
444
  autonomy: raw.autonomy ?? "default",
445
445
  lastError: raw.lastError ?? null,
446
+ agentPid: raw.agentPid ?? null,
446
447
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
447
448
  prTitle: raw.prTitle ?? null,
448
449
  userSetTitle: Boolean(raw.userSetTitle),
@@ -478,7 +479,8 @@ function createEmptyThread(partial) {
478
479
  worktreePath: partial.worktreePath,
479
480
  repoPath: partial.repoPath,
480
481
  agent: partial.agent,
481
- lastError: null
482
+ lastError: null,
483
+ agentPid: null
482
484
  };
483
485
  }
484
486
  async function withThreadLock(id, fn) {
@@ -5615,15 +5617,62 @@ var init_workspaces = __esm({
5615
5617
  }
5616
5618
  });
5617
5619
 
5620
+ // src/agents/cursor-recover.ts
5621
+ var cursor_recover_exports = {};
5622
+ __export(cursor_recover_exports, {
5623
+ recoverFinishedCursorRun: () => recoverFinishedCursorRun
5624
+ });
5625
+ function recoverFinishedCursorRun(opts) {
5626
+ const agentId = opts.agentId.trim();
5627
+ if (!agentId) return null;
5628
+ const runsPath = (0, import_node_path23.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
5629
+ if (!(0, import_node_fs24.existsSync)(runsPath)) return null;
5630
+ try {
5631
+ const lines = (0, import_node_fs24.readFileSync)(runsPath, "utf8").split("\n");
5632
+ let best = null;
5633
+ for (const line of lines) {
5634
+ const trimmed = line.trim();
5635
+ if (!trimmed) continue;
5636
+ let row;
5637
+ try {
5638
+ row = JSON.parse(trimmed);
5639
+ } catch {
5640
+ continue;
5641
+ }
5642
+ if (row.agentId !== agentId) continue;
5643
+ if (row.status !== "finished") continue;
5644
+ if (typeof row.result !== "string" || !row.result.trim()) continue;
5645
+ const endedAt = typeof row.endedAt === "number" ? row.endedAt : 0;
5646
+ const createdAt = typeof row.createdAt === "number" ? row.createdAt : 0;
5647
+ if (createdAt < opts.startedAfterMs && endedAt < opts.startedAfterMs) continue;
5648
+ if (!best || endedAt >= best.endedAt) {
5649
+ best = { runId: row.runId || "", result: row.result.trim(), endedAt };
5650
+ }
5651
+ }
5652
+ return best;
5653
+ } catch {
5654
+ return null;
5655
+ }
5656
+ }
5657
+ var import_node_fs24, import_node_path23;
5658
+ var init_cursor_recover = __esm({
5659
+ "src/agents/cursor-recover.ts"() {
5660
+ "use strict";
5661
+ import_node_fs24 = require("fs");
5662
+ import_node_path23 = require("path");
5663
+ init_paths();
5664
+ }
5665
+ });
5666
+
5618
5667
  // src/mcp/server.ts
5619
5668
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
5620
5669
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5621
5670
  var import_zod = require("zod");
5622
- var import_node_path23 = require("path");
5671
+ var import_node_path24 = require("path");
5623
5672
 
5624
5673
  // src/orchestrator/orchestrator.ts
5625
5674
  var import_node_events = require("events");
5626
- var import_node_fs24 = require("fs");
5675
+ var import_node_fs25 = require("fs");
5627
5676
  init_error_detail();
5628
5677
 
5629
5678
  // src/agents/spawn.ts
@@ -8677,6 +8726,15 @@ async function syncThreadBranchFromGit(threadId) {
8677
8726
  init_workspaces();
8678
8727
  init_global_workspace();
8679
8728
  init_coordinator_prompt();
8729
+ function isPidAlive(pid) {
8730
+ if (!Number.isFinite(pid) || pid <= 0) return false;
8731
+ try {
8732
+ process.kill(pid, 0);
8733
+ return true;
8734
+ } catch {
8735
+ return false;
8736
+ }
8737
+ }
8680
8738
  var Orchestrator = class {
8681
8739
  events = new import_node_events.EventEmitter();
8682
8740
  processes = /* @__PURE__ */ new Map();
@@ -8713,8 +8771,18 @@ var Orchestrator = class {
8713
8771
  isStaleRunningThread(threadId, status) {
8714
8772
  return status === "running" && !this.activeTurns.has(threadId) && !this.startingTurns.has(threadId);
8715
8773
  }
8774
+ /**
8775
+ * Cross-process guard: another Sideboard process (MCP stdio) may call reconcile
8776
+ * while the desktop still owns a live agent child. Never reclaim those.
8777
+ */
8778
+ shouldReclaimRunningThread(thread) {
8779
+ if (!this.isStaleRunningThread(thread.id, thread.status)) return false;
8780
+ const pid = thread.agentPid;
8781
+ if (typeof pid === "number" && pid > 0 && isPidAlive(pid)) return false;
8782
+ return true;
8783
+ }
8716
8784
  async reconcile(repoPath, opts) {
8717
- const reclaimStaleTurns = opts?.reclaimStaleTurns !== false;
8785
+ const reclaimStaleTurns = opts?.reclaimStaleTurns === true;
8718
8786
  healOrchestrationSoccerTitles();
8719
8787
  for (const thread of listThreads({ includeArchived: true })) {
8720
8788
  if (thread.status === "archived") continue;
@@ -8730,18 +8798,18 @@ var Orchestrator = class {
8730
8798
  if (Object.keys(heal).length) {
8731
8799
  updateThread(thread.id, heal);
8732
8800
  }
8733
- if (reclaimStaleTurns && this.isStaleRunningThread(thread.id, thread.status)) {
8801
+ if (reclaimStaleTurns && this.shouldReclaimRunningThread(thread)) {
8734
8802
  setStatus(thread.id, "stopped", "Process died (reconciled on startup)");
8735
8803
  this.emit({ type: "status_changed", threadId: thread.id, status: "stopped" });
8736
8804
  }
8737
8805
  continue;
8738
8806
  }
8739
- if (!(0, import_node_fs24.existsSync)(thread.worktreePath)) {
8807
+ if (!(0, import_node_fs25.existsSync)(thread.worktreePath)) {
8740
8808
  setStatus(thread.id, "broken", "Worktree missing on disk");
8741
8809
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
8742
8810
  continue;
8743
8811
  }
8744
- if (reclaimStaleTurns && this.isStaleRunningThread(thread.id, thread.status)) {
8812
+ if (reclaimStaleTurns && this.shouldReclaimRunningThread(thread)) {
8745
8813
  setStatus(thread.id, "stopped", "Process died (reconciled on startup)");
8746
8814
  this.emit({ type: "status_changed", threadId: thread.id, status: "stopped" });
8747
8815
  }
@@ -8923,6 +8991,11 @@ var Orchestrator = class {
8923
8991
  await new Promise((r) => setTimeout(r, 100));
8924
8992
  continue;
8925
8993
  }
8994
+ const livePid = thread.agentPid;
8995
+ if (typeof livePid === "number" && livePid > 0 && isPidAlive(livePid)) {
8996
+ await new Promise((r) => setTimeout(r, 200));
8997
+ continue;
8998
+ }
8926
8999
  const prompt = thread.queue[0];
8927
9000
  const remaining = thread.queue.slice(1);
8928
9001
  updateThread(threadId, { queue: remaining });
@@ -9066,10 +9139,18 @@ var Orchestrator = class {
9066
9139
  if (event.type === "stderr" && typeof event.data === "string") {
9067
9140
  pushTurnStderr(stderrTail, event.data);
9068
9141
  }
9142
+ const live = readThread(threadId);
9143
+ if (live?.lastError?.includes("reconciled on startup") && (this.activeTurns.has(threadId) || this.startingTurns.has(threadId))) {
9144
+ setStatus(threadId, "running");
9145
+ this.emit({ type: "status_changed", threadId, status: "running" });
9146
+ }
9069
9147
  }
9070
9148
  );
9071
9149
  this.activeTurns.set(threadId, handle);
9072
9150
  this.startingTurns.delete(threadId);
9151
+ if (typeof handle.pid === "number" && handle.pid > 0) {
9152
+ updateThread(threadId, { agentPid: handle.pid });
9153
+ }
9073
9154
  if (this.stoppedTurns.has(threadId)) {
9074
9155
  handle.kill();
9075
9156
  } else {
@@ -9089,20 +9170,41 @@ var Orchestrator = class {
9089
9170
  if (result.sessionId) {
9090
9171
  updateThread(threadId, { sessionId: result.sessionId });
9091
9172
  }
9092
- const assistantText = result.assistantText.trim();
9093
- const failureOnlyMessage = result.exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !result.parts.some((p) => p.type === "tool" || p.type === "thinking");
9094
- if (!failureOnlyMessage && (assistantText || result.parts.length > 0)) {
9173
+ let assistantText = result.assistantText.trim();
9174
+ let parts = result.parts;
9175
+ let usage = result.usage ?? void 0;
9176
+ let exitCode = result.exitCode;
9177
+ if (this.requireThread(threadId).agent === "cursor" && exitCode !== 0 && !assistantText && parts.length === 0) {
9178
+ const sessionId = result.sessionId || this.requireThread(threadId).sessionId || "";
9179
+ if (sessionId) {
9180
+ const { recoverFinishedCursorRun: recoverFinishedCursorRun2 } = await Promise.resolve().then(() => (init_cursor_recover(), cursor_recover_exports));
9181
+ for (let i = 0; i < 8; i++) {
9182
+ const recovered = recoverFinishedCursorRun2({
9183
+ agentId: sessionId,
9184
+ startedAfterMs: turnStartedAt - 5e3
9185
+ });
9186
+ if (recovered?.result) {
9187
+ assistantText = recovered.result;
9188
+ exitCode = 0;
9189
+ break;
9190
+ }
9191
+ await new Promise((r) => setTimeout(r, 500));
9192
+ }
9193
+ }
9194
+ }
9195
+ const failureOnlyMessage = exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !parts.some((p) => p.type === "tool" || p.type === "thinking");
9196
+ if (!failureOnlyMessage && (assistantText || parts.length > 0)) {
9095
9197
  appendMessage(threadId, {
9096
9198
  role: "agent",
9097
9199
  text: assistantText,
9098
- parts: result.parts.length > 0 ? result.parts : void 0,
9200
+ parts: parts.length > 0 ? parts : void 0,
9099
9201
  durationMs: Math.max(0, Date.now() - turnStartedAt),
9100
- usage: result.usage ?? void 0,
9202
+ usage,
9101
9203
  ts: (/* @__PURE__ */ new Date()).toISOString()
9102
9204
  });
9103
9205
  }
9104
9206
  const afterTurn = this.requireThread(threadId);
9105
- if (afterTurn.planMode && afterTurn.agent === "claude" && result.parts.some(
9207
+ if (afterTurn.planMode && afterTurn.agent === "claude" && parts.some(
9106
9208
  (p) => p.type === "tool" && /exitplanmode/i.test(p.name)
9107
9209
  )) {
9108
9210
  updateThread(threadId, { sessionId: null });
@@ -9111,22 +9213,22 @@ var Orchestrator = class {
9111
9213
  if (this.stoppedTurns.has(threadId)) {
9112
9214
  setStatus(threadId, "stopped");
9113
9215
  this.emit({ type: "status_changed", threadId, status: "stopped" });
9114
- this.emit({ type: "turn_finished", threadId, exitCode: result.exitCode });
9216
+ this.emit({ type: "turn_finished", threadId, exitCode });
9115
9217
  } else {
9116
9218
  const lastStderr = summarizeTurnStderr(stderrTail);
9117
- const detail = lastStderr || (result.exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
9118
- const failDetail = formatTurnExitError(result.exitCode, detail);
9219
+ const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
9220
+ const failDetail = formatTurnExitError(exitCode, detail);
9119
9221
  setStatus(
9120
9222
  threadId,
9121
- result.exitCode === 0 ? "idle" : "error",
9122
- result.exitCode === 0 ? null : failDetail
9223
+ exitCode === 0 ? "idle" : "error",
9224
+ exitCode === 0 ? null : failDetail
9123
9225
  );
9124
9226
  this.emit({
9125
9227
  type: "status_changed",
9126
9228
  threadId,
9127
- status: result.exitCode === 0 ? "idle" : "error"
9229
+ status: exitCode === 0 ? "idle" : "error"
9128
9230
  });
9129
- this.emit({ type: "turn_finished", threadId, exitCode: result.exitCode });
9231
+ this.emit({ type: "turn_finished", threadId, exitCode });
9130
9232
  }
9131
9233
  } catch (err) {
9132
9234
  const message = err instanceof Error ? err.message : String(err);
@@ -9147,6 +9249,10 @@ var Orchestrator = class {
9147
9249
  this.processes.delete(`${threadId}:agent`);
9148
9250
  this.stoppedTurns.delete(threadId);
9149
9251
  this.runningCount = Math.max(0, this.runningCount - 1);
9252
+ try {
9253
+ updateThread(threadId, { agentPid: null });
9254
+ } catch {
9255
+ }
9150
9256
  }
9151
9257
  }
9152
9258
  /**
@@ -9719,7 +9825,7 @@ var Orchestrator = class {
9719
9825
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
9720
9826
  return setStatus(thread.id, "idle");
9721
9827
  }
9722
- if (!(0, import_node_fs24.existsSync)(thread.worktreePath)) {
9828
+ if (!(0, import_node_fs25.existsSync)(thread.worktreePath)) {
9723
9829
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
9724
9830
  const { execa: execa7 } = await import("execa");
9725
9831
  const slug = thread.worktreePath.split("/").pop();
@@ -9937,7 +10043,7 @@ async function startMcpServer() {
9937
10043
  async () => {
9938
10044
  const threads = orch.getThreads(true);
9939
10045
  const lines = threads.map((t) => {
9940
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path23.basename)(t.repoPath) || t.repoPath;
10046
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path24.basename)(t.repoPath) || t.repoPath;
9941
10047
  return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
9942
10048
  });
9943
10049
  return {
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startMcpServer
4
- } from "../chunk-2YFQNL3O.js";
5
- import "../chunk-FVGRUZHI.js";
6
- import "../chunk-IS3AGU33.js";
7
- import "../chunk-PTASB7SJ.js";
8
- import "../chunk-BEXVE7LX.js";
4
+ } from "../chunk-WD35X6U5.js";
5
+ import "../chunk-44LYDJFB.js";
6
+ import "../chunk-6TZSJMXF.js";
7
+ import "../chunk-UPMGXM4X.js";
8
+ import "../chunk-SX2R2PCE.js";
9
9
  import "../chunk-ILQK4P5R.js";
10
10
  import "../chunk-PU27NUO4.js";
11
11
  import "../chunk-YZ23S32T.js";
12
- import "../chunk-XBEQI5H4.js";
13
- import "../chunk-HYRHI3QU.js";
12
+ import "../chunk-VA2U5EQH.js";
13
+ import "../chunk-5UIKSPDD.js";
14
14
  import "../chunk-M37RITA6.js";
15
15
  import "../chunk-AJ6ROGD7.js";
16
16
 
@@ -10,7 +10,7 @@ import {
10
10
  updateThread,
11
11
  withThreadLock,
12
12
  writeThread
13
- } from "./chunk-HYRHI3QU.js";
13
+ } from "./chunk-5UIKSPDD.js";
14
14
  import "./chunk-M37RITA6.js";
15
15
  export {
16
16
  appendMessage,
@@ -4,11 +4,11 @@ import {
4
4
  listWorkspaces,
5
5
  removeWorkspace,
6
6
  syncWorkspacesFromThreads
7
- } from "./chunk-FVGRUZHI.js";
8
- import "./chunk-IS3AGU33.js";
9
- import "./chunk-PTASB7SJ.js";
10
- import "./chunk-XBEQI5H4.js";
11
- import "./chunk-HYRHI3QU.js";
7
+ } from "./chunk-44LYDJFB.js";
8
+ import "./chunk-6TZSJMXF.js";
9
+ import "./chunk-UPMGXM4X.js";
10
+ import "./chunk-VA2U5EQH.js";
11
+ import "./chunk-5UIKSPDD.js";
12
12
  import "./chunk-M37RITA6.js";
13
13
  import "./chunk-AJ6ROGD7.js";
14
14
  export {
@@ -41,8 +41,8 @@ import {
41
41
  worktreeDisplayLabel,
42
42
  worktreeDisplayLabelForGroup,
43
43
  worktreeNameFromPath
44
- } from "./chunk-XBEQI5H4.js";
45
- import "./chunk-HYRHI3QU.js";
44
+ } from "./chunk-VA2U5EQH.js";
45
+ import "./chunk-5UIKSPDD.js";
46
46
  import "./chunk-M37RITA6.js";
47
47
  import "./chunk-AJ6ROGD7.js";
48
48
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sideboard-ai/core",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Sideboard core — orchestration, agents, git worktrees, MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",