@sideboard-ai/core 0.1.225 → 0.1.227

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.
@@ -418,7 +418,7 @@ async function continueSourceThread(threadId, prompt) {
418
418
  await continueOnReply(threadId, prompt);
419
419
  return;
420
420
  }
421
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-ZAJ2M4UW.js");
421
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-CWOSAYAE.js");
422
422
  await getOrchestrator2().send(threadId, prompt);
423
423
  } catch {
424
424
  }
@@ -1143,6 +1143,12 @@ function samePrIdentity(a, b) {
1143
1143
  const rn = right.match(/\/pull\/(\d+)/i)?.[1];
1144
1144
  return Boolean(ln && rn && ln === rn);
1145
1145
  }
1146
+ function shouldStopRunOnPrRetarget(previousPrUrl, nextPrUrl) {
1147
+ const prev = (previousPrUrl ?? "").trim();
1148
+ const next = (nextPrUrl ?? "").trim();
1149
+ if (!prev || !next) return false;
1150
+ return !samePrIdentity(prev, next);
1151
+ }
1146
1152
  function shouldPersistFetchedPrMeta(opts) {
1147
1153
  const meta = opts.metaUrl?.trim() || "";
1148
1154
  if (!meta) return false;
@@ -1197,6 +1203,7 @@ import {
1197
1203
  readdirSync,
1198
1204
  readFileSync as readFileSync4
1199
1205
  } from "fs";
1206
+ import { execFileSync } from "child_process";
1200
1207
  import { createServer } from "net";
1201
1208
  import { basename, dirname as dirname2, join as join7 } from "path";
1202
1209
  import { execa as execa2 } from "execa";
@@ -2177,6 +2184,44 @@ function pipeLines(stream, onLine) {
2177
2184
  const rl = createInterface2({ input: stream });
2178
2185
  rl.on("line", onLine);
2179
2186
  }
2187
+ function killListenersOnPorts(ports) {
2188
+ for (const port of ports) {
2189
+ if (!Number.isFinite(port) || port <= 0) continue;
2190
+ try {
2191
+ if (process.platform === "win32") {
2192
+ execFileSync(
2193
+ "powershell",
2194
+ [
2195
+ "-NoProfile",
2196
+ "-Command",
2197
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
2198
+ ],
2199
+ { stdio: "ignore", timeout: 3e3 }
2200
+ );
2201
+ continue;
2202
+ }
2203
+ let raw = "";
2204
+ try {
2205
+ raw = execFileSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
2206
+ encoding: "utf8",
2207
+ stdio: ["ignore", "pipe", "ignore"],
2208
+ timeout: 2e3
2209
+ });
2210
+ } catch {
2211
+ continue;
2212
+ }
2213
+ for (const token of raw.trim().split(/\s+/)) {
2214
+ const pid = Number(token);
2215
+ if (!Number.isFinite(pid) || pid <= 0) continue;
2216
+ try {
2217
+ process.kill(pid, "SIGTERM");
2218
+ } catch {
2219
+ }
2220
+ }
2221
+ } catch {
2222
+ }
2223
+ }
2224
+ }
2180
2225
  function killScriptTree(child, ports = []) {
2181
2226
  const pid = child.pid;
2182
2227
  if (pid) {
@@ -2199,26 +2244,7 @@ function killScriptTree(child, ports = []) {
2199
2244
  }
2200
2245
  }
2201
2246
  }
2202
- for (const port of ports) {
2203
- if (!Number.isFinite(port) || port <= 0) continue;
2204
- if (process.platform === "win32") {
2205
- void execa2(
2206
- "powershell",
2207
- [
2208
- "-NoProfile",
2209
- "-Command",
2210
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
2211
- ],
2212
- { reject: false }
2213
- );
2214
- } else {
2215
- void execa2(
2216
- "zsh",
2217
- ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
2218
- { reject: false }
2219
- );
2220
- }
2221
- }
2247
+ killListenersOnPorts(ports);
2222
2248
  }
2223
2249
  async function spawnWorkspaceScript(command, opts) {
2224
2250
  const loginEnv = await captureLoginEnv();
@@ -4385,7 +4411,7 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
4385
4411
  }
4386
4412
 
4387
4413
  // src/threads/adopt.ts
4388
- import { execFileSync } from "child_process";
4414
+ import { execFileSync as execFileSync2 } from "child_process";
4389
4415
  import {
4390
4416
  copyFileSync as copyFileSync2,
4391
4417
  existsSync as existsSync12,
@@ -4649,7 +4675,7 @@ function importConductorWorkspace(workspaceId) {
4649
4675
  }
4650
4676
  let repoPath = worktreePath;
4651
4677
  try {
4652
- repoPath = execFileSync("git", ["rev-parse", "--show-toplevel"], {
4678
+ repoPath = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
4653
4679
  cwd: worktreePath,
4654
4680
  encoding: "utf8"
4655
4681
  }).trim();
@@ -6957,7 +6983,7 @@ function formatScheduledPrompt(name, prompt) {
6957
6983
  ${prompt}`;
6958
6984
  }
6959
6985
  async function defaultDeps() {
6960
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-ZAJ2M4UW.js");
6986
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-CWOSAYAE.js");
6961
6987
  const orch = getOrchestrator2();
6962
6988
  return {
6963
6989
  findThread: (id) => findThreadByRef(id),
@@ -7094,6 +7120,16 @@ function writeLiveStatus(threadId, status, lastError) {
7094
7120
  if (!latest || latest.status === "archived") return latest;
7095
7121
  return setStatus(threadId, status, lastError);
7096
7122
  }
7123
+ function collectActiveRunPorts(runs) {
7124
+ const ports = /* @__PURE__ */ new Set();
7125
+ for (const run2 of runs) {
7126
+ const list = run2.ports?.length ? run2.ports : run2.port != null ? [run2.port] : [];
7127
+ for (const p of list) {
7128
+ if (Number.isFinite(p) && p > 0) ports.add(p);
7129
+ }
7130
+ }
7131
+ return [...ports];
7132
+ }
7097
7133
  var Orchestrator = class {
7098
7134
  events = new EventEmitter();
7099
7135
  processes = /* @__PURE__ */ new Map();
@@ -7235,6 +7271,9 @@ var Orchestrator = class {
7235
7271
  this.emit({ type: "status_changed", threadId: thread.id, status: "stopped" });
7236
7272
  }
7237
7273
  }
7274
+ if (reclaimStaleTurns) {
7275
+ this.reapOrphanedRunScripts();
7276
+ }
7238
7277
  const repoPaths = (repoPath ? [repoPath] : [...new Set(listThreads({ includeArchived: true }).map((t) => t.repoPath))]).filter((p) => !isGlobalRepoPath(p));
7239
7278
  await this.reconcileOrphans(repoPaths);
7240
7279
  try {
@@ -8453,13 +8492,30 @@ var Orchestrator = class {
8453
8492
  worktreeRunProcessKey(thread.worktreePath, scriptName),
8454
8493
  ...siblings.map((t) => `${t.id}:run:${scriptName}`)
8455
8494
  ];
8495
+ let hadHandle2 = false;
8456
8496
  for (const key of keys) {
8457
8497
  const proc = this.processes.get(key);
8458
- if (proc) proc.kill();
8498
+ if (proc) {
8499
+ proc.kill();
8500
+ hadHandle2 = true;
8501
+ }
8459
8502
  this.processes.delete(key);
8460
8503
  }
8504
+ const legacyKey = worktreeDevProcessKey(thread.worktreePath);
8505
+ const legacy = this.processes.get(legacyKey);
8506
+ if (legacy?.scriptName === scriptName || !legacy?.scriptName && scriptName === "dev") {
8507
+ if (legacy && !hadHandle2) {
8508
+ legacy.kill();
8509
+ hadHandle2 = true;
8510
+ }
8511
+ this.processes.delete(legacyKey);
8512
+ }
8513
+ const run2 = shared.activeRuns.find((r) => r.scriptName === scriptName);
8514
+ if (!hadHandle2 && run2) {
8515
+ killListenersOnPorts(collectActiveRunPorts([run2]));
8516
+ }
8461
8517
  const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
8462
- const isPrimary = shared.devPort != null && shared.activeRuns.find((r) => r.scriptName === scriptName)?.port === shared.devPort;
8518
+ const isPrimary = shared.devPort != null && run2?.port === shared.devPort;
8463
8519
  this.syncWorktreeRuns(
8464
8520
  thread.worktreePath,
8465
8521
  remaining,
@@ -8468,15 +8524,66 @@ var Orchestrator = class {
8468
8524
  this.emit({ type: "dev_server_stopped", threadId: thread.id, scriptName });
8469
8525
  return;
8470
8526
  }
8527
+ let hadHandle = false;
8471
8528
  for (const [key, proc] of [...this.processes.entries()]) {
8472
8529
  if (isWorktreeRunProcessKey(key, thread.worktreePath) || siblings.some((t) => key.startsWith(`${t.id}:run:`) || key === `${t.id}:dev`)) {
8473
8530
  proc.kill();
8531
+ hadHandle = true;
8474
8532
  this.processes.delete(key);
8475
8533
  }
8476
8534
  }
8535
+ if (!hadHandle && shared.activeRuns.length > 0) {
8536
+ killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
8537
+ }
8477
8538
  this.syncWorktreeRuns(thread.worktreePath, [], null);
8478
8539
  this.emit({ type: "dev_server_stopped", threadId: thread.id });
8479
8540
  }
8541
+ /**
8542
+ * App quit: tear down every registered run/setup child and free persisted
8543
+ * ports so the next launch is not blocked by orphans.
8544
+ */
8545
+ stopAllRunScripts() {
8546
+ for (const [key, proc] of [...this.processes.entries()]) {
8547
+ if (proc.kind !== "dev" && proc.kind !== "setup") continue;
8548
+ try {
8549
+ proc.kill();
8550
+ } catch {
8551
+ }
8552
+ this.processes.delete(key);
8553
+ }
8554
+ this.reapOrphanedRunScripts({ force: true });
8555
+ }
8556
+ /**
8557
+ * After restart (or force quit), in-memory handles are gone but `activeRuns`
8558
+ * and OS listeners may remain. Kill by port and clear disk metadata.
8559
+ * Skips worktrees that still have a live handle unless `force` (quit path).
8560
+ */
8561
+ reapOrphanedRunScripts(opts) {
8562
+ const force = opts?.force === true;
8563
+ const seen = /* @__PURE__ */ new Set();
8564
+ for (const thread of listThreads({ includeArchived: true })) {
8565
+ if (thread.status === "archived") continue;
8566
+ if (isGlobalThread(thread)) continue;
8567
+ const wt = normalizeWorktreePath(thread.worktreePath);
8568
+ if (seen.has(wt)) continue;
8569
+ seen.add(wt);
8570
+ const siblings = threadsSharingWorktree(thread.worktreePath);
8571
+ const shared = mergeWorktreeActiveRuns(siblings);
8572
+ if (shared.activeRuns.length === 0 && shared.devPort == null) continue;
8573
+ const hasLiveHandle = [...this.processes.keys()].some(
8574
+ (k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
8575
+ );
8576
+ if (!force && hasLiveHandle) continue;
8577
+ killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
8578
+ if (shared.devPort != null) {
8579
+ killListenersOnPorts([shared.devPort]);
8580
+ }
8581
+ this.syncWorktreeRuns(thread.worktreePath, [], null);
8582
+ for (const sibling of siblings) {
8583
+ this.emit({ type: "dev_server_stopped", threadId: sibling.id });
8584
+ }
8585
+ }
8586
+ }
8480
8587
  syncWorktreeRuns(worktreePath, activeRuns, devPort) {
8481
8588
  for (const sibling of threadsSharingWorktree(worktreePath)) {
8482
8589
  updateThread(sibling.id, { activeRuns, devPort });
@@ -8832,6 +8939,9 @@ var Orchestrator = class {
8832
8939
  this.assertNotGlobal(thread, "Land");
8833
8940
  const result = await confirmLand(thread, opts);
8834
8941
  if (result.prUrl) {
8942
+ if (shouldStopRunOnPrRetarget(thread.prUrl, result.prUrl)) {
8943
+ this.stopDev(thread.id);
8944
+ }
8835
8945
  const patch = { prUrl: result.prUrl };
8836
8946
  try {
8837
8947
  const meta = await getPrMeta(thread.worktreePath, result.prUrl);
@@ -8941,6 +9051,9 @@ var Orchestrator = class {
8941
9051
  async persistPrMetaAndMaybeArchive(thread, meta) {
8942
9052
  const prevState = normalizePrState(thread.prState);
8943
9053
  const nextState = normalizePrState(meta.state);
9054
+ if (shouldStopRunOnPrRetarget(thread.prUrl, meta.url)) {
9055
+ this.stopDev(thread.id);
9056
+ }
8944
9057
  const siblings = threadsSharingWorktree(thread.worktreePath);
8945
9058
  const targets = siblings.length > 0 ? siblings : [thread];
8946
9059
  for (const t of targets) {
@@ -8983,6 +9096,9 @@ var Orchestrator = class {
8983
9096
  if (current?.position != null && thread.stackLayer !== current.position) {
8984
9097
  patch.stackLayer = current.position;
8985
9098
  }
9099
+ if (current?.prUrl && shouldStopRunOnPrRetarget(thread.prUrl, current.prUrl)) {
9100
+ this.stopDev(thread.id);
9101
+ }
8986
9102
  if (current?.prUrl && current.prUrl !== thread.prUrl) patch.prUrl = current.prUrl;
8987
9103
  if (current?.title && current.title !== thread.prTitle) patch.prTitle = current.title;
8988
9104
  if (current?.branchName && current.branchName !== thread.branchName) {
@@ -482,7 +482,7 @@ async function continueSourceThread(threadId, prompt) {
482
482
  await continueOnReply(threadId, prompt);
483
483
  return;
484
484
  }
485
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-O3FNYIRE.js");
485
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-VKPHI64Q.js");
486
486
  await getOrchestrator2().send(threadId, prompt);
487
487
  } catch {
488
488
  }
@@ -1148,6 +1148,12 @@ function samePrIdentity(a, b) {
1148
1148
  const rn = right.match(/\/pull\/(\d+)/i)?.[1];
1149
1149
  return Boolean(ln && rn && ln === rn);
1150
1150
  }
1151
+ function shouldStopRunOnPrRetarget(previousPrUrl, nextPrUrl) {
1152
+ const prev = (previousPrUrl ?? "").trim();
1153
+ const next = (nextPrUrl ?? "").trim();
1154
+ if (!prev || !next) return false;
1155
+ return !samePrIdentity(prev, next);
1156
+ }
1151
1157
  function shouldPersistFetchedPrMeta(opts) {
1152
1158
  const meta = opts.metaUrl?.trim() || "";
1153
1159
  if (!meta) return false;
@@ -1202,6 +1208,7 @@ import {
1202
1208
  readdirSync,
1203
1209
  readFileSync as readFileSync4
1204
1210
  } from "fs";
1211
+ import { execFileSync } from "child_process";
1205
1212
  import { createServer } from "net";
1206
1213
  import { basename, dirname as dirname2, join as join7 } from "path";
1207
1214
  import { execa as execa2 } from "execa";
@@ -2235,6 +2242,44 @@ function pipeLines(stream, onLine) {
2235
2242
  const rl = createInterface2({ input: stream });
2236
2243
  rl.on("line", onLine);
2237
2244
  }
2245
+ function killListenersOnPorts(ports) {
2246
+ for (const port of ports) {
2247
+ if (!Number.isFinite(port) || port <= 0) continue;
2248
+ try {
2249
+ if (process.platform === "win32") {
2250
+ execFileSync(
2251
+ "powershell",
2252
+ [
2253
+ "-NoProfile",
2254
+ "-Command",
2255
+ `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
2256
+ ],
2257
+ { stdio: "ignore", timeout: 3e3 }
2258
+ );
2259
+ continue;
2260
+ }
2261
+ let raw = "";
2262
+ try {
2263
+ raw = execFileSync("lsof", [`-tiTCP:${port}`, "-sTCP:LISTEN"], {
2264
+ encoding: "utf8",
2265
+ stdio: ["ignore", "pipe", "ignore"],
2266
+ timeout: 2e3
2267
+ });
2268
+ } catch {
2269
+ continue;
2270
+ }
2271
+ for (const token of raw.trim().split(/\s+/)) {
2272
+ const pid = Number(token);
2273
+ if (!Number.isFinite(pid) || pid <= 0) continue;
2274
+ try {
2275
+ process.kill(pid, "SIGTERM");
2276
+ } catch {
2277
+ }
2278
+ }
2279
+ } catch {
2280
+ }
2281
+ }
2282
+ }
2238
2283
  function killScriptTree(child, ports = []) {
2239
2284
  const pid = child.pid;
2240
2285
  if (pid) {
@@ -2257,26 +2302,7 @@ function killScriptTree(child, ports = []) {
2257
2302
  }
2258
2303
  }
2259
2304
  }
2260
- for (const port of ports) {
2261
- if (!Number.isFinite(port) || port <= 0) continue;
2262
- if (process.platform === "win32") {
2263
- void execa2(
2264
- "powershell",
2265
- [
2266
- "-NoProfile",
2267
- "-Command",
2268
- `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }`
2269
- ],
2270
- { reject: false }
2271
- );
2272
- } else {
2273
- void execa2(
2274
- "zsh",
2275
- ["-lc", `pids=$(lsof -tiTCP:${port} -sTCP:LISTEN 2>/dev/null); [ -n "$pids" ] && kill -TERM $pids 2>/dev/null; true`],
2276
- { reject: false }
2277
- );
2278
- }
2279
- }
2305
+ killListenersOnPorts(ports);
2280
2306
  }
2281
2307
  async function spawnWorkspaceScript(command, opts) {
2282
2308
  const loginEnv = await captureLoginEnv();
@@ -4554,7 +4580,7 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
4554
4580
  }
4555
4581
 
4556
4582
  // src/threads/adopt.ts
4557
- import { execFileSync } from "child_process";
4583
+ import { execFileSync as execFileSync2 } from "child_process";
4558
4584
  import {
4559
4585
  copyFileSync as copyFileSync2,
4560
4586
  existsSync as existsSync12,
@@ -4821,7 +4847,7 @@ function importConductorWorkspace(workspaceId) {
4821
4847
  }
4822
4848
  let repoPath = worktreePath;
4823
4849
  try {
4824
- repoPath = execFileSync("git", ["rev-parse", "--show-toplevel"], {
4850
+ repoPath = execFileSync2("git", ["rev-parse", "--show-toplevel"], {
4825
4851
  cwd: worktreePath,
4826
4852
  encoding: "utf8"
4827
4853
  }).trim();
@@ -7332,7 +7358,7 @@ function formatScheduledPrompt(name, prompt) {
7332
7358
  ${prompt}`;
7333
7359
  }
7334
7360
  async function defaultDeps() {
7335
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-O3FNYIRE.js");
7361
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-VKPHI64Q.js");
7336
7362
  const orch = getOrchestrator2();
7337
7363
  return {
7338
7364
  findThread: (id) => findThreadByRef(id),
@@ -7469,6 +7495,16 @@ function writeLiveStatus(threadId, status, lastError) {
7469
7495
  if (!latest || latest.status === "archived") return latest;
7470
7496
  return setStatus(threadId, status, lastError);
7471
7497
  }
7498
+ function collectActiveRunPorts(runs) {
7499
+ const ports = /* @__PURE__ */ new Set();
7500
+ for (const run2 of runs) {
7501
+ const list = run2.ports?.length ? run2.ports : run2.port != null ? [run2.port] : [];
7502
+ for (const p of list) {
7503
+ if (Number.isFinite(p) && p > 0) ports.add(p);
7504
+ }
7505
+ }
7506
+ return [...ports];
7507
+ }
7472
7508
  var Orchestrator = class {
7473
7509
  events = new EventEmitter();
7474
7510
  processes = /* @__PURE__ */ new Map();
@@ -7610,6 +7646,9 @@ var Orchestrator = class {
7610
7646
  this.emit({ type: "status_changed", threadId: thread.id, status: "stopped" });
7611
7647
  }
7612
7648
  }
7649
+ if (reclaimStaleTurns) {
7650
+ this.reapOrphanedRunScripts();
7651
+ }
7613
7652
  const repoPaths = (repoPath ? [repoPath] : [...new Set(listThreads({ includeArchived: true }).map((t) => t.repoPath))]).filter((p) => !isGlobalRepoPath(p));
7614
7653
  await this.reconcileOrphans(repoPaths);
7615
7654
  try {
@@ -8828,13 +8867,30 @@ var Orchestrator = class {
8828
8867
  worktreeRunProcessKey(thread.worktreePath, scriptName),
8829
8868
  ...siblings.map((t) => `${t.id}:run:${scriptName}`)
8830
8869
  ];
8870
+ let hadHandle2 = false;
8831
8871
  for (const key of keys) {
8832
8872
  const proc = this.processes.get(key);
8833
- if (proc) proc.kill();
8873
+ if (proc) {
8874
+ proc.kill();
8875
+ hadHandle2 = true;
8876
+ }
8834
8877
  this.processes.delete(key);
8835
8878
  }
8879
+ const legacyKey = worktreeDevProcessKey(thread.worktreePath);
8880
+ const legacy = this.processes.get(legacyKey);
8881
+ if (legacy?.scriptName === scriptName || !legacy?.scriptName && scriptName === "dev") {
8882
+ if (legacy && !hadHandle2) {
8883
+ legacy.kill();
8884
+ hadHandle2 = true;
8885
+ }
8886
+ this.processes.delete(legacyKey);
8887
+ }
8888
+ const run2 = shared.activeRuns.find((r) => r.scriptName === scriptName);
8889
+ if (!hadHandle2 && run2) {
8890
+ killListenersOnPorts(collectActiveRunPorts([run2]));
8891
+ }
8836
8892
  const remaining = shared.activeRuns.filter((r) => r.scriptName !== scriptName);
8837
- const isPrimary = shared.devPort != null && shared.activeRuns.find((r) => r.scriptName === scriptName)?.port === shared.devPort;
8893
+ const isPrimary = shared.devPort != null && run2?.port === shared.devPort;
8838
8894
  this.syncWorktreeRuns(
8839
8895
  thread.worktreePath,
8840
8896
  remaining,
@@ -8843,15 +8899,66 @@ var Orchestrator = class {
8843
8899
  this.emit({ type: "dev_server_stopped", threadId: thread.id, scriptName });
8844
8900
  return;
8845
8901
  }
8902
+ let hadHandle = false;
8846
8903
  for (const [key, proc] of [...this.processes.entries()]) {
8847
8904
  if (isWorktreeRunProcessKey(key, thread.worktreePath) || siblings.some((t) => key.startsWith(`${t.id}:run:`) || key === `${t.id}:dev`)) {
8848
8905
  proc.kill();
8906
+ hadHandle = true;
8849
8907
  this.processes.delete(key);
8850
8908
  }
8851
8909
  }
8910
+ if (!hadHandle && shared.activeRuns.length > 0) {
8911
+ killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
8912
+ }
8852
8913
  this.syncWorktreeRuns(thread.worktreePath, [], null);
8853
8914
  this.emit({ type: "dev_server_stopped", threadId: thread.id });
8854
8915
  }
8916
+ /**
8917
+ * App quit: tear down every registered run/setup child and free persisted
8918
+ * ports so the next launch is not blocked by orphans.
8919
+ */
8920
+ stopAllRunScripts() {
8921
+ for (const [key, proc] of [...this.processes.entries()]) {
8922
+ if (proc.kind !== "dev" && proc.kind !== "setup") continue;
8923
+ try {
8924
+ proc.kill();
8925
+ } catch {
8926
+ }
8927
+ this.processes.delete(key);
8928
+ }
8929
+ this.reapOrphanedRunScripts({ force: true });
8930
+ }
8931
+ /**
8932
+ * After restart (or force quit), in-memory handles are gone but `activeRuns`
8933
+ * and OS listeners may remain. Kill by port and clear disk metadata.
8934
+ * Skips worktrees that still have a live handle unless `force` (quit path).
8935
+ */
8936
+ reapOrphanedRunScripts(opts) {
8937
+ const force = opts?.force === true;
8938
+ const seen = /* @__PURE__ */ new Set();
8939
+ for (const thread of listThreads({ includeArchived: true })) {
8940
+ if (thread.status === "archived") continue;
8941
+ if (isGlobalThread(thread)) continue;
8942
+ const wt = normalizeWorktreePath(thread.worktreePath);
8943
+ if (seen.has(wt)) continue;
8944
+ seen.add(wt);
8945
+ const siblings = threadsSharingWorktree(thread.worktreePath);
8946
+ const shared = mergeWorktreeActiveRuns(siblings);
8947
+ if (shared.activeRuns.length === 0 && shared.devPort == null) continue;
8948
+ const hasLiveHandle = [...this.processes.keys()].some(
8949
+ (k) => isWorktreeRunProcessKey(k, thread.worktreePath) || siblings.some((t) => k.startsWith(`${t.id}:run:`) || k === `${t.id}:dev`)
8950
+ );
8951
+ if (!force && hasLiveHandle) continue;
8952
+ killListenersOnPorts(collectActiveRunPorts(shared.activeRuns));
8953
+ if (shared.devPort != null) {
8954
+ killListenersOnPorts([shared.devPort]);
8955
+ }
8956
+ this.syncWorktreeRuns(thread.worktreePath, [], null);
8957
+ for (const sibling of siblings) {
8958
+ this.emit({ type: "dev_server_stopped", threadId: sibling.id });
8959
+ }
8960
+ }
8961
+ }
8855
8962
  syncWorktreeRuns(worktreePath, activeRuns, devPort) {
8856
8963
  for (const sibling of threadsSharingWorktree(worktreePath)) {
8857
8964
  updateThread(sibling.id, { activeRuns, devPort });
@@ -9207,6 +9314,9 @@ var Orchestrator = class {
9207
9314
  this.assertNotGlobal(thread, "Land");
9208
9315
  const result = await confirmLand(thread, opts);
9209
9316
  if (result.prUrl) {
9317
+ if (shouldStopRunOnPrRetarget(thread.prUrl, result.prUrl)) {
9318
+ this.stopDev(thread.id);
9319
+ }
9210
9320
  const patch = { prUrl: result.prUrl };
9211
9321
  try {
9212
9322
  const meta = await getPrMeta(thread.worktreePath, result.prUrl);
@@ -9316,6 +9426,9 @@ var Orchestrator = class {
9316
9426
  async persistPrMetaAndMaybeArchive(thread, meta) {
9317
9427
  const prevState = normalizePrState(thread.prState);
9318
9428
  const nextState = normalizePrState(meta.state);
9429
+ if (shouldStopRunOnPrRetarget(thread.prUrl, meta.url)) {
9430
+ this.stopDev(thread.id);
9431
+ }
9319
9432
  const siblings = threadsSharingWorktree(thread.worktreePath);
9320
9433
  const targets = siblings.length > 0 ? siblings : [thread];
9321
9434
  for (const t of targets) {
@@ -9358,6 +9471,9 @@ var Orchestrator = class {
9358
9471
  if (current?.position != null && thread.stackLayer !== current.position) {
9359
9472
  patch.stackLayer = current.position;
9360
9473
  }
9474
+ if (current?.prUrl && shouldStopRunOnPrRetarget(thread.prUrl, current.prUrl)) {
9475
+ this.stopDev(thread.id);
9476
+ }
9361
9477
  if (current?.prUrl && current.prUrl !== thread.prUrl) patch.prUrl = current.prUrl;
9362
9478
  if (current?.title && current.title !== thread.prTitle) patch.prTitle = current.title;
9363
9479
  if (current?.branchName && current.branchName !== thread.branchName) {
@@ -9924,6 +10040,7 @@ export {
9924
10040
  copyConfiguredFiles,
9925
10041
  captureLoginEnv,
9926
10042
  buildWorkspaceScriptEnv,
10043
+ killListenersOnPorts,
9927
10044
  runSetupScript,
9928
10045
  runConventionSetup,
9929
10046
  runWorkspaceSetup,