Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function main
created: 2026-10-15T04:47:23Z
norm: '1'
sig: 7d5d3f63a3bddde4
body_hash: e48607de6d665818
raw_hash: a6282bc5158525e4
vouched: 2026-10-22T15:17:06Z
body_hash: 2189bea8448ae31b
raw_hash: 1b839377f7aa43ec
vouched: 2026-10-22T15:20:51Z
lines: 253-482
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function main
created: 2026-10-22T14:31:16Z
norm: '1'
sig: 7d5d3f63a3bddde4
body_hash: e48607de6d665818
raw_hash: a6282bc5158525e4
vouched: 2026-10-22T15:17:07Z
body_hash: 2189bea8448ae31b
raw_hash: 1b839377f7aa43ec
vouched: 2026-10-22T15:20:51Z
lines: 332-614
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-15T03:39:45Z
norm: '1'
sig: 171797818ef4d836
body_hash: 1332a8dc2a8a58e7
raw_hash: 99138b0aa8226285
vouched: 2026-10-22T15:02:43Z
body_hash: 6333f6a695d3ff98
raw_hash: ad0574ab5dfd2999
vouched: 2026-10-22T15:20:51Z
lines: 668-751
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function serve
created: 2026-10-15T10:17:10Z
norm: '1'
sig: 171797818ef4d836
body_hash: 1332a8dc2a8a58e7
raw_hash: 99138b0aa8226285
vouched: 2026-10-22T15:02:43Z
body_hash: 6333f6a695d3ff98
raw_hash: ad0574ab5dfd2999
vouched: 2026-10-22T15:20:51Z
lines: 830-948
supersedes: 01M4Z2R1HEVDKEA8VEJ7XRJQ8F
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-20T17:04:56Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: 38cce3f1f25177dd
raw_hash: e92b21447ef6a159
vouched: 2026-10-22T15:09:48Z
body_hash: 2c286a27ab7e9d25
raw_hash: 0da8b2103ee9b539
vouched: 2026-10-22T15:20:51Z
lines: 1-1153
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: '@file'
created: 2026-10-22T14:00:47Z
norm: '1'
sig: 1acdc0f6a0895ac5
body_hash: 38cce3f1f25177dd
raw_hash: e92b21447ef6a159
vouched: 2026-10-22T15:09:55Z
body_hash: 2c286a27ab7e9d25
raw_hash: 0da8b2103ee9b539
vouched: 2026-10-22T15:20:51Z
lines: 1-1309
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5J36RXR2GN6T4P61RJKG55Y
anchor: function claim
created: 2026-10-22T15:20:51Z
norm: '1'
sig: fe5a25627e23d6bd
body_hash: dffc6420f4a82617
raw_hash: 5e5be372f64ebf07
lines: 1172-1183
---

serve checks $ACE_HOME/host.port before resume() and writes it after listen; a second host on another port must refuse while the first still answers /health with the same home
21 changes: 17 additions & 4 deletions apps/host/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,14 @@ async function main() {
case "fork":
return console.log((await fork(record.id, catalog.user)).name);
case "kill":
// A running host owns the channel's terminals and closes them with it, as for the app.
if (await isHostRunning()) {
return void (await local({
op: "channel",
channel: record.id,
request: { op: "kill", author: catalog.user },
}));
}
return kill(record.id);
case "move": {
// `--hosted <url>` only finishes a move to a hosting service that an earlier version started.
Expand Down Expand Up @@ -639,11 +647,16 @@ async function main() {
);
}
case "archive":
return void (await archive(record.id, true));
case "unarchive":
return void (await archive(record.id, false));
case "unarchive": {
const archived = command === "archive";
if (await isHostRunning()) {
return void (await local({ op: "archive", channel: record.id, archived }));
}
return void (await archive(record.id, archived));
}
case "delete":
await remove(record.id);
if (await isHostRunning()) await local({ op: "delete", channel: record.id });
else await remove(record.id);
return console.log(`Deleted ${record.name}. Its lane branches remain in ${record.project}.`);
}
}
Expand Down
21 changes: 20 additions & 1 deletion apps/host/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as served from "./directory-server";
import { retryLoginShell, shell } from "./environment";
import type { GatewayClient, Watch } from "./gateway-client";
import * as github from "./github";
import { health, HostConflict } from "./health";
import { describe, health, HostConflict } from "./health";
import { seal } from "./keys";
import * as lobby from "./lobby";
import { failure, log, open as openLog } from "./log";
Expand Down Expand Up @@ -1165,6 +1165,23 @@ async function listen(port: number): Promise<Server<Client>> {
}
}

/** The port of the host serving this home, so a second host on another port finds it. */
const SERVED = () => join(config.home, "host.port");

/** Two hosts on one home would run the same channels' workers and stores twice; the first keeps it. */
async function claim(port: number) {
const held = Number(await Bun.file(SERVED()).text().catch(() => ""));
if (!held || held === port) return;
const info = await health(held).catch((error) =>
error instanceof HostConflict ? error : undefined
);
if (info?.home !== config.home) return;
const listener = info instanceof HostConflict ? info.listener : describe(info);
throw new Error(
`${config.home} is already served by ${listener.text} on port ${held}. Stop it, or set ACE_HOME to give this host its own data directory.`,
);
}

/**
* `loopback` keeps a disposable host off the team: no tailnet listener, peer discovery, directory
* entry, or lobby presence. A second host on this machine would otherwise publish under the
Expand All @@ -1178,8 +1195,10 @@ export async function serve(port: number, loopback = false, script?: string): Pr
// Hosted channels run tools in this process.
seal();
openLog("host");
await claim(port);
await resume();
const server = await listen(port);
await Bun.write(SERVED(), String(server.port));
log("info", "host.start", { port: server.port, user: catalog.user, bun: Bun.version });
if (shell.state === "pending") {
log("warn", "host.shell", { error: shell.error });
Expand Down