@coreplane/switchboard 1.249.0 → 1.249.1

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.
@@ -262,7 +262,7 @@ execution:
262
262
  # baseUrl: https://switchboard-resident.<account>.workers.dev
263
263
  # tokenEnv: RESIDENT_OPERATOR_TOKEN # operator bearer secret (default)
264
264
  # adminTokenEnv: RESIDENT_ADMIN_TOKEN # admin bearer for `repo ...` commands (default)
265
- # probeTimeoutMs: 2000 # /status probe budget; timeout = not warm
265
+ # probeTimeoutMs: 8000 # /status probe deadline; a miss = not warm for that dispatch, never an outage
266
266
 
267
267
  # Where per-conversation agent workspaces are created (local execution only).
268
268
  # Under docker compose this directory is a named volume, so checkouts survive a restart.
@@ -21,6 +21,7 @@ import {
21
21
  } from "../../src/execution/residentInstanceId.js";
22
22
  import { RESTORE_MAX_MS, type RefreshPlan } from "../../src/execution/residentRefresh.js";
23
23
  import { residentText } from "../../src/execution/residentText.js";
24
+ import { RUNTIME_BUSY_REASON } from "../../src/execution/sandboxErrors.js";
24
25
  import { graftResidentSteps } from "../../src/execution/residentTrace.js";
25
26
  import {
26
27
  DEFAULT_EXEC_TIMEOUT_MS,
@@ -288,9 +289,10 @@ export class ResidentRefresh extends WorkflowEntrypoint<Env, RefreshInstancePara
288
289
  }
289
290
  // Housekeeping rides on every instance whatever the cycle's verdict — a
290
291
  // parked resident still releases its idle bindings and re-measures, and
291
- // neither step wakes a slept container — except an offboarded one:
292
- // nothing is left to keep.
293
- if (cycle.outcome !== "offboarded") {
292
+ // neither step wakes a slept container — except an offboarded one
293
+ // (nothing is left to keep) and one whose container turned the cycle
294
+ // away (item 68: the sweep and the measure exec on that same container).
295
+ if (cycle.outcome !== "offboarded" && cycle.outcome !== RUNTIME_BUSY_REASON) {
294
296
  const swept = await step.do("sweep", { retries, timeout: stepTimeoutMs(REFRESH_SWEEP_STEP_BUDGET_MS) }, () =>
295
297
  stub.refreshInstanceSweep({ resource, instance }),
296
298
  );
@@ -159,7 +159,11 @@ import {
159
159
  } from "../../src/core/schedules.js";
160
160
  import type { ResidentLifecycleState } from "../../src/execution/residentState.js";
161
161
  import { RestoreWaiters } from "../../src/execution/restoreWaiters.js";
162
- import { isRuntimeBusySignal, SandboxRuntimeBusyError } from "../../src/execution/sandboxErrors.js";
162
+ import {
163
+ isRuntimeBusySignal,
164
+ RUNTIME_BUSY_REASON,
165
+ SandboxRuntimeBusyError,
166
+ } from "../../src/execution/sandboxErrors.js";
163
167
  import {
164
168
  decisivePull,
165
169
  effectiveLimits,
@@ -613,6 +617,12 @@ const LIFECYCLE_KEY = "resident:lifecycle";
613
617
  * resident, with the step it last reported and the cycle lease it holds, and
614
618
  * the last bucket the cron skipped (a live cycle, a duplicate id). */
615
619
  const REFRESH_INSTANCE_KEY = "resident:refreshInstance";
620
+ /** The settled state a cycle found before it wrote `refreshing` (item 68):
621
+ * what a cycle that yields to a busy container puts back. Rewritten by every
622
+ * cycle right before its `refreshing` write, so it always names the state
623
+ * under the current marker and nothing older. */
624
+ const REFRESHING_FROM_KEY = "resident:refreshingFrom";
625
+ type RefreshingFrom = ResidentStatus;
616
626
  /** A `du` over a multi-GB checkout plus every live tree is seconds warm, tens
617
627
  * of seconds on a cold page cache — the same class as a git network step. */
618
628
  const DU_TIMEOUT_MS = GIT_NETWORK_TIMEOUT_MS;
@@ -3358,6 +3368,9 @@ export class ResidentDO extends Sandbox<Env> {
3358
3368
  }
3359
3369
  }
3360
3370
 
3371
+ // Item 68: remember what `refreshing` covers, so a cycle the busy container
3372
+ // turns away can put it back — the last snapshot never stopped serving.
3373
+ await this.ctx.storage.put(REFRESHING_FROM_KEY, (await this.getStatus()) satisfies RefreshingFrom);
3361
3374
  await this.setResidentState("refreshing");
3362
3375
  let sha: string;
3363
3376
  try {
@@ -3380,6 +3393,9 @@ export class ResidentDO extends Sandbox<Env> {
3380
3393
  // and die at git-setup). Name the disk instead: not
3381
3394
  // serviceable, and the recovery below can free it.
3382
3395
  const failure = await this.classifyFailure("fetch", message);
3396
+ // Item 68: the container did not accept the connect — a run's command
3397
+ // has its cores. Not GitHub, not the mirror: the instance step yields.
3398
+ if (failure.busy) throw err;
3383
3399
  if (failure.diskFull) {
3384
3400
  await this.setResidentState("degraded", failure.reason);
3385
3401
  await this.recoverFromDiskFull(failure.reason, selfInFlight);
@@ -3533,7 +3549,9 @@ export class ResidentDO extends Sandbox<Env> {
3533
3549
  * error can surface between steps — with the generic "refresh" step, whose
3534
3550
  * failure reason is the `refresh-failed: …` shape. A full disk is a third
3535
3551
  * class: `disk-full: …`, never serviceable, and the one failure the
3536
- * resident can act on itself (recoverFromDiskFull). */
3552
+ * resident can act on itself (recoverFromDiskFull). A container that did
3553
+ * not accept the connect (`runtime-busy`, item 68) is a fourth: `busy`,
3554
+ * and the cycle yields to the run that holds it. */
3537
3555
  private async classifyCycleError(err: unknown): Promise<RefreshFailure> {
3538
3556
  // The control port never answered (item 64): the count decides, and a disk
3539
3557
  // probe would only cost another 30 s abort against the same silent port.
@@ -3911,7 +3929,10 @@ export class ResidentDO extends Sandbox<Env> {
3911
3929
  * on purpose (`CycleRestartError`), is thrown to the engine, whose retry
3912
3930
  * re-enters the same idempotent method — the row stays `refreshing`, never
3913
3931
  * `degraded`, and a `refreshing` younger than the stale bound keeps the
3914
- * cron from creating a second instance meanwhile. A failure of the repo's
3932
+ * cron from creating a second instance meanwhile. A container that turned
3933
+ * the step's connect away (`runtime-busy`, item 68) ends the cycle as
3934
+ * `stopped` with nothing recorded — `yieldCycle` puts back the state the
3935
+ * cycle found. A failure of the repo's
3915
3936
  * own is recorded — `degraded` with the reason, the last snapshot still
3916
3937
  * serving — and answered `failed`, which ends the cycle; the next cron
3917
3938
  * firing starts the next one from that state. */
@@ -3963,6 +3984,21 @@ export class ResidentDO extends Sandbox<Env> {
3963
3984
  return { ...result, startedAt, trace: trace.steps() };
3964
3985
  }
3965
3986
  const failure = await this.classifyCycleError(err);
3987
+ if (failure.busy) {
3988
+ // Item 68: the container did not accept the cycle's connect — a run's
3989
+ // command has its cores. Nothing ran and nothing about the repository
3990
+ // is known: the cycle yields, the state it found goes back, and no
3991
+ // failure is recorded — not `degraded`, not a rung of item 67's ladder
3992
+ // (three cycles of it used to destroy the container under the run).
3993
+ // The next cron firing tries again; the engine is not asked to retry
3994
+ // into the same busy container.
3995
+ outcome = `yielded (${failure.reason})`;
3996
+ console.log(
3997
+ `refresh instance ${instance}: ${step} yielded — ${failure.reason.slice(0, 400)}; the next cycle retries`,
3998
+ );
3999
+ await this.yieldCycle(instance);
4000
+ return { status: "stopped", why: RUNTIME_BUSY_REASON, startedAt, trace: trace.steps() };
4001
+ }
3966
4002
  if (failure.interrupted) {
3967
4003
  outcome = `interrupted (${failure.reason}) — the engine retries`;
3968
4004
  console.log(`refresh instance ${instance}: ${step} interrupted — ${failure.reason.slice(0, 400)}; retrying`);
@@ -3984,6 +4020,27 @@ export class ResidentDO extends Sandbox<Env> {
3984
4020
  }
3985
4021
  }
3986
4022
 
4023
+ /** A cycle that met the busy container ends here (item 68): the lease it
4024
+ * holds goes back, and the `refreshing` it wrote is undone to the settled
4025
+ * state it found, so the row says what the last snapshot still is — warm,
4026
+ * or the degraded an earlier cycle earned — never a `degraded` of this
4027
+ * cycle's own. A `refreshing` this cycle did not write (a step past the
4028
+ * fetch found the marker of a cycle that died mid-flight) is left to the
4029
+ * watchdog, which normalizes it. */
4030
+ private async yieldCycle(instance: string): Promise<void> {
4031
+ await this.clearInstanceLease(instance);
4032
+ const status = await this.getStatus();
4033
+ if (status.state !== "refreshing") return;
4034
+ const from = await this.ctx.storage.get<RefreshingFrom>(REFRESHING_FROM_KEY);
4035
+ if (!from || (from.state !== "warm" && from.state !== "degraded")) {
4036
+ console.log(
4037
+ `refresh instance ${instance}: yielded from \`refreshing\` over ${from?.state ?? "no recorded state"}; left for the watchdog`,
4038
+ );
4039
+ return;
4040
+ }
4041
+ await this.setResidentState(from.state, from.reason);
4042
+ }
4043
+
3987
4044
  /** Step `fetch`: the gates, the cycle lease, the fetch and the plan. The
3988
4045
  * instance id is the cycle `fetchMirror` records, so a retry of this step
3989
4046
  * finds its fetch done. A rebuild's markers come off here, once per cycle,
@@ -4227,7 +4284,8 @@ export class ResidentDO extends Sandbox<Env> {
4227
4284
  * errno, so a full-disk attach reads like a lock bug without the probe. */
4228
4285
  private async classifyFailure(step: string, message: string): Promise<RefreshFailure> {
4229
4286
  const direct = classifyRefreshFailure({ step, message });
4230
- if (direct.diskFull || direct.interrupted) return direct;
4287
+ // A busy container (item 68) would only refuse the disk probe's exec too.
4288
+ if (direct.diskFull || direct.interrupted || direct.busy) return direct;
4231
4289
  return classifyRefreshFailure({ step, message, freeKiB: await this.freeKiB() });
4232
4290
  }
4233
4291
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.249.0",
3
+ "version": "1.249.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.249.0",
9
+ "version": "1.249.1",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -20445,7 +20445,7 @@
20445
20445
  },
20446
20446
  "packages/switchboard": {
20447
20447
  "name": "@coreplane/switchboard",
20448
- "version": "1.249.0",
20448
+ "version": "1.249.1",
20449
20449
  "license": "Apache-2.0",
20450
20450
  "dependencies": {
20451
20451
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.249.0",
3
+ "version": "1.249.1",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.249.0",
3
- "commit": "1145eea8ffdd6a2bd1ae8f1b26a4919db2caf47d",
4
- "builtAt": "2026-09-18T07:16:19.891Z"
2
+ "version": "1.249.1",
3
+ "commit": "7fa16da36339e9c0b04e3f9c037002efcf5975f4",
4
+ "builtAt": "2026-09-18T08:08:43.148Z"
5
5
  }
@@ -1781,9 +1781,17 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
1781
1781
  : "Remaining gate: a person's merge — the runner merges only when the instance's `merge` field says runner, and ship never approves.",
1782
1782
  ].join("\n");
1783
1783
  case "merge_refused":
1784
+ // The approved work is on the branch, so the remedy is a person's hand
1785
+ // merge, never a re-run: a seeded plan re-issued afterwards finds the
1786
+ // merged pull request (the pre-check's `merged` by other, or
1787
+ // `already_landed`) and moves on to the dependents. The generated
1788
+ // plan's line already says to re-issue with the PR URL, which takes the
1789
+ // same recognition path.
1784
1790
  return join([
1785
1791
  `⚠️ The review approved ${e.pr.url} but the runner did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
1786
- reissue,
1792
+ s.input.generated
1793
+ ? reissue
1794
+ : `The approved work is on the branch: rebase or fix it, push, and merge it by hand. Then re-issue the plan naming the remaining units — a unit whose pull request has merged is recognized and not run again, and its dependents start from there.`,
1787
1795
  ]);
1788
1796
  case "round_cap":
1789
1797
  return join([
@@ -22,6 +22,7 @@
22
22
  * interrupted one stopped instead of redoing the whole rebuild. */
23
23
 
24
24
  import { DISK_FULL_FREE_KIB, diskFullReason, isDiskFullMessage } from "./residentDisk.js";
25
+ import { carriesRuntimeBusyToken } from "./sandboxErrors.js";
25
26
 
26
27
  export interface RefreshDisk {
27
28
  /** `git rev-parse HEAD` of the warm checkout; null when unreadable. */
@@ -121,6 +122,9 @@ export function checkoutUpdateCommand(sha: string, clean: CleanScope): string {
121
122
  * repository: the container's control port never answered the SDK's connect
122
123
  * (`runtimeUnreachableReason`), so no command ran at all — the resident
123
124
  * escalates on its persisted count instead of recording a command failure.
125
+ * `busy` is the third: the container did not accept the cycle's connect
126
+ * inside the platform's allowance (`runtime-busy`, resident-repos item 68) —
127
+ * a run's command has its cores; nothing ran, and the cycle yields to it.
124
128
  * Everything else is the repo's own build failing. */
125
129
  export interface RefreshFailure {
126
130
  /** The reason. An interruption's is prefixed `refresh-interrupted:` and is
@@ -130,7 +134,8 @@ export interface RefreshFailure {
130
134
  * `disk-full:` (`residentDisk.ts` — the cycle's entry gate and the recycle
131
135
  * decision key on it); an unanswered control port is `runtime-unreachable:`
132
136
  * with the attempt count (the ladder below decides what the resident does
133
- * about it); real failures keep `<step>-failed:`. */
137
+ * about it); a busy container's is `refresh-yielded:` (the cycle ends
138
+ * `stopped`, the state it found put back); real failures keep `<step>-failed:`. */
134
139
  reason: string;
135
140
  /** The step that failed, as the caller named it (`refresh` for a failure
136
141
  * between steps): item 67 reads whether it ran the repository's own command
@@ -139,6 +144,7 @@ export interface RefreshFailure {
139
144
  interrupted: boolean;
140
145
  diskFull: boolean;
141
146
  runtimeUnreachable: boolean;
147
+ busy: boolean;
142
148
  }
143
149
 
144
150
  /** Root argv that kills every process the build user still owns and waits
@@ -290,15 +296,31 @@ export function classifyRefreshFailure(input: {
290
296
  interrupted: false,
291
297
  diskFull: false,
292
298
  runtimeUnreachable: true,
299
+ busy: false,
293
300
  reason: runtimeUnreachableReason(input.runtimeUnreachable.count),
294
301
  };
295
302
  }
303
+ // Item 68: the token the Worker's `run()` named the refused connect with,
304
+ // anywhere in the text — a wrapper (a StepError, the fetch's mint prefix)
305
+ // keeps it. Decided before the disk inference: no command ran, so a low
306
+ // free-disk reading says nothing about this failure.
307
+ if (carriesRuntimeBusyToken(message)) {
308
+ return {
309
+ step,
310
+ interrupted: false,
311
+ diskFull: false,
312
+ runtimeUnreachable: false,
313
+ busy: true,
314
+ reason: `refresh-yielded: ${step} ${message}`,
315
+ };
316
+ }
296
317
  if (isDiskFullMessage(message)) {
297
318
  return {
298
319
  step,
299
320
  interrupted: false,
300
321
  diskFull: true,
301
322
  runtimeUnreachable: false,
323
+ busy: false,
302
324
  reason: diskFullReason({ step, message, freeKiB: input.freeKiB ?? null }),
303
325
  };
304
326
  }
@@ -309,6 +331,7 @@ export function classifyRefreshFailure(input: {
309
331
  interrupted: true,
310
332
  diskFull: false,
311
333
  runtimeUnreachable: false,
334
+ busy: false,
312
335
  reason: `refresh-interrupted: ${step} ${message}`,
313
336
  };
314
337
  }
@@ -318,6 +341,7 @@ export function classifyRefreshFailure(input: {
318
341
  interrupted: false,
319
342
  diskFull: true,
320
343
  runtimeUnreachable: false,
344
+ busy: false,
321
345
  reason: diskFullReason({ step, message, freeKiB: input.freeKiB }),
322
346
  };
323
347
  }
@@ -326,6 +350,7 @@ export function classifyRefreshFailure(input: {
326
350
  interrupted: false,
327
351
  diskFull: false,
328
352
  runtimeUnreachable: false,
353
+ busy: false,
329
354
  reason: `${step}-failed: ${message}`,
330
355
  };
331
356
  }
@@ -403,6 +403,14 @@ export function isRuntimeBusyError(err: unknown): boolean {
403
403
  return s.name === RUNTIME_BUSY_ERROR_NAME || !!s.message?.startsWith(`${RUNTIME_BUSY_REASON}:`);
404
404
  }
405
405
 
406
+ /** Does a failure's text carry the token anywhere — the typed error's own
407
+ * message, or that message behind a wrapper's prefix (a `StepError` built
408
+ * from it, the fetch step's mint prefix)? The refresh cycle's classifier
409
+ * reads this: the token decides, never the platform's words. */
410
+ export function carriesRuntimeBusyToken(message: string): boolean {
411
+ return new RegExp(`(?:^|[\\s;(])${RUNTIME_BUSY_REASON}: `).test(message);
412
+ }
413
+
406
414
  /** The `/read` and `/write` answer (sent as HTTP 503): the token and the text. */
407
415
  export function runtimeBusyAnswer(message: string): { error: string; reason: typeof RUNTIME_BUSY_REASON } {
408
416
  return { error: message, reason: RUNTIME_BUSY_REASON };
package/dist/cli.js CHANGED
@@ -10136,6 +10136,9 @@ function execDeadline(timeoutMs, signal2) {
10136
10136
  signal2.addEventListener("abort", onStop, { once: true });
10137
10137
  return AbortSignal.any([deadline.signal, signal2]);
10138
10138
  }
10139
+ function isDeadlineMiss(err2) {
10140
+ return err2 instanceof Error && err2.name === "TimeoutError";
10141
+ }
10139
10142
  function isRunStopError(err2) {
10140
10143
  return err2 instanceof ExecInfraError && err2.reason === "aborted";
10141
10144
  }
@@ -11121,6 +11124,7 @@ var init_residentRefresh = __esm({
11121
11124
  "../../src/execution/residentRefresh.ts"() {
11122
11125
  "use strict";
11123
11126
  init_residentDisk();
11127
+ init_sandboxErrors();
11124
11128
  RUNTIME_MOVED_WORDING = /previous runtime incarnation|interrupted because the runtime changed|runtime identity is no longer active|sandbox lifetime is no longer current|platform was updating the sandbox runtime|no longer identifies pid/i;
11125
11129
  STOPPED_CONTAINER_WORDING = /process supervisor is closed|container is not running, consider calling start|the container just exited/i;
11126
11130
  RUNTIME_REPLACEMENT_WORDING = new RegExp(
@@ -11498,6 +11502,7 @@ var init_resident = __esm({
11498
11502
  init_residentDiskBudget();
11499
11503
  init_sandboxErrors();
11500
11504
  init_executor();
11505
+ init_executor();
11501
11506
  DETACH_TIMEOUT_MS = 1e4;
11502
11507
  DRAIN_POLL_MS = DRAIN.pollMs;
11503
11508
  DRAIN_WAIT_MAX_MS = DRAIN.waitMaxMs;
@@ -11685,7 +11690,9 @@ var init_resident = __esm({
11685
11690
  await ex.attach();
11686
11691
  return ex;
11687
11692
  }
11688
- /** One operator-scope GET /status, bounded by timeoutMs. Never throws. */
11693
+ /** One operator-scope GET /status, bounded by timeoutMs. Never throws. A
11694
+ * deadline miss is flagged `timedOut` beside `transport`, so the factory
11695
+ * falls cold for the one dispatch without arming its breaker. */
11689
11696
  static async probeStatus(baseUrl, token2, resource, timeoutMs, span, signal2) {
11690
11697
  const url = `${baseUrl.replace(/\/$/, "")}/status?resource=${encodeURIComponent(resource)}`;
11691
11698
  let res;
@@ -11697,7 +11704,12 @@ var init_resident = __esm({
11697
11704
  { route: "/status" }
11698
11705
  );
11699
11706
  } catch (err2) {
11700
- return { kind: "unreachable", error: err2 instanceof Error ? err2.message : String(err2), transport: true };
11707
+ return {
11708
+ kind: "unreachable",
11709
+ error: err2 instanceof Error ? err2.message : String(err2),
11710
+ transport: true,
11711
+ ...isDeadlineMiss(err2) ? { timedOut: true } : {}
11712
+ };
11701
11713
  }
11702
11714
  if (res.status === 404) {
11703
11715
  return { kind: "status", state: "not-onboarded", reason: "" };
@@ -12825,13 +12837,10 @@ function residentSlugsLister(cfg, secrets = processSecrets) {
12825
12837
  try {
12826
12838
  res = await fetch(`${cfg.baseUrl.replace(/\/$/, "")}/residents`, {
12827
12839
  headers: { authorization: `Bearer ${token2.reveal()}` },
12828
- signal: AbortSignal.timeout(cfg.probeTimeoutMs ?? 2e3)
12840
+ signal: execDeadline(cfg.probeTimeoutMs ?? PROBE_TIMEOUT_MS2)
12829
12841
  });
12830
12842
  } catch (err2) {
12831
- probeOutage = {
12832
- until: systemClock() + PROBE_OUTAGE_WINDOW_MS,
12833
- error: err2 instanceof Error ? err2.message : String(err2)
12834
- };
12843
+ if (!isDeadlineMiss(err2)) armOutage(err2 instanceof Error ? err2.message : String(err2));
12835
12844
  return void 0;
12836
12845
  }
12837
12846
  if (!res.ok) return void 0;
@@ -12856,11 +12865,11 @@ async function probeResident(cfg, token2, resource, span, stop) {
12856
12865
  cfg.baseUrl,
12857
12866
  token2.reveal(),
12858
12867
  resource,
12859
- cfg.probeTimeoutMs ?? 2e3,
12868
+ cfg.probeTimeoutMs ?? PROBE_TIMEOUT_MS2,
12860
12869
  span,
12861
12870
  stop
12862
12871
  );
12863
- if (probe.kind === "unreachable" && probe.transport && !stop?.aborted) armOutage(probe.error);
12872
+ if (probe.kind === "unreachable" && probe.transport && !probe.timedOut && !stop?.aborted) armOutage(probe.error);
12864
12873
  return probe;
12865
12874
  }
12866
12875
  function armOutage(error) {
@@ -12968,7 +12977,7 @@ function perThreadBackend(opts) {
12968
12977
  const type = opts.execution?.type ?? "local";
12969
12978
  return type === "e2b" ? "e2b" : type === "cloudflare" ? "sandbox" : "local";
12970
12979
  }
12971
- var BACKENDS, WorkspaceReattachRefusedError, WorkspaceReattachLeaseSpentError, PROBE_OUTAGE_WINDOW_MS, AWAIT_RESTORE_TIMEOUT_MS, probeOutage, NullExecutor;
12980
+ var BACKENDS, WorkspaceReattachRefusedError, WorkspaceReattachLeaseSpentError, PROBE_OUTAGE_WINDOW_MS, PROBE_TIMEOUT_MS2, AWAIT_RESTORE_TIMEOUT_MS, probeOutage, NullExecutor;
12972
12981
  var init_factory = __esm({
12973
12982
  "../../src/execution/factory.ts"() {
12974
12983
  "use strict";
@@ -13011,6 +13020,7 @@ var init_factory = __esm({
13011
13020
  note;
13012
13021
  };
13013
13022
  PROBE_OUTAGE_WINDOW_MS = 3e4;
13023
+ PROBE_TIMEOUT_MS2 = 8e3;
13014
13024
  AWAIT_RESTORE_TIMEOUT_MS = 10 * 6e4;
13015
13025
  NullExecutor = class {
13016
13026
  constructor(agentName) {
@@ -60469,10 +60479,12 @@ async function merge(body, deps, subject2) {
60469
60479
  return refused7(
60470
60480
  `the head of ${where} moved: \`${facts2.headSha?.slice(0, 7) ?? "?"}\` is not the approved \`${headSha.slice(0, 7)}\``
60471
60481
  );
60472
- if (facts2.mergeableState === "dirty")
60482
+ if (facts2.mergeableState === "dirty") {
60483
+ const base = facts2.baseRef ?? "its base";
60473
60484
  return refused7(
60474
- `${where} conflicts with \`${facts2.baseRef ?? "its base"}\` at \`${headSha.slice(0, 7)}\`, rebase and re-issue`
60485
+ `${where} conflicts with \`${base}\` at \`${headSha.slice(0, 7)}\` \u2014 rebase onto \`${base}\`, push, and merge it by hand once the checks are green; the approved work stands`
60475
60486
  );
60487
+ }
60476
60488
  const approved = await reviewPostedAt(deps, pr, "approve", headSha);
60477
60489
  if (approved === void 0)
60478
60490
  return json2(502, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.249.0",
3
+ "version": "1.249.1",
4
4
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://openswitchboard.dev",