relay-companion 0.1.575 → 0.1.577

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.
@@ -23,6 +23,32 @@ function inside(root, file) {
23
23
  const relative = path.relative(root, file);
24
24
  return relative && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
25
25
  }
26
+ // Relay's own recovery check runs every minute. Setup stops the existing
27
+ // services during activation; a check that notices starts repairing them and
28
+ // collides with setup, which then fails with "Rollback failed" (CI, 2026-10-01).
29
+ // Pause the check (recovery honours intent.json) and wait out a run already in
30
+ // progress. The returned function restores the previous state, or leaves
31
+ // recovery running when setup succeeded.
32
+ async function pauseRecoveryChecks({ homeDir, timeoutMs = 3 * 60_000, pollMs = 2000,
33
+ sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) } = {}) {
34
+ const intent = require("./recovery-intent.cjs");
35
+ const wasStopped = intent.stopped(homeDir);
36
+ intent.setStopped(true, homeDir);
37
+ const runLock = path.join(homeDir, ".relay", "recovery", "run.lock");
38
+ for (const deadline = Date.now() + timeoutMs; ;) {
39
+ try { bootstrap.acquireCanonicalLock(runLock).release(); break; }
40
+ catch (error) {
41
+ // Past the deadline setup proceeds, as it did before this pause existed.
42
+ if (!/already in progress/.test(error.message) || Date.now() >= deadline) break;
43
+ await sleep(pollMs);
44
+ }
45
+ }
46
+ return {
47
+ repause: () => intent.setStopped(true, homeDir),
48
+ resume: succeeded => intent.setStopped(succeeded ? false : wasStopped, homeDir),
49
+ };
50
+ }
51
+
26
52
  function versionCompare(a, b) {
27
53
  const aa = a.split(".").map(BigInt), bb = b.split(".").map(BigInt);
28
54
  for (let i = 0; i < 3; i++) if (aa[i] !== bb[i]) return aa[i] > bb[i] ? 1 : -1;
@@ -80,7 +106,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
80
106
  allowRecovery = true,
81
107
  homeDir = os.homedir(), verify = verifyBundle, extract = extractBundle, activate = bootstrap.activateRuntime,
82
108
  acquireLock = bootstrap.acquireCanonicalLock, health = require("./runtime-health.cjs").exactRuntimeHealth,
83
- recover = recovery.recoverWithCandidate,
109
+ recover = recovery.recoverWithCandidate, pauseRecovery = pauseRecoveryChecks,
84
110
  drain = require("./update-activity.cjs").drainCalls, download = bootstrap.downloadVerifiedArtifact,
85
111
  onProgress = () => {}, signal } = {}) {
86
112
  onProgress({ phase: "verifying", canCancel: false });
@@ -102,6 +128,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
102
128
  const journalPath = path.join(homeDir, ".relay", "application-migration.json");
103
129
  let releaseDrain;
104
130
  let downloadDirectory;
131
+ let pausedRecovery, succeeded = false;
105
132
  try {
106
133
  const recordedCurrent = recovery.marker(path.join(runtimeRoot, "current.json"));
107
134
  const removed = recovery.marker(path.join(homeDir, ".relay", "application-uninstall.json"));
@@ -171,6 +198,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
171
198
  onProgress({ phase: "ready", canCancel: false });
172
199
  return { ok: true, applicationOnly: true, owner, runtime: current, updateOwner: "canonical-runtime" };
173
200
  }
201
+ pausedRecovery = await pauseRecovery({ homeDir });
174
202
  if (bundle.receipt.runtimeDelivery === "download") {
175
203
  signal?.throwIfAborted();
176
204
  // Stage beside releases: tar requires archive and destination on one volume.
@@ -200,7 +228,8 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
200
228
  atomic(recoveryFile, { ...cleaned, schema: 1, state: "installing", releaseRoot, version: bundle.receipt.version });
201
229
  current = null; previousOwner = null; config = {};
202
230
  }
203
- require("./recovery-intent.cjs").setStopped(false, homeDir);
231
+ // A repair may have reset the recovery folder; keep checks paused until setup ends.
232
+ pausedRecovery.repause();
204
233
  const layout = { root: runtimeRoot, releaseId, releaseRoot, releasesDir: path.dirname(releaseRoot),
205
234
  packageRoot: runtime.packageRoot, pointerPath: path.join(runtimeRoot, "current.json"), lockPath: path.join(runtimeRoot, "transaction.lock") };
206
235
  const owner = { schema: 1, appId: APPLICATION_ID, platform: process.platform,
@@ -229,6 +258,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
229
258
  atomic(journalPath, { ...journal, state: "complete", completedAt: Date.now() });
230
259
  if (needsRecovery) atomic(recoveryFile, { ...recovery.marker(recoveryFile), state: "complete", completedAt: Date.now() });
231
260
  onProgress({ phase: "ready", canCancel: false });
261
+ succeeded = true;
232
262
  return { ok: true, recovered: needsRecovery, owner, runtime: result.candidate, updateOwner: "canonical-runtime" };
233
263
  } catch (error) {
234
264
  // Bootstrap owns service rollback. Only restore our launcher marker once
@@ -244,6 +274,7 @@ async function installFromApplication({ resourcesDir, applicationRoot, executabl
244
274
  } finally {
245
275
  try { releaseDrain?.(); }
246
276
  finally {
277
+ try { pausedRecovery?.resume(succeeded); } catch { /* recovery reads a missing intent as running */ }
247
278
  lock.release();
248
279
  if (downloadDirectory) fs.rmSync(downloadDirectory, { recursive: true, force: true });
249
280
  }
@@ -276,4 +307,4 @@ async function reconcileApplication({ homeDir = os.homedir(), acquireLock = boot
276
307
  } finally { lock.release(); }
277
308
  }
278
309
 
279
- module.exports = { verifyBundle, extractBundle, installFromApplication, reconcileApplication, versionCompare };
310
+ module.exports = { pauseRecoveryChecks, verifyBundle, extractBundle, installFromApplication, reconcileApplication, versionCompare };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.575",
3
+ "version": "0.1.577",
4
4
  "description": "Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.",
5
5
  "homepage": "https://sendrelays.com/get-started",
6
6
  "repository": {