From ce79c7744620bc93102a03164dd5b78da983e12d Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 17:42:28 -0400 Subject: [PATCH 1/3] fix(host): restart Ace Helper by itself when Tailscale starts or changes tailnet Why: apps/host/src/gateway.ts#function serve | Ace Helper's tailnet self-restart exits non-zero because launchd's KeepAlive relaunches only a failed exit Why: apps/host/src/tailnet.ts#function watch | Tailscale stopping must not restart the host; only a running tailnet other than the one the host started on does --- apps/host/src/catalog.ts | 11 ++++++----- apps/host/src/gateway.ts | 13 ++++++++++--- apps/host/src/tailnet.ts | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/apps/host/src/catalog.ts b/apps/host/src/catalog.ts index c3b32b2f..443a3e51 100644 --- a/apps/host/src/catalog.ts +++ b/apps/host/src/catalog.ts @@ -199,12 +199,13 @@ export function fork(source: Listing, model: ModelRef | undefined, branches: Set /** A new channel's record, not yet saved: a hosted channel saves it once the service has it. */ export function draft(project: string, model?: ModelRef, name?: string, hosted?: string): Listing { if (!tailnet) { - // The tailnet is read once at startup, so Tailscale started later needs a restart. - const host = config.helper ? "Ace Helper" : "ace serve"; + // The tailnet is read once at startup; Ace Helper restarts itself when Tailscale starts later. throw new Error( - identity().tailnet - ? `Restart ${host}: it started before Tailscale was running.` - : "Start Tailscale first: every channel belongs to a tailnet.", + !identity().tailnet + ? "Start Tailscale first: every channel belongs to a tailnet." + : config.helper + ? "Ace Helper is restarting to use Tailscale. Try again in a moment." + : "Restart ace serve: it started before Tailscale was running.", ); } const names = taken(); diff --git a/apps/host/src/gateway.ts b/apps/host/src/gateway.ts index 81dbcba6..3a920493 100644 --- a/apps/host/src/gateway.ts +++ b/apps/host/src/gateway.ts @@ -51,7 +51,7 @@ import { projectId, type WorkerRequest, } from "./protocol"; -import { type Machine, self, whois } from "./tailnet"; +import { type Machine, self, watch as tailscale, whois } from "./tailnet"; import { checkKey, forgetKey, @@ -1231,7 +1231,7 @@ export async function serve(port: number, loopback = false, script?: string): Pr tick = now; }, 1000); cleanup.push(() => clearInterval(stalls)); - async function stop() { + async function stop(code = 0) { if (closing) return; closing = true; log("info", "host.stop", { pending: pending.size }); @@ -1244,10 +1244,17 @@ export async function serve(port: number, loopback = false, script?: string): Pr log("error", "host.shutdown.failed", failure(result.reason)); } } - process.exit(results.some((result) => result.status === "rejected") ? 1 : 0); + process.exit(results.some((result) => result.status === "rejected") ? 1 : code); } process.on("SIGTERM", () => void stop()); process.on("SIGINT", () => void stop()); + // The owner's login, the channels' tailnet, and every tailnet listener are read once at startup. + cleanup.push(tailscale(catalog.tailnet, (tailnet) => { + log("info", "host.tailnet.moved", { from: catalog.tailnet, to: tailnet }); + // launchd starts Ace Helper again after a failed exit, which picks up the new tailnet. + if (config.helper) void stop(75); + else console.log(`Tailscale is on ${tailnet} now. Restart ace serve to use it.`); + })); console.log(`Ace on http://${server.hostname}:${server.port}`); // Ace Helper's console is its log file, which must not hold the token. if (!config.helper) { diff --git a/apps/host/src/tailnet.ts b/apps/host/src/tailnet.ts index 57bf5670..ab80b581 100644 --- a/apps/host/src/tailnet.ts +++ b/apps/host/src/tailnet.ts @@ -106,6 +106,24 @@ export async function diagnostics(started?: string): Promise void): () => void { + let seen: string | undefined; + const timer = setInterval(async () => { + const now = (await status())?.MagicDNSSuffix; + if (!now || now === started) seen = undefined; + else if (now !== seen) seen = now; + else { + clearInterval(timer); + moved(now); + } + }, 10_000); + return () => clearInterval(timer); +} + /** Online machines owned by people; tagged nodes are servers, not participants. */ export async function peers(): Promise { const value = await status(); From d2806687aa397a3ccf944f037fde194374e820d7 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 17:42:38 -0400 Subject: [PATCH 2/3] chore: confirm lane notes --- ...2ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md | 6 +++--- ...YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md | 6 +++--- ...ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md | 6 +++--- ...D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md | 6 +++--- ...HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md | 6 +++--- ...JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md | 5 +++-- ...JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md | 8 ++++++++ ...5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md | 8 ++++++++ 8 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 .lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md create mode 100644 .lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md diff --git a/.lane/memory/apps/host/src/catalog.ts/01M52ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md b/.lane/memory/apps/host/src/catalog.ts/01M52ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md index 69672aff..c85ab2d7 100644 --- a/.lane/memory/apps/host/src/catalog.ts/01M52ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md +++ b/.lane/memory/apps/host/src/catalog.ts/01M52ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md @@ -4,9 +4,9 @@ anchor: function draft created: 2026-10-16T18:32:55Z norm: '1' sig: 9775c8890db42443 -body_hash: 715d3c6a84062b19 -raw_hash: 4ac416bc7ff419f2 -vouched: 2026-10-21T21:54:51Z +body_hash: 6918a46a3994c088 +raw_hash: 8984d6500c60a112 +vouched: 2026-10-22T21:42:37Z lines: 133-155 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md b/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md index 1a64f1b2..9c27ae90 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md @@ -4,9 +4,9 @@ anchor: function serve created: 2026-10-15T03:39:45Z norm: '1' sig: 171797818ef4d836 -body_hash: 0f15dc30991bf467 -raw_hash: aa609814e48a6d89 -vouched: 2026-10-22T21:10:47Z +body_hash: f0a0ef2c205726cc +raw_hash: a98db292f7a9e13d +vouched: 2026-10-22T21:42:37Z lines: 668-751 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md b/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md index 13b94120..4d43e9c9 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md @@ -4,9 +4,9 @@ anchor: function serve created: 2026-10-15T10:17:10Z norm: '1' sig: 171797818ef4d836 -body_hash: 0f15dc30991bf467 -raw_hash: aa609814e48a6d89 -vouched: 2026-10-22T21:10:47Z +body_hash: f0a0ef2c205726cc +raw_hash: a98db292f7a9e13d +vouched: 2026-10-22T21:42:37Z lines: 830-948 supersedes: 01M4Z2R1HEVDKEA8VEJ7XRJQ8F --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md index 87799eda..747a13e8 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-20T17:04:56Z norm: '1' sig: 1acdc0f6a0895ac5 -body_hash: 734539b04f3c9fb5 -raw_hash: faceff1158ed3942 -vouched: 2026-10-22T21:30:21Z +body_hash: 94af0091ce8ea05b +raw_hash: 43d4d53a6b1fecd9 +vouched: 2026-10-22T21:42:37Z lines: 1-1153 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md b/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md index cdf92fe1..d4f13cd9 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-22T14:00:47Z norm: '1' sig: 1acdc0f6a0895ac5 -body_hash: 734539b04f3c9fb5 -raw_hash: faceff1158ed3942 -vouched: 2026-10-22T21:30:21Z +body_hash: 94af0091ce8ea05b +raw_hash: 43d4d53a6b1fecd9 +vouched: 2026-10-22T21:42:37Z lines: 1-1309 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md b/.lane/memory/apps/host/src/gateway.ts/01M5JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md index c6250da2..4d238023 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md @@ -4,8 +4,9 @@ anchor: function serve created: 2026-10-22T21:10:40Z norm: '1' sig: 171797818ef4d836 -body_hash: 0f15dc30991bf467 -raw_hash: aa609814e48a6d89 +body_hash: f0a0ef2c205726cc +raw_hash: a98db292f7a9e13d +vouched: 2026-10-22T21:42:37Z lines: 1191-1341 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md b/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md new file mode 100644 index 00000000..0735c474 --- /dev/null +++ b/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md @@ -0,0 +1,8 @@ +--- +id: 01M5JS1HEWX8RK77BQVSA9XQZG +anchor: function serve +created: 2026-10-22T21:42:29Z +norm: '1' +--- + +Ace Helper's tailnet self-restart exits non-zero because launchd's KeepAlive relaunches only a failed exit diff --git a/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md b/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md new file mode 100644 index 00000000..d28ce19b --- /dev/null +++ b/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md @@ -0,0 +1,8 @@ +--- +id: 01M5JS1HGCGP3SEXKET1EFDJ87 +anchor: function watch +created: 2026-10-22T21:42:29Z +norm: '1' +--- + +Tailscale stopping must not restart the host; only a running tailnet other than the one the host started on does From 98239e40206c52fc47e261cc77cc3518f8b2e4f7 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Thu, 22 Oct 2026 17:42:39 -0400 Subject: [PATCH 3/3] lane: sync fix-tailnet-pickup memory --- ...01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md | 4 ++++ .../01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md b/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md index 0735c474..838821cb 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md @@ -3,6 +3,10 @@ id: 01M5JS1HEWX8RK77BQVSA9XQZG anchor: function serve created: 2026-10-22T21:42:29Z norm: '1' +sig: 171797818ef4d836 +body_hash: f0a0ef2c205726cc +raw_hash: a98db292f7a9e13d +lines: 1200-1357 --- Ace Helper's tailnet self-restart exits non-zero because launchd's KeepAlive relaunches only a failed exit diff --git a/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md b/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md index d28ce19b..9fb6eefb 100644 --- a/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md +++ b/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md @@ -3,6 +3,10 @@ id: 01M5JS1HGCGP3SEXKET1EFDJ87 anchor: function watch created: 2026-10-22T21:42:29Z norm: '1' +sig: 6c1e8049ae309bc1 +body_hash: 8530c8fc8dec8e95 +raw_hash: 03c20bc7ed33888a +lines: 113-125 --- Tailscale stopping must not restart the host; only a running tailnet other than the one the host started on does