@staff0rd/assist 0.775.6 → 0.777.0

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.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.775.6",
9
+ version: "0.777.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -1936,6 +1936,7 @@ var daemonPaths = {
1936
1936
  socket: process.platform === "win32" ? perUserWindowsPipe() : join9(DAEMON_DIR, "daemon.sock"),
1937
1937
  log: join9(DAEMON_DIR, "daemon.log"),
1938
1938
  pid: join9(DAEMON_DIR, "daemon.pid"),
1939
+ autoUpdatePaused: join9(DAEMON_DIR, "auto-update-paused"),
1939
1940
  spawnLock: join9(DAEMON_DIR, "spawn.lock"),
1940
1941
  hooksSettings: join9(DAEMON_DIR, "hooks-settings.json")
1941
1942
  };
@@ -11055,7 +11056,7 @@ var defaultDeps = {
11055
11056
  httpsPort: tailnetHttpsPort
11056
11057
  };
11057
11058
  async function ensureTailscaleServe(port, deps2 = {}) {
11058
- const { enabled, status: status2, serveStatus, serve, sleep: sleep3, httpsPort } = {
11059
+ const { enabled, status: status2, serveStatus, serve, sleep: sleep2, httpsPort } = {
11059
11060
  ...defaultDeps,
11060
11061
  ...deps2
11061
11062
  };
@@ -11070,7 +11071,7 @@ async function ensureTailscaleServe(port, deps2 = {}) {
11070
11071
  return `serving ${plan2.target} at ${plan2.url}`;
11071
11072
  } catch (error) {
11072
11073
  if (attempt >= SERVE_WRITE_ATTEMPTS) throw error;
11073
- await sleep3(attempt * 500 + Math.random() * 500);
11074
+ await sleep2(attempt * 500 + Math.random() * 500);
11074
11075
  }
11075
11076
  }
11076
11077
  }
@@ -13009,7 +13010,7 @@ var nodesConfigHelp = [
13009
13010
  {
13010
13011
  key: "sessions.linkVersionCheck",
13011
13012
  setter: "assist config set sessions.linkVersionCheck block -g",
13012
- note: "reaction to a protocol mismatch with a linked node (app versions may differ): block (default) heals an older peer via its /api/self-update and latches if the gap remains, warn proceeds, off skips the check"
13013
+ note: "reaction when a linked node's protocol range doesn't overlap this node's (app versions may differ; never restarts a peer): block (default) holds the link version-blocked until the older node is updated, warn proceeds, off skips the check"
13013
13014
  }
13014
13015
  ];
13015
13016
 
@@ -13592,11 +13593,13 @@ function harnessCapabilities(_req, res) {
13592
13593
  // src/commands/sessions/daemon/buildHello.ts
13593
13594
  var ASSIST_VERSION = package_default.version;
13594
13595
  var PROTOCOL_VERSION = 2;
13596
+ var MIN_PROTOCOL_VERSION = 1;
13595
13597
  function buildHello(extra = {}) {
13596
13598
  return {
13597
13599
  type: "hello",
13598
13600
  version: ASSIST_VERSION,
13599
13601
  protocol: PROTOCOL_VERSION,
13602
+ minProtocol: MIN_PROTOCOL_VERSION,
13600
13603
  nodeName: resolveNodeName(),
13601
13604
  ...extra
13602
13605
  };
@@ -13604,15 +13607,26 @@ function buildHello(extra = {}) {
13604
13607
  function isHello(msg) {
13605
13608
  return msg.type === "hello" && typeof msg.version === "string" && (msg.protocol === void 0 || typeof msg.protocol === "number");
13606
13609
  }
13607
- function helloCompatible(msg) {
13608
- if (typeof msg.protocol === "number") return !protocolMismatched(msg);
13609
- return msg.version === ASSIST_VERSION;
13610
+ function peerProtocolRange(msg) {
13611
+ if (typeof msg.protocol !== "number") return void 0;
13612
+ const min = typeof msg.minProtocol === "number" ? Math.min(msg.minProtocol, msg.protocol) : msg.protocol;
13613
+ return { min, max: msg.protocol };
13610
13614
  }
13611
- function helloMismatchKind(msg) {
13612
- return protocolMismatched(msg) ? "protocol" : "version";
13615
+ function negotiateProtocol(msg) {
13616
+ const peer = peerProtocolRange(msg);
13617
+ if (!peer) return void 0;
13618
+ const common = Math.min(peer.max, PROTOCOL_VERSION);
13619
+ return common >= Math.max(peer.min, MIN_PROTOCOL_VERSION) ? common : void 0;
13613
13620
  }
13614
- function protocolMismatched(msg) {
13615
- return typeof msg.protocol === "number" && msg.protocol !== PROTOCOL_VERSION;
13621
+ function formatRange(range) {
13622
+ if (!range) return "legacy (no protocol)";
13623
+ return range.min === range.max ? `protocol ${range.max}` : `protocols ${range.min}\u2013${range.max}`;
13624
+ }
13625
+ function describeProtocolGap(node, msg) {
13626
+ const peer = peerProtocolRange(msg);
13627
+ const local = { min: MIN_PROTOCOL_VERSION, max: PROTOCOL_VERSION };
13628
+ const behind = peer && peer.min > local.max ? "this node" : node;
13629
+ return `no common protocol: ${node} ${msg.version} speaks ${formatRange(peer)}, this node ${ASSIST_VERSION} speaks ${formatRange(local)}; update ${behind} and restart it when convenient \u2014 the link reconnects once the ranges overlap`;
13616
13630
  }
13617
13631
 
13618
13632
  // src/commands/sessions/daemon/requestDaemonReply.ts
@@ -16194,19 +16208,17 @@ async function helloHop(spec, probes) {
16194
16208
  `malformed hello: ${JSON.stringify(msg)}`,
16195
16209
  `run \`assist update\` on ${spec.name}`
16196
16210
  );
16197
- const detail = `hello ${msg.version} (protocol ${msg.protocol ?? "legacy"})`;
16198
- if (helloCompatible(msg)) return passed("ws", detail);
16199
- const mismatch = `${helloMismatchKind(msg)} mismatch: peer ${msg.version} (protocol ${msg.protocol ?? "legacy"}), this node ${ASSIST_VERSION} (protocol ${PROTOCOL_VERSION})`;
16211
+ const protocol = negotiateProtocol(msg);
16212
+ if (protocol !== void 0)
16213
+ return passed("ws", `hello ${msg.version} (protocol ${protocol})`);
16214
+ const gap = describeProtocolGap(spec.name, msg);
16200
16215
  const mode = linkVersionCheck();
16201
16216
  if (mode !== "block")
16202
- return passed(
16203
- "ws",
16204
- `${mismatch}; allowed by sessions.linkVersionCheck=${mode}`
16205
- );
16217
+ return passed("ws", `${gap}; allowed by sessions.linkVersionCheck=${mode}`);
16206
16218
  return failed(
16207
16219
  "ws",
16208
- mismatch,
16209
- `this node heals an older peer via its /api/self-update on connect; if the heal latched, run \`assist update\` on the older node, then \`assist daemon restart\` here`
16220
+ gap,
16221
+ `run \`assist update\` on the older node, then restart its daemon (\`assist daemon restart\`) when convenient`
16210
16222
  );
16211
16223
  }
16212
16224
 
@@ -16228,7 +16240,11 @@ function linkStateHop(spec, probes) {
16228
16240
  if (status2.state === "connected") return passed("link", "connected");
16229
16241
  const error = status2.error ?? status2.state;
16230
16242
  if (status2.state === "version-blocked")
16231
- return failed("link", error, `version mismatch \u2014 heal latched. ${error}`);
16243
+ return failed(
16244
+ "link",
16245
+ error,
16246
+ "no common protocol \u2014 update the older node and restart its daemon when convenient; the link retries every minute"
16247
+ );
16232
16248
  return failed(
16233
16249
  "link",
16234
16250
  error,
@@ -17206,6 +17222,33 @@ async function updates(_req, res) {
17206
17222
  respondJson(res, 200, status2);
17207
17223
  }
17208
17224
 
17225
+ // src/commands/sessions/shared/AutoUpdateAction.ts
17226
+ var AUTO_UPDATE_ACTIONS = ["pause", "resume", "check"];
17227
+
17228
+ // src/commands/sessions/web/updates/updatesControl.ts
17229
+ async function updatesControl(req, res) {
17230
+ const url = new URL(req.url ?? "/", "http://localhost");
17231
+ const action = url.searchParams.get("action");
17232
+ if (!AUTO_UPDATE_ACTIONS.includes(action)) {
17233
+ respondJson(res, 400, { error: "Invalid action" });
17234
+ return;
17235
+ }
17236
+ console.log(`auto-update ${action} ${linkedRequester(req)}`);
17237
+ const reply2 = await requestDaemonReply(
17238
+ { type: "auto-update-control", action },
17239
+ "auto-update-control"
17240
+ );
17241
+ if (!reply2) {
17242
+ respondJson(res, 503, { error: "Daemon not reachable" });
17243
+ return;
17244
+ }
17245
+ if (!reply2.ok) {
17246
+ respondJson(res, 409, { error: reply2.error ?? `${action} failed` });
17247
+ return;
17248
+ }
17249
+ respondJson(res, 200, { ok: true });
17250
+ }
17251
+
17209
17252
  // src/commands/sessions/web/readRequestBuffer.ts
17210
17253
  async function readRequestBuffer(req, limit) {
17211
17254
  const chunks = [];
@@ -17479,7 +17522,8 @@ var routes2 = {
17479
17522
  "POST /api/config/set": setConfig,
17480
17523
  "POST /api/config/unset": unsetConfig,
17481
17524
  "GET /api/review/synthesis": getReviewSynthesis,
17482
- "GET /api/updates": updates
17525
+ "GET /api/updates": updates,
17526
+ "POST /api/updates/control": updatesControl
17483
17527
  };
17484
17528
  var handleRequest = createFallbackHandler(
17485
17529
  routes2,
@@ -39995,15 +40039,20 @@ function gitFailureReason(error) {
39995
40039
  return message3.trim() || "git failed without reporting a reason";
39996
40040
  }
39997
40041
 
39998
- // src/commands/watch/simulatedDivergencePath.ts
40042
+ // src/commands/watch/gitMarkerPath.ts
39999
40043
  import { resolve as resolve25 } from "path";
40000
- function simulatedDivergencePath(cwd) {
40044
+ function gitMarkerPath(name, cwd) {
40001
40045
  return resolve25(
40002
40046
  cwd ?? process.cwd(),
40003
- runGit3(["rev-parse", "--git-path", "assist-simulate-divergence"], cwd)
40047
+ runGit3(["rev-parse", "--git-path", name], cwd)
40004
40048
  );
40005
40049
  }
40006
40050
 
40051
+ // src/commands/watch/simulatedDivergencePath.ts
40052
+ function simulatedDivergencePath(cwd) {
40053
+ return gitMarkerPath("assist-simulate-divergence", cwd);
40054
+ }
40055
+
40007
40056
  // src/commands/watch/simulateDivergence.ts
40008
40057
  function simulateDivergence() {
40009
40058
  try {
@@ -40511,11 +40560,10 @@ ${buildWatchReport(from)}`);
40511
40560
  process.exit(0);
40512
40561
  }
40513
40562
 
40514
- // src/commands/watch/consumeSimulatedDivergence.ts
40563
+ // src/commands/watch/consumeMarker.ts
40515
40564
  import { existsSync as existsSync81, rmSync as rmSync8 } from "fs";
40516
- function consumeSimulatedDivergence(cwd) {
40565
+ function consumeMarker(path106) {
40517
40566
  try {
40518
- const path106 = simulatedDivergencePath(cwd);
40519
40567
  if (!existsSync81(path106)) return false;
40520
40568
  rmSync8(path106);
40521
40569
  return true;
@@ -40524,6 +40572,15 @@ function consumeSimulatedDivergence(cwd) {
40524
40572
  }
40525
40573
  }
40526
40574
 
40575
+ // src/commands/watch/consumeSimulatedDivergence.ts
40576
+ function consumeSimulatedDivergence(cwd) {
40577
+ try {
40578
+ return consumeMarker(simulatedDivergencePath(cwd));
40579
+ } catch {
40580
+ return false;
40581
+ }
40582
+ }
40583
+
40527
40584
  // src/commands/watch/fetchQuietly.ts
40528
40585
  import { execFileSync as execFileSync21 } from "child_process";
40529
40586
  var MIN_FETCH_TIMEOUT_MS = 6e4;
@@ -40558,26 +40615,33 @@ function readMovement(cwd) {
40558
40615
  }
40559
40616
 
40560
40617
  // src/commands/watch/pollForMovement.ts
40618
+ var CONTROL_POLL_MS = 1e3;
40561
40619
  function pollForMovement(options2) {
40562
- const { upstream, intervalMs, timeoutMs, timeout, cwd } = options2;
40620
+ const { upstream, intervalMs, timeoutMs, timeout, cwd, control } = options2;
40563
40621
  return new Promise((resolve28) => {
40564
40622
  let settled = false;
40565
40623
  const finish2 = (outcome) => {
40566
40624
  if (settled) return;
40567
40625
  settled = true;
40568
40626
  clearInterval(ticker);
40627
+ clearInterval(controller);
40569
40628
  clearTimeout(deadline);
40570
40629
  process.off("SIGINT", onInterrupt);
40571
40630
  resolve28(outcome);
40572
40631
  };
40573
40632
  const onInterrupt = () => finish2({ kind: "interrupted" });
40574
- const ticker = setInterval(() => {
40633
+ const check2 = () => {
40575
40634
  if (consumeSimulatedDivergence(cwd))
40576
40635
  return finish2({ kind: "simulated-divergence", upstream });
40577
40636
  fetchQuietly(cwd, intervalMs);
40578
40637
  const found = readMovement(cwd);
40579
40638
  if (found) finish2({ kind: "moved", upstream, ...found });
40580
- }, intervalMs);
40639
+ };
40640
+ const ticker = setInterval(check2, intervalMs);
40641
+ const controller = control && setInterval(() => {
40642
+ if (consumeMarker(control.stop)) return finish2({ kind: "interrupted" });
40643
+ if (consumeMarker(control.check)) check2();
40644
+ }, CONTROL_POLL_MS);
40581
40645
  const deadline = timeoutMs === void 0 ? void 0 : setTimeout(
40582
40646
  () => finish2({ kind: "timeout", upstream, timeout }),
40583
40647
  timeoutMs
@@ -40586,6 +40650,18 @@ function pollForMovement(options2) {
40586
40650
  });
40587
40651
  }
40588
40652
 
40653
+ // src/commands/watch/watchControlPaths.ts
40654
+ function watchControlPaths(cwd) {
40655
+ try {
40656
+ return {
40657
+ check: gitMarkerPath("assist-watch-check", cwd),
40658
+ stop: gitMarkerPath("assist-watch-stop", cwd)
40659
+ };
40660
+ } catch {
40661
+ return void 0;
40662
+ }
40663
+ }
40664
+
40589
40665
  // src/commands/watch/waitForUpstream.ts
40590
40666
  function waitForUpstream(options2) {
40591
40667
  const { intervalMs, timeoutMs, timeout, cwd, onStart } = options2;
@@ -40601,10 +40677,20 @@ function waitForUpstream(options2) {
40601
40677
  onStart?.(upstream);
40602
40678
  if (consumeSimulatedDivergence(cwd))
40603
40679
  return Promise.resolve({ kind: "simulated-divergence", upstream });
40680
+ const control = watchControlPaths(cwd);
40604
40681
  fetchQuietly(cwd, intervalMs);
40682
+ if (control && consumeMarker(control.stop))
40683
+ return Promise.resolve({ kind: "interrupted" });
40605
40684
  const moved = readMovement(cwd);
40606
40685
  if (moved) return Promise.resolve({ kind: "moved", upstream, ...moved });
40607
- return pollForMovement({ upstream, intervalMs, timeoutMs, timeout, cwd });
40686
+ return pollForMovement({
40687
+ upstream,
40688
+ intervalMs,
40689
+ timeoutMs,
40690
+ timeout,
40691
+ cwd,
40692
+ control
40693
+ });
40608
40694
  }
40609
40695
 
40610
40696
  // src/commands/watch/watchWait.ts
@@ -42121,6 +42207,39 @@ function createRunSession(id, { runName, runArgs, cwd, meta }) {
42121
42207
  };
42122
42208
  }
42123
42209
 
42210
+ // src/commands/sessions/daemon/autoUpdate/autoUpdateControl.ts
42211
+ import { existsSync as existsSync86, rmSync as rmSync9, writeFileSync as writeFileSync54 } from "fs";
42212
+ var paused;
42213
+ var wake;
42214
+ function persist2(value) {
42215
+ try {
42216
+ if (value) writeFileSync54(daemonPaths.autoUpdatePaused, "");
42217
+ else rmSync9(daemonPaths.autoUpdatePaused, { force: true });
42218
+ } catch {
42219
+ }
42220
+ }
42221
+ var autoUpdateControl = {
42222
+ paused: () => {
42223
+ paused ??= existsSync86(daemonPaths.autoUpdatePaused);
42224
+ return paused;
42225
+ },
42226
+ setPaused: (value) => {
42227
+ paused = value;
42228
+ persist2(value);
42229
+ wake?.();
42230
+ },
42231
+ sleep: (ms) => new Promise((resolve28) => {
42232
+ const done2 = () => {
42233
+ clearTimeout(timer);
42234
+ if (wake === done2) wake = void 0;
42235
+ resolve28();
42236
+ };
42237
+ const timer = setTimeout(done2, ms);
42238
+ wake = done2;
42239
+ }),
42240
+ wake: () => wake?.()
42241
+ };
42242
+
42124
42243
  // src/commands/sessions/daemon/autoUpdate/autoUpdateEnabled.ts
42125
42244
  function autoUpdateEnabled(installDir) {
42126
42245
  try {
@@ -42151,6 +42270,14 @@ var autoUpdateState = {
42151
42270
  }
42152
42271
  };
42153
42272
 
42273
+ // src/commands/sessions/daemon/autoUpdate/backOff.ts
42274
+ var RETRY_AFTER_FAILURE_MS = 5 * 60 * 1e3;
42275
+ async function backOff(reason4, deps2) {
42276
+ deps2.enter("retrying", { reason: reason4 });
42277
+ deps2.note(`${reason4}; retrying in ${RETRY_AFTER_FAILURE_MS / 6e4}m`);
42278
+ await deps2.sleep(RETRY_AFTER_FAILURE_MS);
42279
+ }
42280
+
42154
42281
  // src/commands/sessions/daemon/autoUpdate/holdForEscalation.ts
42155
42282
  var ESCALATION_POLL_MS = 5e3;
42156
42283
  async function holdForEscalation(sessionId, deps2) {
@@ -42171,50 +42298,47 @@ async function runLapCapturingTail(deps2) {
42171
42298
  return { end, tail };
42172
42299
  }
42173
42300
 
42174
- // src/commands/sessions/daemon/autoUpdate/runAutoUpdateLoop.ts
42301
+ // src/commands/sessions/daemon/autoUpdate/settleLap.ts
42175
42302
  var DIVERGED_EXIT_CODE = 3;
42176
- var RETRY_AFTER_FAILURE_MS = 5 * 60 * 1e3;
42303
+ async function settleLap(lap, { end, tail }, deps2) {
42304
+ const how = end.signal ? `killed by ${end.signal}` : `exited ${end.code}`;
42305
+ if (end.code === DIVERGED_EXIT_CODE) {
42306
+ const held = deps2.escalate(tail);
42307
+ deps2.note(`lap ${lap} ${how} (divergence); escalated to session ${held}`);
42308
+ return held;
42309
+ }
42310
+ if (deps2.paused()) deps2.note(`lap ${lap} ${how}; paused`);
42311
+ else if (decideLap(end, false).kind === "relaunch")
42312
+ deps2.note(`lap ${lap} ${how}; relaunching`);
42313
+ else await backOff(`lap ${lap} ${how}`, deps2);
42314
+ return void 0;
42315
+ }
42316
+
42317
+ // src/commands/sessions/daemon/autoUpdate/runAutoUpdateLoop.ts
42318
+ var PAUSE_POLL_MS = 60 * 1e3;
42177
42319
  async function runAutoUpdateLoop(deps2, maxLaps = Number.POSITIVE_INFINITY) {
42178
42320
  let held = deps2.liveEscalation();
42179
42321
  for (let lap = 1; lap <= maxLaps; lap++) {
42180
42322
  if (held) await holdForEscalation(held, deps2);
42181
42323
  held = void 0;
42324
+ while (deps2.paused()) await deps2.sleep(PAUSE_POLL_MS);
42182
42325
  deps2.enter("waiting");
42183
42326
  deps2.note(
42184
42327
  `lap ${lap} at ${(/* @__PURE__ */ new Date()).toLocaleString()}: assist ${WATCH_LAP_ARGS.join(" ")}`
42185
42328
  );
42186
- let result;
42187
42329
  try {
42188
- result = await runLapCapturingTail(deps2);
42330
+ held = await settleLap(lap, await runLapCapturingTail(deps2), deps2);
42189
42331
  } catch (error) {
42190
42332
  await backOff(
42191
42333
  `lap ${lap} could not start (${error instanceof Error ? error.message : String(error)})`,
42192
42334
  deps2
42193
42335
  );
42194
- continue;
42195
- }
42196
- const { end, tail } = result;
42197
- const how = end.signal ? `killed by ${end.signal}` : `exited ${end.code}`;
42198
- if (end.code === DIVERGED_EXIT_CODE) {
42199
- held = deps2.escalate(tail);
42200
- deps2.note(`lap ${lap} ${how} (divergence); escalated to session ${held}`);
42201
- continue;
42202
- }
42203
- if (decideLap(end, false).kind === "relaunch") {
42204
- deps2.note(`lap ${lap} ${how}; relaunching`);
42205
- continue;
42206
42336
  }
42207
- await backOff(`lap ${lap} ${how}`, deps2);
42208
42337
  }
42209
42338
  }
42210
- async function backOff(reason4, deps2) {
42211
- deps2.enter("retrying", { reason: reason4 });
42212
- deps2.note(`${reason4}; retrying in ${RETRY_AFTER_FAILURE_MS / 6e4}m`);
42213
- await deps2.sleep(RETRY_AFTER_FAILURE_MS);
42214
- }
42215
42339
 
42216
42340
  // src/commands/sessions/daemon/autoUpdate/trimWatcherLog.ts
42217
- import { readFileSync as readFileSync65, writeFileSync as writeFileSync54 } from "fs";
42341
+ import { readFileSync as readFileSync65, writeFileSync as writeFileSync55 } from "fs";
42218
42342
  var KEEP_TAIL = 256 * 1024;
42219
42343
  var MAX_LOG_SIZE = 4 * 1024 * 1024;
42220
42344
  function trimWatcherLog(cwd) {
@@ -42226,20 +42350,10 @@ function trimWatcherLog(cwd) {
42226
42350
  return;
42227
42351
  }
42228
42352
  if (log2.length <= MAX_LOG_SIZE) return;
42229
- writeFileSync54(path106, log2.slice(-KEEP_TAIL));
42353
+ writeFileSync55(path106, log2.slice(-KEEP_TAIL));
42230
42354
  daemonLog(`auto-update: log ${path106} trimmed to its last ${KEEP_TAIL} chars`);
42231
42355
  }
42232
42356
 
42233
- // src/commands/sessions/daemon/autoUpdate/loopDeps.ts
42234
- import { setTimeout as sleep2 } from "timers/promises";
42235
-
42236
- // src/commands/sessions/daemon/watcherNote.ts
42237
- function watcherNote(text17) {
42238
- return `\r
42239
- \x1B[2m\u2500\u2500 ${text17} \u2500\u2500\x1B[0m\r
42240
- `;
42241
- }
42242
-
42243
42357
  // src/commands/sessions/daemon/worktree/appendWatcherLog.ts
42244
42358
  import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync34 } from "fs";
42245
42359
  import { dirname as dirname42 } from "path";
@@ -42409,9 +42523,30 @@ function lineRecorder(onLine) {
42409
42523
  };
42410
42524
  }
42411
42525
 
42526
+ // src/commands/sessions/daemon/watcherNote.ts
42527
+ function watcherNote(text17) {
42528
+ return `\r
42529
+ \x1B[2m\u2500\u2500 ${text17} \u2500\u2500\x1B[0m\r
42530
+ `;
42531
+ }
42532
+
42533
+ // src/commands/sessions/daemon/autoUpdate/noteAutoUpdate.ts
42534
+ function noteAutoUpdate(installDir, text17) {
42535
+ appendWatcherLog(installDir, watcherNote(text17));
42536
+ daemonLog(`auto-update: ${text17}`);
42537
+ }
42538
+
42412
42539
  // src/commands/sessions/daemon/autoUpdate/runUpdateLap.ts
42413
42540
  import { spawn as spawn11 } from "child_process";
42541
+ import { rmSync as rmSync10 } from "fs";
42542
+ function clearLapSignals(cwd) {
42543
+ const control = watchControlPaths(cwd);
42544
+ if (!control) return;
42545
+ for (const path106 of [control.check, control.stop])
42546
+ rmSync10(path106, { force: true });
42547
+ }
42414
42548
  function runUpdateLap(cwd, onOutput) {
42549
+ clearLapSignals(cwd);
42415
42550
  return new Promise((resolve28, reject) => {
42416
42551
  const { ASSIST_ACTIVITY_ID: _activity, ...env } = process.env;
42417
42552
  const child = spawn11(
@@ -42451,10 +42586,7 @@ function loopDeps(installDir, ctx) {
42451
42586
  appendWatcherLog(installDir, text17);
42452
42587
  logLine(text17);
42453
42588
  },
42454
- note: (text17) => {
42455
- appendWatcherLog(installDir, watcherNote(text17));
42456
- daemonLog(`auto-update: ${text17}`);
42457
- },
42589
+ note: (text17) => noteAutoUpdate(installDir, text17),
42458
42590
  enter: autoUpdateState.enter,
42459
42591
  liveEscalation: () => liveEscalationIn(ctx().sessions, clone)?.id,
42460
42592
  escalate: (output) => escalateDivergence(ctx(), installDir, output),
@@ -42462,7 +42594,8 @@ function loopDeps(installDir, ctx) {
42462
42594
  const status2 = ctx().sessions.get(sessionId)?.status;
42463
42595
  return status2 !== void 0 && status2 !== "done" && status2 !== "error";
42464
42596
  },
42465
- sleep: (ms) => sleep2(ms)
42597
+ paused: autoUpdateControl.paused,
42598
+ sleep: autoUpdateControl.sleep
42466
42599
  };
42467
42600
  }
42468
42601
 
@@ -42483,7 +42616,9 @@ function startAutoUpdate(ctx) {
42483
42616
  return;
42484
42617
  }
42485
42618
  trimWatcherLog(installDir);
42486
- daemonLog(`auto-update: pulling and building ${installDir} as origin moves`);
42619
+ daemonLog(
42620
+ autoUpdateControl.paused() ? `auto-update: paused for ${installDir}; resume it from the Updates page` : `auto-update: pulling and building ${installDir} as origin moves`
42621
+ );
42487
42622
  void runAutoUpdateLoop(loopDeps(installDir, ctx)).catch(
42488
42623
  (error) => daemonLog(
42489
42624
  `auto-update: loop stopped: ${error instanceof Error ? error.message : String(error)}`
@@ -42512,11 +42647,11 @@ function setStatus2(session, newStatus) {
42512
42647
  }
42513
42648
 
42514
42649
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
42515
- import { existsSync as existsSync87 } from "fs";
42650
+ import { existsSync as existsSync88 } from "fs";
42516
42651
  import { basename as basename30 } from "path";
42517
42652
 
42518
42653
  // src/commands/sessions/daemon/worktree/deleteStrandedTree.ts
42519
- import { existsSync as existsSync86 } from "fs";
42654
+ import { existsSync as existsSync87 } from "fs";
42520
42655
  import { join as join103 } from "path";
42521
42656
 
42522
42657
  // src/commands/sessions/daemon/worktree/deleteTreeDirectly.ts
@@ -42586,7 +42721,7 @@ async function deleteStrandedTree(clone, worktreePath, cause) {
42586
42721
  );
42587
42722
  }
42588
42723
  function strandedReason(worktreePath, cause) {
42589
- if (!existsSync86(join103(worktreePath, ".git")))
42724
+ if (!existsSync87(join103(worktreePath, ".git")))
42590
42725
  return "its .git link is already gone";
42591
42726
  if (/not a working tree|not a git repository/i.test(reason2(cause)))
42592
42727
  return "git no longer recognises it as a working tree";
@@ -42638,7 +42773,7 @@ function reason3(error) {
42638
42773
 
42639
42774
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
42640
42775
  async function reapWorktree(worktreePath, force = false) {
42641
- if (!existsSync87(worktreePath)) {
42776
+ if (!existsSync88(worktreePath)) {
42642
42777
  forgetWorktree(worktreePath);
42643
42778
  daemonLog(
42644
42779
  `worktree ${worktreePath} already gone; its record was forgotten`
@@ -42663,7 +42798,7 @@ async function reapWorktree(worktreePath, force = false) {
42663
42798
  }
42664
42799
  function owningClone(worktreePath) {
42665
42800
  const recorded = worktreeAttributionIncludingReaped(worktreePath)?.clone;
42666
- if (recorded && existsSync87(recorded)) return recorded;
42801
+ if (recorded && existsSync88(recorded)) return recorded;
42667
42802
  const detected = mainWorktree(worktreePath);
42668
42803
  if (detected) return detected;
42669
42804
  daemonLog(
@@ -42866,12 +43001,12 @@ async function closeBlockReason(sessionId, tree) {
42866
43001
  }
42867
43002
 
42868
43003
  // src/commands/sessions/daemon/worktree/watchGitState.ts
42869
- import { existsSync as existsSync88, watch } from "fs";
43004
+ import { existsSync as existsSync89, watch } from "fs";
42870
43005
  var DEBOUNCE_MS = 500;
42871
43006
  var POLL_MS2 = 3e4;
42872
43007
  function watchGitState(cwd, onChange) {
42873
43008
  const common = gitCommonDir(cwd);
42874
- if (!common || !existsSync88(common)) return void 0;
43009
+ if (!common || !existsSync89(common)) return void 0;
42875
43010
  const watchers = [
42876
43011
  watchGitDir(common, onChange),
42877
43012
  pollGitState(cwd, onChange)
@@ -43418,10 +43553,10 @@ function emitSessionOutput(session, clients, data) {
43418
43553
  }
43419
43554
 
43420
43555
  // src/commands/sessions/daemon/exitReason.ts
43421
- import { existsSync as existsSync89 } from "fs";
43556
+ import { existsSync as existsSync90 } from "fs";
43422
43557
  import { resolve as resolve27 } from "path";
43423
43558
  function exitDetail(session) {
43424
- if (session.cwd && !existsSync89(session.cwd))
43559
+ if (session.cwd && !existsSync90(session.cwd))
43425
43560
  return `working directory ${session.cwd} no longer exists`;
43426
43561
  return missingRunConfigCwd(session);
43427
43562
  }
@@ -43435,7 +43570,7 @@ function missingRunConfigCwd(session) {
43435
43570
  const config = resolveRunConfig(session.runName, dir);
43436
43571
  if (!config?.cwd) return void 0;
43437
43572
  const configured = resolve27(runConfigBaseDirFrom(dir), config.cwd);
43438
- if (existsSync89(configured)) return void 0;
43573
+ if (existsSync90(configured)) return void 0;
43439
43574
  return `run config "${config.name}": cwd ${configured} does not exist`;
43440
43575
  }
43441
43576
 
@@ -43483,7 +43618,7 @@ function handleRefusedRun(session, exitCode, onStatusChange) {
43483
43618
  }
43484
43619
 
43485
43620
  // src/commands/sessions/daemon/watchActivity.ts
43486
- import { existsSync as existsSync90, mkdirSync as mkdirSync35, watch as watch2 } from "fs";
43621
+ import { existsSync as existsSync91, mkdirSync as mkdirSync35, watch as watch2 } from "fs";
43487
43622
  import { dirname as dirname43 } from "path";
43488
43623
 
43489
43624
  // src/commands/sessions/daemon/applyActivityToSession.ts
@@ -43569,7 +43704,7 @@ function watchActivity(session, notify2, onClaudeSessionId) {
43569
43704
  if (timer) clearTimeout(timer);
43570
43705
  timer = setTimeout(read3, DEBOUNCE_MS2);
43571
43706
  });
43572
- if (existsSync90(path106)) read3();
43707
+ if (existsSync91(path106)) read3();
43573
43708
  }
43574
43709
  function refreshActivity(session) {
43575
43710
  if (session.commandType !== "assist" || !session.cwd) return;
@@ -45560,7 +45695,7 @@ function rearmStoppedSessions(sessions, notify2) {
45560
45695
  }
45561
45696
 
45562
45697
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
45563
- import { existsSync as existsSync93 } from "fs";
45698
+ import { existsSync as existsSync94 } from "fs";
45564
45699
  import { basename as basename32 } from "path";
45565
45700
 
45566
45701
  // src/commands/sessions/daemon/worktree/accountedTrees.ts
@@ -45581,9 +45716,9 @@ function accountedTrees(sessions) {
45581
45716
  }
45582
45717
 
45583
45718
  // src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
45584
- import { existsSync as existsSync92 } from "fs";
45719
+ import { existsSync as existsSync93 } from "fs";
45585
45720
  async function reclaimVanishedWorktrees(clone, paths) {
45586
- if (!existsSync92(clone)) {
45721
+ if (!existsSync93(clone)) {
45587
45722
  for (const { path: path106 } of paths) forgetWorktree(path106);
45588
45723
  daemonLog(
45589
45724
  `clone ${clone} is gone; forgot ${paths.length} worktree record(s) it owned`
@@ -45749,7 +45884,7 @@ async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
45749
45884
  );
45750
45885
  continue;
45751
45886
  }
45752
- if (!existsSync93(path106)) {
45887
+ if (!existsSync94(path106)) {
45753
45888
  logVanishedTree(sessions, path106);
45754
45889
  vanished.set(clone, [
45755
45890
  ...vanished.get(clone) ?? [],
@@ -46060,18 +46195,6 @@ function guardLinkCallback(label2, callback) {
46060
46195
  };
46061
46196
  }
46062
46197
 
46063
- // src/commands/sessions/daemon/links/selfUpdatePeer.ts
46064
- var SELF_UPDATE_TIMEOUT_MS = 10 * 6e4;
46065
- async function selfUpdatePeer(url) {
46066
- const response = await fetch(new URL("/api/self-update", url), {
46067
- method: "POST",
46068
- signal: AbortSignal.timeout(SELF_UPDATE_TIMEOUT_MS)
46069
- });
46070
- if (response.ok) return;
46071
- const body = await response.json().catch(() => ({}));
46072
- throw new Error(body.error ?? `self-update responded ${response.status}`);
46073
- }
46074
-
46075
46198
  // src/commands/sessions/daemon/links/linkDeps.ts
46076
46199
  function linkDeps(options2, callbacks) {
46077
46200
  return {
@@ -46089,7 +46212,6 @@ function linkDeps(options2, callbacks) {
46089
46212
  callbacks.onHistoryChanged
46090
46213
  ),
46091
46214
  transport: options2.transport ?? connectLinkWebSocket,
46092
- heal: options2.heal ?? selfUpdatePeer,
46093
46215
  reconnectMs: options2.reconnectMs ?? 3e3,
46094
46216
  blockedRetryMs: options2.blockedRetryMs ?? 6e4,
46095
46217
  createTimeoutMs: options2.createTimeoutMs ?? 15e3
@@ -46201,10 +46323,6 @@ function createLinkContext(spec, deps2) {
46201
46323
  state: "disconnected",
46202
46324
  disposed: false,
46203
46325
  connect: () => {
46204
- },
46205
- onMismatch: () => {
46206
- },
46207
- onCompatible: () => {
46208
46326
  }
46209
46327
  };
46210
46328
  }
@@ -46218,7 +46336,8 @@ function describeLink(ctx) {
46218
46336
  error: ctx.blockedMessage ?? ctx.lastError?.message,
46219
46337
  errorAt: ctx.lastError?.at,
46220
46338
  peerVersion: ctx.peer?.version,
46221
- peerProtocol: ctx.peer?.protocol
46339
+ peerProtocol: ctx.peer?.protocol,
46340
+ protocol: ctx.protocol
46222
46341
  };
46223
46342
  }
46224
46343
  function unavailableReason(ctx) {
@@ -46299,139 +46418,12 @@ function forwardToLink(ctx, client, data) {
46299
46418
  daemonLog(`link ${node} ws: forwarded ${data.type} for ${data.sessionId}`);
46300
46419
  }
46301
46420
 
46302
- // src/commands/sessions/daemon/links/linkHealRefusal.ts
46303
- import semver2 from "semver";
46304
- function peerIsNewer(peerVersion) {
46305
- return semver2.valid(peerVersion) !== null && semver2.valid(ASSIST_VERSION) !== null && semver2.gt(peerVersion, ASSIST_VERSION);
46306
- }
46307
- function linkHealRefusal(node, peerVersion, healAttempted) {
46308
- if (peerIsNewer(peerVersion))
46309
- return `This node (${ASSIST_VERSION}) is older than ${node} (${peerVersion}); update this node and restart its daemon.`;
46310
- if (healAttempted)
46311
- return `${node} is still on ${peerVersion} after auto-update (this node is ${ASSIST_VERSION}); update ${node} manually; this node reconnects once it reports a compatible version.`;
46312
- return void 0;
46313
- }
46314
-
46315
- // src/commands/sessions/daemon/links/scheduleReconnect.ts
46316
- function armReconnect(ctx, delay2) {
46317
- clearTimeout(ctx.reconnectTimer);
46318
- ctx.reconnectTimer = setTimeout(ctx.connect, delay2);
46319
- ctx.reconnectTimer.unref?.();
46320
- }
46321
- function scheduleReconnect2(ctx) {
46322
- if (ctx.disposed) return;
46323
- if (ctx.blockedMessage) return armReconnect(ctx, ctx.deps.blockedRetryMs);
46324
- const delay2 = ctx.breaker.tripped() ? ctx.breaker.remainingMs() : ctx.deps.reconnectMs;
46325
- armReconnect(ctx, delay2);
46326
- }
46327
- function reconnectNow(ctx) {
46328
- ctx.breaker.clear();
46329
- armReconnect(ctx, ctx.deps.reconnectMs);
46330
- }
46331
-
46332
- // src/commands/sessions/daemon/links/teardownLink.ts
46333
- function teardownLink(ctx) {
46334
- const hadSessions = ctx.relay.sessions.length > 0;
46335
- ctx.socket = null;
46336
- ctx.greeted = false;
46337
- resetRelayState(ctx.relay, `${ctx.spec.name} connection closed`);
46338
- if (!ctx.blockedMessage) setLinkState(ctx, "disconnected");
46339
- if (hadSessions) ctx.deps.onSessionsChanged();
46340
- }
46341
- function disconnectLink(ctx) {
46342
- const socket = ctx.socket;
46343
- teardownLink(ctx);
46344
- socket?.close();
46345
- }
46346
- function failLink(ctx, message3) {
46347
- daemonLog(`link ${ctx.spec.name} ws: ${message3}`);
46348
- ctx.lastError = { message: message3, at: (/* @__PURE__ */ new Date()).toISOString() };
46349
- ctx.breaker.fail(message3);
46350
- setLinkState(ctx, ctx.blockedMessage ? "version-blocked" : "disconnected");
46351
- scheduleReconnect2(ctx);
46352
- }
46353
- function blockLink(ctx, message3) {
46354
- const repeat = ctx.blockedMessage === message3;
46355
- ctx.blockedMessage = message3;
46356
- disconnectLink(ctx);
46357
- setLinkState(ctx, "version-blocked");
46358
- if (!repeat) broadcastToViewers(ctx.relay, { type: "error", message: message3 });
46359
- scheduleReconnect2(ctx);
46360
- }
46361
-
46362
- // src/commands/sessions/daemon/links/runLinkHeal.ts
46363
- async function runLinkHeal(ctx, peerVersion) {
46364
- const node = ctx.spec.name;
46365
- daemonLog(
46366
- `link ${node} heal: peer ${peerVersion} is older than ${ASSIST_VERSION}; requesting self-update`
46367
- );
46368
- failPendingCreators(
46369
- ctx.relay,
46370
- `${node} is out of date; updating it now \u2014 retry once the update finishes.`
46371
- );
46372
- disconnectLink(ctx);
46373
- try {
46374
- await ctx.deps.heal(ctx.spec.url);
46375
- daemonLog(`link ${node} heal: self-update done, reconnecting`);
46376
- } catch (error) {
46377
- const message3 = error instanceof Error ? error.message : String(error);
46378
- daemonLog(`link ${node} heal: self-update failed: ${message3}`);
46379
- broadcastToViewers(ctx.relay, {
46380
- type: "error",
46381
- message: `${node} auto-update failed: ${message3}`
46382
- });
46383
- }
46384
- reconnectNow(ctx);
46385
- }
46386
-
46387
- // src/commands/sessions/daemon/links/LinkHealer.ts
46388
- var LinkHealer = class {
46389
- constructor(ctx) {
46390
- this.ctx = ctx;
46391
- }
46392
- healAttempted = false;
46393
- healing = false;
46394
- awaitingConfirmation = false;
46395
- async onMismatch(peerVersion) {
46396
- if (this.healing) return;
46397
- const node = this.ctx.spec.name;
46398
- const refusal = linkHealRefusal(node, peerVersion, this.healAttempted);
46399
- if (refusal) {
46400
- this.awaitingConfirmation = false;
46401
- if (this.ctx.blockedMessage !== refusal)
46402
- daemonLog(`link ${node} heal: latched, retrying slowly: ${refusal}`);
46403
- return blockLink(this.ctx, refusal);
46404
- }
46405
- this.healing = true;
46406
- this.healAttempted = true;
46407
- this.awaitingConfirmation = true;
46408
- try {
46409
- await runLinkHeal(this.ctx, peerVersion);
46410
- } finally {
46411
- this.healing = false;
46412
- }
46413
- }
46414
- reset() {
46415
- this.healAttempted = false;
46416
- this.awaitingConfirmation = false;
46417
- }
46418
- onCompatible() {
46419
- if (!this.awaitingConfirmation) return;
46420
- this.awaitingConfirmation = false;
46421
- const node = this.ctx.spec.name;
46422
- daemonLog(`link ${node} heal: post-heal handshake compatible`);
46423
- broadcastToViewers(this.ctx.relay, {
46424
- type: "notice",
46425
- message: `${node} is up to date.`
46426
- });
46427
- }
46428
- };
46429
-
46430
46421
  // src/commands/sessions/daemon/links/acceptPeerHello.ts
46431
46422
  var helloFields = /* @__PURE__ */ new Set([
46432
46423
  "type",
46433
46424
  "version",
46434
46425
  "protocol",
46426
+ "minProtocol",
46435
46427
  "nodeName",
46436
46428
  "peer"
46437
46429
  ]);
@@ -46448,22 +46440,22 @@ function acceptPeerHello(linkName, msg) {
46448
46440
  reason: `peer reports nodeName ${msg.nodeName}, link expects ${linkName}`,
46449
46441
  peer
46450
46442
  };
46451
- if (helloCompatible(msg)) {
46443
+ const protocol = negotiateProtocol(msg);
46444
+ if (protocol !== void 0) {
46452
46445
  daemonLog(
46453
- `link ${linkName} ws: hello ok (${msg.version})${unrecognisedHelloFields(msg)}`
46446
+ `link ${linkName} ws: hello ok (${msg.version}, protocol ${protocol})${unrecognisedHelloFields(msg)}`
46454
46447
  );
46455
- return { kind: "accept", peer };
46448
+ return { kind: "accept", peer, protocol };
46456
46449
  }
46457
46450
  const mode = linkVersionCheck();
46458
- const detail = `${helloMismatchKind(msg)} mismatch: peer protocol ${msg.protocol ?? "legacy"} version ${msg.version} (this node protocol ${PROTOCOL_VERSION} version ${ASSIST_VERSION})`;
46451
+ const reason4 = describeProtocolGap(linkName, msg);
46459
46452
  if (mode !== "block") {
46460
46453
  daemonLog(
46461
- `link ${linkName} ws: ${detail}; proceeding (sessions.linkVersionCheck=${mode})`
46454
+ `link ${linkName} ws: ${reason4}; proceeding (sessions.linkVersionCheck=${mode})`
46462
46455
  );
46463
46456
  return { kind: "accept", peer };
46464
46457
  }
46465
- daemonLog(`link ${linkName} ws: ${detail}`);
46466
- return { kind: "heal", peer };
46458
+ return { kind: "block", reason: reason4, peer };
46467
46459
  }
46468
46460
 
46469
46461
  // src/commands/sessions/daemon/logUnrecognisedType.ts
@@ -46566,6 +46558,55 @@ function relayLinkMessage(state, msg) {
46566
46558
  );
46567
46559
  }
46568
46560
 
46561
+ // src/commands/sessions/daemon/links/scheduleReconnect.ts
46562
+ function armReconnect(ctx, delay2) {
46563
+ clearTimeout(ctx.reconnectTimer);
46564
+ ctx.reconnectTimer = setTimeout(ctx.connect, delay2);
46565
+ ctx.reconnectTimer.unref?.();
46566
+ }
46567
+ function scheduleReconnect2(ctx) {
46568
+ if (ctx.disposed) return;
46569
+ if (ctx.blockedMessage) return armReconnect(ctx, ctx.deps.blockedRetryMs);
46570
+ const delay2 = ctx.breaker.tripped() ? ctx.breaker.remainingMs() : ctx.deps.reconnectMs;
46571
+ armReconnect(ctx, delay2);
46572
+ }
46573
+ function reconnectNow(ctx) {
46574
+ ctx.breaker.clear();
46575
+ armReconnect(ctx, ctx.deps.reconnectMs);
46576
+ }
46577
+
46578
+ // src/commands/sessions/daemon/links/teardownLink.ts
46579
+ function teardownLink(ctx) {
46580
+ const hadSessions = ctx.relay.sessions.length > 0;
46581
+ ctx.socket = null;
46582
+ ctx.greeted = false;
46583
+ ctx.protocol = void 0;
46584
+ resetRelayState(ctx.relay, `${ctx.spec.name} connection closed`);
46585
+ if (!ctx.blockedMessage) setLinkState(ctx, "disconnected");
46586
+ if (hadSessions) ctx.deps.onSessionsChanged();
46587
+ }
46588
+ function disconnectLink(ctx) {
46589
+ const socket = ctx.socket;
46590
+ teardownLink(ctx);
46591
+ socket?.close();
46592
+ }
46593
+ function failLink(ctx, message3) {
46594
+ daemonLog(`link ${ctx.spec.name} ws: ${message3}`);
46595
+ ctx.lastError = { message: message3, at: (/* @__PURE__ */ new Date()).toISOString() };
46596
+ ctx.breaker.fail(message3);
46597
+ setLinkState(ctx, ctx.blockedMessage ? "version-blocked" : "disconnected");
46598
+ scheduleReconnect2(ctx);
46599
+ }
46600
+ function blockLink(ctx, message3) {
46601
+ const repeat = ctx.blockedMessage === message3;
46602
+ if (!repeat) daemonLog(`link ${ctx.spec.name} ws: ${message3}`);
46603
+ ctx.blockedMessage = message3;
46604
+ disconnectLink(ctx);
46605
+ setLinkState(ctx, "version-blocked");
46606
+ if (!repeat) broadcastToViewers(ctx.relay, { type: "error", message: message3 });
46607
+ scheduleReconnect2(ctx);
46608
+ }
46609
+
46569
46610
  // src/commands/sessions/daemon/links/onLinkLine.ts
46570
46611
  function onHello(ctx, msg) {
46571
46612
  const verdict = acceptPeerHello(ctx.spec.name, msg);
@@ -46577,18 +46618,18 @@ function onHello(ctx, msg) {
46577
46618
  disconnectLink(ctx);
46578
46619
  return failLink(ctx, verdict.reason);
46579
46620
  }
46580
- if (verdict.kind === "heal") return ctx.onMismatch(verdict.peer.version);
46621
+ if (verdict.kind === "block") return blockLink(ctx, verdict.reason);
46581
46622
  if (ctx.blockedMessage)
46582
46623
  daemonLog(
46583
- `link ${ctx.spec.name} heal: peer now reports ${verdict.peer.version}; latch cleared`
46624
+ `link ${ctx.spec.name} ws: peer now reports ${verdict.peer.version}; protocol block cleared`
46584
46625
  );
46585
46626
  ctx.blockedMessage = void 0;
46627
+ ctx.protocol = verdict.protocol;
46586
46628
  ctx.greeted = true;
46587
46629
  ctx.breaker.clear();
46588
46630
  ctx.lastError = void 0;
46589
46631
  setLinkState(ctx, "connected");
46590
46632
  ctx.deps.onHistoryChanged();
46591
- ctx.onCompatible();
46592
46633
  }
46593
46634
  function onLinkLine(ctx, line) {
46594
46635
  let msg;
@@ -46661,21 +46702,15 @@ var NodeLink = class {
46661
46702
  constructor(spec, deps2) {
46662
46703
  this.spec = spec;
46663
46704
  const ctx = createLinkContext(spec, deps2);
46664
- const healer = new LinkHealer(ctx);
46665
46705
  ctx.connect = () => void openLink(ctx);
46666
- ctx.onMismatch = (version2) => void healer.onMismatch(version2);
46667
- ctx.onCompatible = () => healer.onCompatible();
46668
46706
  this.ctx = ctx;
46669
- this.healer = healer;
46670
46707
  }
46671
46708
  ctx;
46672
- healer;
46673
46709
  start = () => this.ctx.connect();
46674
46710
  unlatch() {
46675
46711
  if (!this.ctx.blockedMessage) return;
46676
- daemonLog(`link ${this.spec.name} heal: latch cleared by reload`);
46712
+ daemonLog(`link ${this.spec.name} ws: protocol block cleared by reload`);
46677
46713
  this.ctx.blockedMessage = void 0;
46678
- this.healer.reset();
46679
46714
  setLinkState(this.ctx, "disconnected");
46680
46715
  reconnectNow(this.ctx);
46681
46716
  }
@@ -46894,7 +46929,7 @@ function spawnAssistInTree(ctx, assistArgs, cwd, meta, context) {
46894
46929
  }
46895
46930
 
46896
46931
  // src/commands/sessions/daemon/worktree/resumeInTree.ts
46897
- import { existsSync as existsSync96 } from "fs";
46932
+ import { existsSync as existsSync97 } from "fs";
46898
46933
 
46899
46934
  // src/commands/sessions/daemon/resumeSession.ts
46900
46935
  function resumeSession(id, sessionId, cwd, name, holdPty, harness) {
@@ -46925,10 +46960,10 @@ function resumeSession(id, sessionId, cwd, name, holdPty, harness) {
46925
46960
  }
46926
46961
 
46927
46962
  // src/commands/sessions/daemon/worktree/resumeInReplacementTree.ts
46928
- import { existsSync as existsSync95 } from "fs";
46963
+ import { existsSync as existsSync96 } from "fs";
46929
46964
 
46930
46965
  // src/commands/sessions/daemon/worktree/carryTranscriptToTree.ts
46931
- import { copyFileSync as copyFileSync5, existsSync as existsSync94, mkdirSync as mkdirSync37 } from "fs";
46966
+ import { copyFileSync as copyFileSync5, existsSync as existsSync95, mkdirSync as mkdirSync37 } from "fs";
46932
46967
  import { join as join105 } from "path";
46933
46968
  function carryTranscriptToTree(claudeSessionId, fromCwd, toCwd) {
46934
46969
  const dir = projectDirForCwd(toCwd);
@@ -46939,7 +46974,7 @@ function carryTranscriptToTree(claudeSessionId, fromCwd, toCwd) {
46939
46974
  return;
46940
46975
  }
46941
46976
  const dest = join105(dir, `${claudeSessionId}.jsonl`);
46942
- if (existsSync94(dest)) {
46977
+ if (existsSync95(dest)) {
46943
46978
  daemonLog(`transcript ${claudeSessionId} already present in ${dir}`);
46944
46979
  return;
46945
46980
  }
@@ -46990,7 +47025,7 @@ function resumeInReplacementTree(ctx, claudeSessionId, missingCwd, name, harness
46990
47025
  }
46991
47026
  function cloneForReapedTree(missingCwd) {
46992
47027
  const clone = worktreeAttributionIncludingReaped(missingCwd)?.clone;
46993
- if (!clone || !existsSync95(clone))
47028
+ if (!clone || !existsSync96(clone))
46994
47029
  throw new Error(
46995
47030
  `working directory no longer exists and no clone is recorded to re-allocate from: ${missingCwd}`
46996
47031
  );
@@ -46999,7 +47034,7 @@ function cloneForReapedTree(missingCwd) {
46999
47034
 
47000
47035
  // src/commands/sessions/daemon/worktree/resumeInTree.ts
47001
47036
  function resumeInTree(ctx, sessionId, cwd, name, harness) {
47002
- if (!existsSync96(cwd))
47037
+ if (!existsSync97(cwd))
47003
47038
  return resumeInReplacementTree(ctx, sessionId, cwd, name, harness);
47004
47039
  const id = ctx.spawnWith(
47005
47040
  (sid) => resumeSession(sid, sessionId, cwd, name, void 0, harness)
@@ -47192,10 +47227,60 @@ function autoUpdateReply() {
47192
47227
  type: "auto-update",
47193
47228
  version: ASSIST_VERSION,
47194
47229
  startCommit: autoUpdateState.startCommit(),
47195
- loop: autoUpdateState.loop()
47230
+ loop: { ...autoUpdateState.loop(), paused: autoUpdateControl.paused() }
47196
47231
  };
47197
47232
  }
47198
47233
 
47234
+ // src/commands/sessions/daemon/autoUpdate/applyAutoUpdateAction.ts
47235
+ import { rmSync as rmSync11, writeFileSync as writeFileSync56 } from "fs";
47236
+ function signalLap(installDir, marker2) {
47237
+ const control = watchControlPaths(installDir);
47238
+ if (control) writeFileSync56(control[marker2], "");
47239
+ }
47240
+ function applyAutoUpdateAction(action) {
47241
+ const installDir = getInstallDir();
47242
+ const loop2 = autoUpdateState.loop();
47243
+ if (action === "resume") {
47244
+ if (!autoUpdateControl.paused()) return void 0;
47245
+ const control = watchControlPaths(installDir);
47246
+ if (control) rmSync11(control.stop, { force: true });
47247
+ autoUpdateControl.setPaused(false);
47248
+ noteAutoUpdate(installDir, "resumed from Updates");
47249
+ return void 0;
47250
+ }
47251
+ if (loop2.phase === "off")
47252
+ return `auto-update is off (${loop2.reason ?? "not running"})`;
47253
+ if (action === "pause") {
47254
+ if (autoUpdateControl.paused()) return void 0;
47255
+ autoUpdateControl.setPaused(true);
47256
+ signalLap(installDir, "stop");
47257
+ noteAutoUpdate(installDir, "paused from Updates");
47258
+ return void 0;
47259
+ }
47260
+ if (autoUpdateControl.paused())
47261
+ return "auto-update is paused; resume it first";
47262
+ signalLap(installDir, "check");
47263
+ autoUpdateControl.wake();
47264
+ noteAutoUpdate(installDir, "check requested from Updates");
47265
+ return void 0;
47266
+ }
47267
+
47268
+ // src/commands/sessions/daemon/autoUpdate/handleAutoUpdateControl.ts
47269
+ function tryAction(action) {
47270
+ if (!AUTO_UPDATE_ACTIONS.includes(action))
47271
+ return `unknown auto-update action ${String(action)}`;
47272
+ try {
47273
+ return applyAutoUpdateAction(action);
47274
+ } catch (error) {
47275
+ return error instanceof Error ? error.message : String(error);
47276
+ }
47277
+ }
47278
+ var handleAutoUpdateControl = (client, _m, d) => {
47279
+ const error = tryAction(d.action);
47280
+ daemonLog(`auto-update-control ${String(d.action)}: ${error ?? "ok"}`);
47281
+ sendTo(client, { type: "auto-update-control", ok: !error, error });
47282
+ };
47283
+
47199
47284
  // src/commands/sessions/daemon/handleHelloRequest.ts
47200
47285
  var handleHelloRequest = (client, m, d) => {
47201
47286
  sendTo(client, buildHello());
@@ -47722,6 +47807,7 @@ var messageHandlers = {
47722
47807
  output: handleOutputRequest,
47723
47808
  nodes: (client, m) => m.links.sendNodes(client),
47724
47809
  "auto-update": (client) => sendTo(client, autoUpdateReply()),
47810
+ "auto-update-control": handleAutoUpdateControl,
47725
47811
  "reload-links": (client, m) => {
47726
47812
  daemonLog("reload-links received");
47727
47813
  m.links.reload();
@@ -47838,7 +47924,7 @@ function handleConnection(socket, manager) {
47838
47924
  }
47839
47925
 
47840
47926
  // src/commands/sessions/daemon/onListening.ts
47841
- import { unlinkSync as unlinkSync22, writeFileSync as writeFileSync55 } from "fs";
47927
+ import { unlinkSync as unlinkSync22, writeFileSync as writeFileSync57 } from "fs";
47842
47928
 
47843
47929
  // src/commands/sessions/daemon/startPidFileWatchdog.ts
47844
47930
  import { readFileSync as readFileSync66 } from "fs";
@@ -47860,7 +47946,7 @@ function ownsPidFile() {
47860
47946
 
47861
47947
  // src/commands/sessions/daemon/onListening.ts
47862
47948
  function onListening(manager, checkAutoExit) {
47863
- writeFileSync55(daemonPaths.pid, String(process.pid));
47949
+ writeFileSync57(daemonPaths.pid, String(process.pid));
47864
47950
  startPidFileWatchdog(() => {
47865
47951
  daemonLog("lost daemon.pid ownership; shutting down sessions and exiting");
47866
47952
  void manager.flushActiveMs().finally(() => {
@@ -48246,14 +48332,14 @@ async function linkStatus(name, query = queryNodes) {
48246
48332
  return (await query())?.links.find((l) => l.name === name);
48247
48333
  }
48248
48334
  async function awaitLinkReturn(name, options2, deps2 = {}) {
48249
- const { query = queryNodes, sleep: sleep3 = wait, now = Date.now } = deps2;
48335
+ const { query = queryNodes, sleep: sleep2 = wait, now = Date.now } = deps2;
48250
48336
  const deadline = now() + options2.timeoutMs;
48251
48337
  let sawDown = options2.sawDown;
48252
48338
  while (now() < deadline) {
48253
48339
  const link3 = await linkStatus(name, query);
48254
48340
  if (link3?.state !== "connected") sawDown = true;
48255
48341
  else if (sawDown) return link3;
48256
- await sleep3(POLL_MS4);
48342
+ await sleep2(POLL_MS4);
48257
48343
  }
48258
48344
  return void 0;
48259
48345
  }