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 handle
created: 2026-10-15T04:08:49Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 3d892bdf2a5a0db7
raw_hash: f681a895851ad4fb
vouched: 2026-10-22T16:18:52Z
body_hash: 41e3e91309de2927
raw_hash: 79444a43e01e6586
vouched: 2026-10-22T17:44:28Z
lines: 266-519
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function handle
created: 2026-10-15T06:00:20Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 3d892bdf2a5a0db7
raw_hash: f681a895851ad4fb
vouched: 2026-10-22T16:18:52Z
body_hash: 41e3e91309de2927
raw_hash: 79444a43e01e6586
vouched: 2026-10-22T17:44:28Z
lines: 281-561
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ anchor: function handle
created: 2026-10-19T21:09:57Z
norm: '1'
sig: e597b5fbc8e664ce
body_hash: 3d892bdf2a5a0db7
raw_hash: f681a895851ad4fb
vouched: 2026-10-22T16:18:52Z
body_hash: 41e3e91309de2927
raw_hash: 79444a43e01e6586
vouched: 2026-10-22T17:44:28Z
lines: 384-700
supersedes: 01M5APK9YCQH467YNMKAC8BT5B
---
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: 308268d80c20b68b
raw_hash: 417f8cf07e0e37ad
vouched: 2026-10-22T16:18:52Z
body_hash: 0b20e056c7f04b14
raw_hash: 2bff560cd8f039e6
vouched: 2026-10-22T17:44:28Z
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: 308268d80c20b68b
raw_hash: 417f8cf07e0e37ad
vouched: 2026-10-22T16:18:52Z
body_hash: 0b20e056c7f04b14
raw_hash: 2bff560cd8f039e6
vouched: 2026-10-22T17:44:28Z
lines: 1-1309
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5JBDJ3Q1B2CB27WSCJMMCP8
anchor: function people
created: 2026-10-22T17:44:22Z
norm: '1'
sig: d4fabfe66743b1ed
body_hash: d893a8d41c8d0aeb
raw_hash: a0226e2068ee94b3
lines: 315-322
---

the directory's hosts[].github is re-announced by each host every heartbeat, so it must override logins remembered from peers' hellos
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
id: 01M5JBDJ536ASQ9VKEE9T4WW7T
anchor: function greet
created: 2026-10-22T17:44:22Z
norm: '1'
sig: 583ffcbd3b791ed2
body_hash: f003a7d2404a7483
raw_hash: 18edf8ce741d1310
lines: 76-86
---

a hello without github leaves the peer in unnamed, and discover re-asks it even with a directory chosen, until a login arrives
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
id: 01M5JBDT9ZCB6X9ANFR2K7NJBE
anchor: function discover
created: 2026-10-22T17:44:31Z
norm: '1'
sig: ab69f97386d0db50
body_hash: 687af47f626b9aa1
raw_hash: ce225aafa3f68775
lines: 37-74
supersedes: 01M4YX4FKJR9W30M9Y5HVTKBW4
---

a peer's hello can arrive before its own gh lookup resolves and carry no github; peers never push a later login, so discover re-asks such peers every pass, even with a directory chosen
27 changes: 12 additions & 15 deletions apps/host/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,13 @@ async function owners(own?: Listing[]): Promise<void> {
await lobbyProjects(own);
}

/** Fresher sources win: the directory, then reachable peers, then this host's own owner. */
/**
* Fresher sources win: peers' hellos, then the directory, which each host re-announces itself to
* on every heartbeat while a hello is asked only on connect, then this host's own owner.
*/
function people(): People {
const known: People = {};
const known: People = { ...peers.people() };
for (const host of directory.read().hosts) if (host.github) known[host.login] = host.github;
Object.assign(known, peers.people());
Object.assign(known, demo.people());
const own = github.login(loginChanged);
if (own) known[catalog.user] = own;
Expand Down Expand Up @@ -721,23 +723,18 @@ async function handle(
case "team-peers":
return peers.teams();
case "team-roster": {
const gh = github.login(loginChanged);
const roster: Member[] = [
{
host: name,
login: catalog.user,
...(gh ? { github: gh } : {}),
online: true,
self: true,
},
{ host: name, login: catalog.user, online: true, self: true },
...peers.online(),
];
const seen = new Set(roster.map((member) => member.host));
for (const { name: host, login, github: owner } of directory.read().hosts) {
if (seen.has(host)) continue;
roster.push({ host, login, ...(owner ? { github: owner } : {}), online: false });
for (const { name: host, login } of directory.read().hosts) {
if (!seen.has(host)) roster.push({ host, login, online: false });
}
return roster;
const known = people();
return roster.map((member) =>
known[member.login] ? { ...member, github: known[member.login] } : member
);
}
case "team-ask": {
const team = await peers.ask(request.host);
Expand Down
32 changes: 15 additions & 17 deletions apps/host/src/peers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const remembered = new Map<string, Listing[]>();
const stalled = new Map<string, number>();
/** Peer owners' GitHub logins by Tailscale login, from their last hello, so remembered channels keep their owner's name and avatar. */
const named: People = {};
/** Peers whose last hello carried no GitHub login, asked again until one does. */
const unnamed = new Set<string>();

function emit() {
for (const listener of listeners) listener();
Expand Down Expand Up @@ -63,18 +65,21 @@ async function discover(signal: AbortSignal) {
});
hosts.set(name, host);
}
// A teammate can start a directory after this host first greeted them.
if (directory.isChosen()) return;
for (const { client, machine } of hosts.values()) {
if (client.status === "open") greet(client, machine);
// A teammate can start a directory after this host first greeted them, and a peer that answered
// before `gh` resolved has a login to give.
const chosen = directory.isChosen();
for (const [name, { client, machine }] of hosts) {
if (client.status === "open" && (!chosen || unnamed.has(name))) greet(client, machine);
}
}

function greet(client: GatewayClient, machine: Machine) {
client.request<Hello>({ op: "hello" }).then((hello) => {
if (hello.directory) void directory.adopt(hello.directory);
// A freshly started peer answers before `gh` has told it its owner's login.
if (!hello.github || named[machine.login] === hello.github) return;
// A freshly started peer answers before `gh` has told it its owner's login, which may have changed.
if (!hello.github) return void unnamed.add(machine.name);
unnamed.delete(machine.name);
if (named[machine.login] === hello.github) return;
named[machine.login] = hello.github;
emit();
}, () => {});
Expand Down Expand Up @@ -167,18 +172,11 @@ export async function teams(): Promise<TeamPeer[]> {
return found.filter((peer) => !!peer);
}

/** Hosts running Ace on the tailnet now. */
/** Hosts running Ace on the tailnet now; the gateway names their owners from every source. */
export function online(): Member[] {
return [...hosts.values()].flatMap(({ machine, client }) => {
if (client.status !== "open") return [];
const github = named[machine.login];
return [{
host: machine.name,
login: machine.login,
...(github ? { github } : {}),
online: true,
}];
});
return [...hosts.values()].flatMap(({ machine, client }) =>
client.status === "open" ? [{ host: machine.name, login: machine.login, online: true }] : []
);
}

/** Waits for the peer's owner to approve; see asks.ts. */
Expand Down